From e4e3aa287a7e28b0ff0a524a3ad00d89169984f4 Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Wed, 15 Jul 2026 16:34:40 -0230 Subject: [PATCH 1/2] initial --- docs/CHANGELOG.md | 1 + editor_patch/level.cpp | 68 ++++++++++++++++++++++++++++++++++++++++++ editor_patch/level.h | 21 +++++++++++++ 3 files changed, 90 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 38549112..8e459399 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -44,6 +44,7 @@ Version 1.4.0 (Lupin): Not yet released - Require a fresh `Alt` press to kill an unresponsive process - Add mini scoreboard HUD element to FFA game types - Add `ui_minisb_dm` console command to toggle whether mini scoreboard is displayed in DM mode +- Retain Glacier-specific chunks when RFLs loaded and re-saved in level editor [@is-this-c](https://github.com/is-this-c) - Add `Anti-aliasing` option to `ADVANCED` options panel diff --git a/editor_patch/level.cpp b/editor_patch/level.cpp index be907bbe..98b20441 100644 --- a/editor_patch/level.cpp +++ b/editor_patch/level.cpp @@ -100,12 +100,77 @@ CodeInjection CDedLevel_LoadLevel_patch1{ }, }; +// Capture Glacier-specific RFL chunks (0x6ED-prefixed ID) verbatim so they can be +// re-emitted unchanged on the next save. +static void retained_chunk_deserialize(CDedLevel& level, rf::File& file, uint32_t chunk_id, std::size_t chunk_len) +{ + auto& chunks = level.GetAlpineLevelProperties().retained_chunks; + std::size_t remaining = chunk_len; + + // Ensures the file advances to the chunk end even on a short read. + rf::File::ChunkGuard chunk_guard{file, remaining}; + + // A legitimate chunk can never be larger than the file itself. + int file_size = file.get_size(); + if (file_size < 0 || chunk_len > static_cast(file_size)) { + xlog::warn("[RetainedChunk] skipping chunk id=0x{:08X} with implausible len={} (file size={})", chunk_id, chunk_len, file_size); + // Leave the position at the chunk-data start and let the loader advance + // through it to EOF. + remaining = 0; + return; + } + + RetainedRflChunk chunk; + chunk.id = chunk_id; + + if (chunk_len > 0) { + chunk.data.resize(chunk_len); + int got = file.read(chunk.data.data(), chunk_len); + if (got <= 0 || file.error()) { + // Nothing usable read (truncated/corrupt source) — don't retain it. + if (got > 0) remaining -= got; + xlog::warn("[RetainedChunk] failed to read chunk id=0x{:08X} len={}", chunk_id, chunk_len); + return; + } + remaining -= got; + if (static_cast(got) < chunk_len) { + chunk.data.resize(got); + } + } + + xlog::debug("[RetainedChunk] retained Glacier chunk id=0x{:08X} len={}", chunk_id, chunk.data.size()); + chunks.push_back(std::move(chunk)); +} + +// Re-write all retained Glacier chunks verbatim, preserving their original IDs. +static void retained_chunks_serialize(CDedLevel& level, rf::File& file) +{ + auto& chunks = level.GetAlpineLevelProperties().retained_chunks; + for (const auto& chunk : chunks) { + auto start_pos = level.BeginRflSection(file, static_cast(chunk.id)); + if (!chunk.data.empty()) { + file.write(chunk.data.data(), chunk.data.size()); + } + level.EndRflSection(file, start_pos); + } +} + // load AlpineLevelProperties chunk from rfl file CodeInjection CDedLevel_LoadLevel_patch2{ 0x0042F2D4, [](auto& regs) { auto& file = *static_cast(regs.esi); + // Preserve unknown chunks from Glacier (0x6ED-prefixed IDs). + uint32_t raw_chunk_id = static_cast(regs.edi); + if (is_foreign_retained_chunk_id(raw_chunk_id)) { + auto& level = *static_cast(regs.ebp); + std::size_t chunk_size = regs.ebx; + retained_chunk_deserialize(level, file, raw_chunk_id, chunk_size); + regs.eip = 0x0043090C; + return; + } + // Alpine level properties chunk was introduced in rfl v302, no point looking for it before that if (file.check_version(302)) { auto& level = *static_cast(regs.ebp); @@ -767,6 +832,9 @@ CodeInjection CDedLevel_SaveLevel_patch{ // Write bag objects chunk bag_serialize_chunk(level, file); + + // Re-write any Glacier chunks + retained_chunks_serialize(level, file); }, }; diff --git a/editor_patch/level.h b/editor_patch/level.h index c8900494..09eab395 100644 --- a/editor_patch/level.h +++ b/editor_patch/level.h @@ -21,6 +21,22 @@ constexpr int alpine_corona_chunk_id = 0x0AFBAE03; constexpr int alpine_bag_chunk_id = 0x0AFBAE04; constexpr int alpine_brush_group_chunk_id = 0x0AFBAE05; // brush metadata in .rfg group files only +// Glacier saves new RFL chunks for its own purposes (metadata). Alpine Faction can +// neither read nor parse these, but AlpineEditor retains them verbatim on load and +// re-emits them on save so the originating editor can still read the file properly. +constexpr uint32_t foreign_chunk_id_mask = 0xFFF00000u; +constexpr uint32_t foreign_chunk_id_prefix = 0x6ED00000u; +inline bool is_foreign_retained_chunk_id(uint32_t id) +{ + return (id & foreign_chunk_id_mask) == foreign_chunk_id_prefix; +} + +// A retained RFL section captured verbatim from Glacier. +struct RetainedRflChunk { + uint32_t id; + std::vector data; +}; + // Per-entry data in the alpine_brush_group_chunk_id chunk (.rfg only). // Stores geoable/breakable flags for brushes by serialization index. #pragma pack(push, 1) @@ -370,6 +386,9 @@ struct AlpineLevelProperties // Alpine bag objects std::vector bag_objects; + // Retained Glacier RFL sections (0x6ED-prefixed IDs). + std::vector retained_chunks; + static constexpr std::uint32_t current_alpine_chunk_version = 4u; // defaults for existing levels, overwritten for maps with these fields in their alpine level props chunk @@ -414,6 +433,8 @@ struct AlpineLevelProperties delete b; } bag_objects.clear(); + + retained_chunks.clear(); } void Serialize(rf::File& file) const From 8bd014719036ca8cc6189cf672f66131f9b7b8ad Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Wed, 15 Jul 2026 16:49:47 -0230 Subject: [PATCH 2/2] fix --- editor_patch/level.cpp | 15 ++++++--------- editor_patch/level.h | 8 ++++---- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/editor_patch/level.cpp b/editor_patch/level.cpp index 98b20441..8be96dff 100644 --- a/editor_patch/level.cpp +++ b/editor_patch/level.cpp @@ -114,9 +114,9 @@ static void retained_chunk_deserialize(CDedLevel& level, rf::File& file, uint32_ int file_size = file.get_size(); if (file_size < 0 || chunk_len > static_cast(file_size)) { xlog::warn("[RetainedChunk] skipping chunk id=0x{:08X} with implausible len={} (file size={})", chunk_id, chunk_len, file_size); - // Leave the position at the chunk-data start and let the loader advance - // through it to EOF. + // Advance to EOF so the section loop terminates cleanly. remaining = 0; + file.seek(0, rf::File::seek_end); return; } @@ -126,16 +126,13 @@ static void retained_chunk_deserialize(CDedLevel& level, rf::File& file, uint32_ if (chunk_len > 0) { chunk.data.resize(chunk_len); int got = file.read(chunk.data.data(), chunk_len); - if (got <= 0 || file.error()) { - // Nothing usable read (truncated/corrupt source) — don't retain it. + // A short read means the source is truncated mid-chunk. + if (got <= 0 || file.error() || static_cast(got) < chunk_len) { if (got > 0) remaining -= got; - xlog::warn("[RetainedChunk] failed to read chunk id=0x{:08X} len={}", chunk_id, chunk_len); + xlog::warn("[RetainedChunk] failed to fully read chunk id=0x{:08X} (len={}, got={})", chunk_id, chunk_len, got); return; } remaining -= got; - if (static_cast(got) < chunk_len) { - chunk.data.resize(got); - } } xlog::debug("[RetainedChunk] retained Glacier chunk id=0x{:08X} len={}", chunk_id, chunk.data.size()); @@ -163,7 +160,7 @@ CodeInjection CDedLevel_LoadLevel_patch2{ // Preserve unknown chunks from Glacier (0x6ED-prefixed IDs). uint32_t raw_chunk_id = static_cast(regs.edi); - if (is_foreign_retained_chunk_id(raw_chunk_id)) { + if (is_glacier_chunk_id(raw_chunk_id)) { auto& level = *static_cast(regs.ebp); std::size_t chunk_size = regs.ebx; retained_chunk_deserialize(level, file, raw_chunk_id, chunk_size); diff --git a/editor_patch/level.h b/editor_patch/level.h index 09eab395..48d21aab 100644 --- a/editor_patch/level.h +++ b/editor_patch/level.h @@ -24,11 +24,11 @@ constexpr int alpine_brush_group_chunk_id = 0x0AFBAE05; // brush metadata in .rf // Glacier saves new RFL chunks for its own purposes (metadata). Alpine Faction can // neither read nor parse these, but AlpineEditor retains them verbatim on load and // re-emits them on save so the originating editor can still read the file properly. -constexpr uint32_t foreign_chunk_id_mask = 0xFFF00000u; -constexpr uint32_t foreign_chunk_id_prefix = 0x6ED00000u; -inline bool is_foreign_retained_chunk_id(uint32_t id) +constexpr uint32_t glacier_chunk_id_mask = 0xFFF00000u; +constexpr uint32_t glacier_chunk_id_prefix = 0x6ED00000u; +inline bool is_glacier_chunk_id(uint32_t id) { - return (id & foreign_chunk_id_mask) == foreign_chunk_id_prefix; + return (id & glacier_chunk_id_mask) == glacier_chunk_id_prefix; } // A retained RFL section captured verbatim from Glacier.