Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
65e09f6
add toml++ library
GooberRF Jul 24, 2025
39e6092
add work in progress asg savegame format
GooberRF Jul 24, 2025
c1c21b4
add savegame format switch
GooberRF Jul 24, 2025
7a467a4
add shortvector and shortquat
GooberRF Jul 24, 2025
67bd649
add is_set for timestamps
GooberRF Jul 24, 2025
62f8330
add structs for make_invulnerable and when_dead events, geomodcraterd…
GooberRF Jul 24, 2025
f7848de
add mapping for trigger_list
GooberRF Jul 24, 2025
51b06ad
add goal_create, alarm_siren, cyclic_timer support to asg
GooberRF Jul 24, 2025
cbf02fd
continue mapping cyclic_timer event struct, resolve alignment issues …
GooberRF Jul 24, 2025
8087dc0
serialize decals in asg
GooberRF Jul 24, 2025
cf5ee54
add new vector for destroyed detail rooms
GooberRF Jul 24, 2025
0fb9d37
add deleted detail rooms, particle emitters, bolt emitters to asg
GooberRF Jul 25, 2025
f3abae2
add movers to asg
GooberRF Jul 25, 2025
d7fd6f8
fix push region alignment issue, add push regions to asg
GooberRF Jul 25, 2025
ff26ca2
add weapons and corpses to asg
GooberRF Jul 25, 2025
db48f47
add blood pools to asg
GooberRF Jul 25, 2025
0e7bf67
add deserialization for entities, player, level info. fix serializati…
GooberRF Jul 27, 2025
16fe476
handle logged message loading, handle loading and deserializing from …
GooberRF Jul 28, 2025
0f1335b
deserialize and load events from asg file
GooberRF Jul 28, 2025
6dd0a99
delayed handler resolution for link targets
GooberRF Jul 28, 2025
c35b7a7
add trigger, emitter, mover, item, clutter, push region, decal deseri…
GooberRF Jul 28, 2025
65eb106
Merge branch 'master' into asg
GooberRF Jan 10, 2026
65039f1
Fix padding redundancy in GenericEvent struct
GooberRF Jan 10, 2026
3243145
Refactor string comparison functions for savegame
GooberRF Jan 10, 2026
4f654a6
Cast mover_flags to MoverFlags type
GooberRF Jan 10, 2026
c72e744
Replace mover_flags with rf::MoverFlags type
GooberRF Jan 10, 2026
f31a811
add corpses, blood pools, glass, weapons deserialization
GooberRF Jul 29, 2025
10a86e9
rename weapon struct specific friendliness param to weap_friendliness…
GooberRF Jan 14, 2026
7b0a9c7
small fixes
GooberRF Jan 14, 2026
a9e858d
fix timestamp serialization, fix some player fields
GooberRF Jan 14, 2026
5d87d54
fix delayed handles, dedupe deleted rooms
GooberRF Jan 14, 2026
5fe5ffc
remove max killed rooms limit, maintain level-specific deleted room u…
GooberRF Jan 14, 2026
8065ea1
fix fpgun_pos, fpgun_orient, and spew_pos, optimize clutter savegame …
GooberRF Jan 14, 2026
2557eca
support entity and clutter skins in asg
GooberRF Jan 15, 2026
a282482
support entity paths in asg, fix type issue with weapon index in asg
GooberRF Jan 15, 2026
d401517
fix default object life being 100, fix corpses not maintaining anim s…
GooberRF Jan 15, 2026
1f5ec65
fixes
GooberRF Jan 16, 2026
bfe51d3
support orientation for triggers in asg, add sp_savehighaccuracy comm…
GooberRF Jan 16, 2026
a5b36b6
fix saving in vehicles, fix ha values not always being properly parse…
GooberRF Jan 16, 2026
d2ac48c
stop tracking shortquat lossy orient for clutter and triggers due to …
GooberRF Jan 16, 2026
3b573a5
deserialize and parse deleted events, reimplement timestamp serializa…
GooberRF Jan 17, 2026
bd15d51
move SetiquidDepthEvent to events.cpp with other event structs; shift…
GooberRF Jan 17, 2026
d428b49
fix make block functions for Make_Invulnerable, Cyclic_Timer, Goal_Cr…
GooberRF Jan 17, 2026
d88201d
changelog
GooberRF Jan 17, 2026
1eef770
include active field for cyclic_timer, support pos and orient for tel…
GooberRF Jan 18, 2026
51b5d4d
optimizations and warnings for corrupt data reads
GooberRF Jan 18, 2026
ca9b880
remove unneeded event fields
GooberRF Jan 18, 2026
0da7e76
support new events
GooberRF Jan 18, 2026
f0334f5
consolidate dir and pos events, add support for play_sound, goto, sho…
GooberRF Jan 18, 2026
d9c6b29
support dynamic lights
GooberRF Jan 19, 2026
077cc30
track trigger reset timestamp
GooberRF Jan 19, 2026
1eb30c6
shift high accuracy mode to speedrun compatibility mode
GooberRF Jan 20, 2026
9a7c88b
fix restoring active ai paths
GooberRF Jan 20, 2026
3bcc5e7
Merge branch 'master' into asg
GooberRF Feb 3, 2026
1ad23e2
add Require Alpine Savegame Format to af_game.tbl
GooberRF Feb 3, 2026
5a00a78
Merge branch 'master' into asg
GooberRF Mar 5, 2026
40bde78
add dead entities, add clutter and entities not in level, consolidate…
GooberRF Mar 6, 2026
d3aed3e
Merge commit 'cd67f371' into asg
GooberRF Apr 22, 2026
b5a76f9
fix build error
GooberRF Apr 22, 2026
b3bac04
fix build
GooberRF Apr 22, 2026
1d9e3ba
Merge branch 'master' into asg
GooberRF May 18, 2026
99d254b
Merge remote-tracking branch 'origin/master' into asg
GooberRF May 31, 2026
fa4e3b0
fix
GooberRF May 31, 2026
9323b07
fix event records, support breakable detail brushes with partial dama…
GooberRF Jun 18, 2026
0302ff0
support rf2 style geomod, support liquid room depth, fix bugs
GooberRF Jul 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions common/include/common/utils/string-utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ inline std::string_view trim(std::string_view s)
return rtrim(ltrim(s));
}

inline std::string_view unquote(std::string_view s)
{
if (s.size() >= 2 && s.front() == '"' && s.back() == '"')
return s.substr(1, s.size() - 2);
return s;
}

inline std::pair<std::string_view, std::string_view> split_once_whitespace(std::string_view s)
{
s = trim(s);
Expand Down
1 change: 1 addition & 0 deletions common/include/common/version/version.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#define ADS_VERSION 2 // .toml dedicated server config files
#define AFS_VERSION 14 // alpine_settings.ini
#define AFCC_VERSION 2 // alpine_system.ini
#define ASG_VERSION 2 // .asg savegame files

// Increment only when absolutely necessary! Will break level compatibility for older clients.
// Stock RF1 uses versions 156 - 200. RF2 uses version 295
Expand Down
13 changes: 13 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ Version 1.4.0 (Lupin): Not yet released
- Fine tuned texture animation without being subject to VBM format limitations
- Accessible to event system via handle for level-driven behaviour scripting
- Support PNG and JPG texture formats in game and level editor
- Implement TOML-based `ASG` savegame file format for single player
- Support (theoretically) any number of each saved object type, removing specific limits
- Support tracking orientation for triggers and clutter (disabled when `sp_savespeedrunmode` is enabled)
- Support tracking trigger reset timestamps (disabled when `sp_savespeedrunmode` is enabled)
- Support tracking skins for clutter and entities
- Support tracking all event links
- Support tracking positions and rotations for relevant event types
- Support tracking dynamic lights
- Support Alpine events `Switch_Random`, `Sequence`, and `World_HUD_Sprite`
- Support persisting destroyed breakable detail brushes of all materials, and partial damage to non-glass breakable brushes
- Support persisting brush-based (RF2-style) geomod destruction
- Fix collision bugs resulting from compressed position/orientation values (disabled when `sp_savespeedrunmode` is enabled)
- Add support for `Require Alpine Savegame Format` in `af_game.tbl` for TC mods that want to force the modern format
- Add Bagman (`BM`) and Team Bagman (`TBM`) game types

### Minor features, changes, and enhancements
Expand Down
3 changes: 3 additions & 0 deletions game_patch/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,8 @@ set(SRCS
misc/alpine_options.cpp
misc/alpine_settings.h
misc/alpine_settings.cpp
misc/alpine_savegame.h
misc/alpine_savegame.cpp
sound/sound.cpp
sound/sound.h
sound/sound_ds.cpp
Expand Down Expand Up @@ -316,6 +318,7 @@ target_include_directories(AlpineFaction PRIVATE
${CMAKE_SOURCE_DIR}/vendor/stb
${CMAKE_SOURCE_DIR}/vendor/freetype/include
${CMAKE_SOURCE_DIR}/vendor/nlohmann
${CMAKE_SOURCE_DIR}/vendor/toml++
)

if(NOT MSVC)
Expand Down
27 changes: 23 additions & 4 deletions game_patch/bmpman/atx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,16 @@ namespace
// Single registry keyed by lowercase basename. With ATX supercede in effect a caller may
// load the same file under any extension (foo.tga, foo.dds, foo.atx, foo) — they all
// resolve to the same controller, so basename is the right key.
std::unordered_map<std::string, std::unique_ptr<AtxController>> g_controllers;
//
// Intentionally heap-allocated and never destructed. AtxController's destructor calls
// rf::bm::release() on its child handles, which is only valid while RF.exe's bitmap manager
// is alive. Controllers are torn down at level transitions (atx_level_reset, below) — the
// correct time. If this container instead destructed at process exit (it would, as a normal
// static object when the user quits with a level still loaded), those AtxController dtors
// would run AFTER RF.exe has shut down its bitmap manager, double-freeing bm slots and
// crashing on quit.
std::unordered_map<std::string, std::unique_ptr<AtxController>>& g_controllers =
*new std::unordered_map<std::string, std::unique_ptr<AtxController>>();

// Basenames whose .atx file has been seen this level and failed to parse/validate. Cached so
// we don't re-run the failing parse on every subsequent load of any extension that supercedes
Expand Down Expand Up @@ -484,10 +493,20 @@ rf::bm::Format lock_atx_bitmap(rf::bm::BitmapEntry& bm_entry, void** pixels_out,
*pixels_out = nullptr;
*palette_out = nullptr;

auto it = g_controllers.find(key_from_bm_name(bm_entry.name));
const std::string key = key_from_bm_name(bm_entry.name);
auto it = g_controllers.find(key);
if (it == g_controllers.end()) {
xlog::warn("ATX: lock for unknown '{}'", bm_entry.name);
return rf::bm::FORMAT_NONE;
// Safely handle cache.
if (!g_failed.contains(key)) {
int w = 0, h = 0, levels = 0, frames = 0;
rf::bm::Format fmt = rf::bm::FORMAT_NONE;
read_atx_header((key + ".atx").c_str(), &w, &h, &fmt, &levels, &frames);
it = g_controllers.find(key);
}
if (it == g_controllers.end()) {
xlog::warn("ATX: lock for unknown '{}'", bm_entry.name);
return rf::bm::FORMAT_NONE;
}
}
AtxController& c = *it->second;
// Insertions are idempotent. We track every bm_handle that locks this controller so
Expand Down
48 changes: 16 additions & 32 deletions game_patch/misc/alpine_options.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "alpine_options.h"
#include <common/utils/string-utils.h>
#include <patch_common/CallHook.h>
#include <patch_common/FunHook.h>
#include <patch_common/CodeInjection.h>
Expand Down Expand Up @@ -69,37 +70,19 @@ bool is_known_run_level(const std::string& filename)
return g_known_run_levels.contains(string_to_lower(level_name));
}

// trim leading and trailing whitespace
std::string trim(const std::string& str, bool remove_quotes = false)
{
auto start = str.find_first_not_of(" \t\n\r");
auto end = str.find_last_not_of(" \t\n\r");
if (start == std::string::npos) {
return "";
}

std::string trimmed = str.substr(start, end - start + 1);

// extract quoted value
if (remove_quotes && trimmed.size() >= 2 && trimmed.front() == '"' && trimmed.back() == '"') {
trimmed = trimmed.substr(1, trimmed.size() - 2);
}

return trimmed;
}

// ===== Parsers for AlpineOptions =====

// strings can be provided in quotation marks or not
std::optional<OptionValue> parse_string(const std::string& value)
{
return trim(value, true);
return std::string{unquote(trim(value))};
}

// colors can be provided in quotation marks or not, and can be hex formatted or RF-style
std::optional<OptionValue> parse_color(const std::string& value)
{
std::string trimmed_value = trim(value, true);
std::string trimmed_value{unquote(trim(value))};

try {
// Check if it's a valid hex string (6 or 8 characters)
Expand Down Expand Up @@ -186,6 +169,7 @@ const std::unordered_map<std::string, OptionMetadata> option_metadata = {
{"$Multi Timer Y Offset", {AlpineOptionID::MultiTimerYOffset, "af_client.tbl", parse_int}},
{"$Multi Timer Color", {AlpineOptionID::MultiTimerColor, "af_client.tbl", parse_color}},
{"$Default Third Person", {AlpineOptionID::DefaultThirdPerson, "af_game.tbl", parse_bool}},
{"$Require Alpine Savegame Format", {AlpineOptionID::RequireAlpineSavegameFormat, "af_game.tbl", parse_bool}},
};

// ===== Parsers for Alpine level info =====
Expand Down Expand Up @@ -214,12 +198,12 @@ std::optional<LevelInfoValue> parse_bool_level(const std::string& value)
}
std::optional<LevelInfoValue> parse_string_level(const std::string& value)
{
return trim(value, true);
return std::string{unquote(trim(value))};
}

std::optional<LevelInfoValue> parse_color_level(const std::string& value)
{
std::string trimmed_value = trim(value, true);
std::string trimmed_value{unquote(trim(value))};

try {
// Check if it's a valid hex string (6 or 8 characters)
Expand Down Expand Up @@ -261,13 +245,13 @@ std::optional<LevelInfoValue> parse_color_level(const std::string& value)

std::optional<std::pair<std::string, std::string>> parse_mesh_replacement(const std::string& value)
{
std::string trimmed_value = trim(value, true);
std::string trimmed_value{unquote(trim(value))};
std::regex mesh_replacement_pattern("\\{\\s*\"([^\"]+)\"\\s*,\\s*\"([^\"]+)\"\\s*\\}");
std::smatch matches;

if (std::regex_match(trimmed_value, matches, mesh_replacement_pattern)) {
if (matches.size() == 3) { // Full match and 2 filename captures
return std::make_pair(trim(matches[1].str(), true), trim(matches[2].str(), true));
return std::make_pair(std::string{unquote(trim(matches[1].str()))}, std::string{unquote(trim(matches[2].str()))});
}
}
return std::nullopt;
Expand Down Expand Up @@ -340,7 +324,7 @@ void load_level_info_config(const std::string& level_filename)

// Search for #Start marker
while (std::getline(file_stream, line)) {
line = trim(line, false);
line = std::string{trim(line)};
if (line == "#Start") {
found_start = true;
break;
Expand All @@ -355,7 +339,7 @@ void load_level_info_config(const std::string& level_filename)
// Process options until #End marker is found
bool found_end = false;
while (std::getline(file_stream, line)) {
line = trim(line, false);
line = std::string{trim(line)};

if (line == "#End") {
found_end = true;
Expand All @@ -370,8 +354,8 @@ void load_level_info_config(const std::string& level_filename)
continue;
}

std::string option_name = trim(line.substr(0, delimiter_pos), false);
std::string option_value = trim(line.substr(delimiter_pos + 1), false);
std::string option_name{trim(line.substr(0, delimiter_pos))};
std::string option_value{trim(line.substr(delimiter_pos + 1))};

// handle mesh replacements
if (option_name == "$Mesh Replacement") {
Expand Down Expand Up @@ -787,7 +771,7 @@ void load_single_af_options_file(const std::string& file_name)

// Search for #Start marker
while (std::getline(file_stream, line)) {
line = trim(line, false);
line = std::string{trim(line)};
if (line == "#Start") {
found_start = true;
break;
Expand All @@ -802,7 +786,7 @@ void load_single_af_options_file(const std::string& file_name)
// Process options until #End marker is found
bool found_end = false;
while (std::getline(file_stream, line)) {
line = trim(line, false);
line = std::string{trim(line)};

if (line == "#End") {
found_end = true;
Expand All @@ -817,8 +801,8 @@ void load_single_af_options_file(const std::string& file_name)
continue;
}

std::string option_name = trim(line.substr(0, delimiter_pos), false);
std::string option_value = trim(line.substr(delimiter_pos + 1), false);
std::string option_name{trim(line.substr(0, delimiter_pos))};
std::string option_value{trim(line.substr(delimiter_pos + 1))};

// Handle af_level_quirks.tbl
if (file_name == "af_level_quirks.tbl" &&
Expand Down
1 change: 1 addition & 0 deletions game_patch/misc/alpine_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ enum class AlpineOptionID
MultiTimerYOffset,
MultiTimerColor,
DefaultThirdPerson,
RequireAlpineSavegameFormat,
_optioncount // dummy for total count
};

Expand Down
Loading
Loading