diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ff16d431..6cadf2de 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -45,6 +45,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 - Add `Attach Spectate Camera` control to toggle spectate between following a player and a detached free camera - Add `Change Spectate View` control to switch first/third person while following a player, or free look/static camera while detached - Add a middle mouse toggled orbit camera to third person spectate diff --git a/editor_patch/level.cpp b/editor_patch/level.cpp index be907bbe..8be96dff 100644 --- a/editor_patch/level.cpp +++ b/editor_patch/level.cpp @@ -100,12 +100,74 @@ 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); + // Advance to EOF so the section loop terminates cleanly. + remaining = 0; + file.seek(0, rf::File::seek_end); + 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); + // 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 fully read chunk id=0x{:08X} (len={}, got={})", chunk_id, chunk_len, got); + return; + } + remaining -= 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_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); + 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 +829,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..48d21aab 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 glacier_chunk_id_mask = 0xFFF00000u; +constexpr uint32_t glacier_chunk_id_prefix = 0x6ED00000u; +inline bool is_glacier_chunk_id(uint32_t id) +{ + return (id & glacier_chunk_id_mask) == glacier_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