Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
65 changes: 65 additions & 0 deletions editor_patch/level.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::size_t>(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;
}
Comment thread
GooberRF marked this conversation as resolved.

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<std::size_t>(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<int>(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<rf::File*>(regs.esi);

// Preserve unknown chunks from Glacier (0x6ED-prefixed IDs).
uint32_t raw_chunk_id = static_cast<uint32_t>(regs.edi);
if (is_glacier_chunk_id(raw_chunk_id)) {
auto& level = *static_cast<CDedLevel*>(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<CDedLevel*>(regs.ebp);
Expand Down Expand Up @@ -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);
},
};

Expand Down
21 changes: 21 additions & 0 deletions editor_patch/level.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint8_t> 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)
Expand Down Expand Up @@ -370,6 +386,9 @@ struct AlpineLevelProperties
// Alpine bag objects
std::vector<DedBag*> bag_objects;

// Retained Glacier RFL sections (0x6ED-prefixed IDs).
std::vector<RetainedRflChunk> 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
Expand Down Expand Up @@ -414,6 +433,8 @@ struct AlpineLevelProperties
delete b;
}
bag_objects.clear();

retained_chunks.clear();
}

void Serialize(rf::File& file) const
Expand Down
Loading