From 5cf265695258df3c9d092cfd48a452143701788b Mon Sep 17 00:00:00 2001 From: Christian Semmler Date: Fri, 24 Jul 2026 23:18:06 -0700 Subject: [PATCH 1/3] lib: preserve MxDSSelectAction list extension through Read/Write MxDSSelectAction child lists ('Act_'/'RAND') replace the regular list count with a null-terminated selector string (e.g. "RANDOM_3"), the actual count, and one null-terminated value string per child. Read previously consumed this extension only for the description stream (assuming single-character values) and Write always emitted a bare child count, corrupting select actions in rewritten files. Retain the raw extension bytes on the object and re-emit them verbatim on write. Verified byte-exact roundtrip of the extension on the four retail SI files that use it (COPTER.SI, DUNECAR.SI, JETSKI.SI, RACECAR.SI). --- include/libweaver/object.h | 5 +++++ lib/interleaf.cpp | 40 ++++++++++++++++++++++++-------------- lib/object.cpp | 1 + 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/include/libweaver/object.h b/include/libweaver/object.h index 24ff6b2..40869ff 100644 --- a/include/libweaver/object.h +++ b/include/libweaver/object.h @@ -66,6 +66,11 @@ class Object : public Core uint32_t time_offset_; + /// Raw MxDSSelectAction child list extension (selector string, count and + /// value strings), empty for regular child lists. Preserved verbatim + /// between Read and Write. + bytearray list_extension_; + ChunkedData data_; private: diff --git a/lib/interleaf.cpp b/lib/interleaf.cpp index e1b2e20..a469160 100644 --- a/lib/interleaf.cpp +++ b/lib/interleaf.cpp @@ -144,23 +144,29 @@ Interleaf::Error Interleaf::ReadChunk(Core *parent, FileBase *f, Info *info) list_count = f->ReadU32(); if (list_count == LIST::Act_ || list_count == LIST::RAND) { - desc << "Extension: "; - if (list_count == LIST::RAND) { - uint32_t rand_upper = f->ReadU32(); - uint64_t rand_val = uint64_t(rand_upper) << 32 | list_count; - f->seek(1, File::SeekCurrent); - desc << ((const char *) &rand_val); - } else if (list_count == LIST::Act_) { - desc << ((const char *) &list_count); - } - desc << std::endl; + // MxDSSelectAction extension: instead of a count, the list starts + // with a null-terminated selector string (e.g. "RANDOM_3") whose + // first four characters were just read, followed by the actual count + // and one null-terminated value string per child. Retain the raw + // bytes on the parent object so Write can reproduce them. + bytearray ext; + ext.append((const char *) &list_count, sizeof(list_count)); + + std::string selector = f->ReadString(); + ext.append(selector.c_str(), selector.size() + 1); + desc << "Extension: " << RIFF::PrintU32AsString(list_count) << selector << std::endl; - // Re-read list count list_count = f->ReadU32(); + ext.append((const char *) &list_count, sizeof(list_count)); + for (uint32_t i=0; iReadU16(); - desc << " " << ((const char *) &val) << std::endl; + std::string val = f->ReadString(); + ext.append(val.c_str(), val.size() + 1); + desc << " " << val << std::endl; + } + + if (parent != static_cast(this)) { + static_cast(parent)->list_extension_ = ext; } } desc << "Count: " << list_count << std::endl; @@ -551,7 +557,11 @@ void Interleaf::WriteObject(FileBase *f, const Object *o) const RIFF::Chk list_mxch = RIFF::BeginChunk(f, RIFF::LIST); f->WriteU32(RIFF::MxCh); - f->WriteU32(o->GetChildCount()); + if (o->list_extension_.empty()) { + f->WriteU32(o->GetChildCount()); + } else { + f->WriteBytes(o->list_extension_); + } for (size_t i = 0; i < o->GetChildCount(); i++) { WriteObject(f, static_cast(o->GetChildAt(i))); diff --git a/lib/object.cpp b/lib/object.cpp index e08ac7f..f41df61 100644 --- a/lib/object.cpp +++ b/lib/object.cpp @@ -298,6 +298,7 @@ size_t Object::CalculateMaximumDiskSize() const if (this->HasChildren()) { s += 16; + s += list_extension_.size(); for (size_t i = 0; i < this->GetChildCount(); i++) { s += static_cast(this->GetChildAt(i))->CalculateMaximumDiskSize(); From 00bc60970e06ce6926dfa900fbf1460e59b81124 Mon Sep 17 00:00:00 2001 From: Christian Semmler Date: Fri, 24 Jul 2026 23:52:11 -0700 Subject: [PATCH 2/3] lib: fix buffer-boundary corruption when rewritten objects change size Three defects conspired to corrupt rewritten SI files once object sizes changed from the source file (any replacement shifts every downstream chunk): - Object::CalculateMaximumDiskSize did not account for RIFF odd-size padding of nested chunks, so the pre-MxSt padding projection could undershoot the actual object tree by one byte per odd-sized chunk. - The projection did not include the MxDa LIST header that follows the object tree, so that header could land within the last bytes of a buffer, where both the game and Interleaf::Read skip it (chunks must not straddle buffer boundaries), desyncing the entire parse. - WritePadding silently no-ops for gaps smaller than a pad_ chunk; WritePaddingIfNecessary now fills such gaps with zeroes instead. Also preserve the MxOf object count field through Read/Write instead of rewriting it with the top-level slot count (retail files store the total object count there; e.g. ISLE.SI says 3708 while having 1186 slots). --- include/libweaver/interleaf.h | 1 + lib/interleaf.cpp | 20 +++++++++++++++++--- lib/object.cpp | 6 ++++-- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/include/libweaver/interleaf.h b/include/libweaver/interleaf.h index 7df3874..92e80ff 100644 --- a/include/libweaver/interleaf.h +++ b/include/libweaver/interleaf.h @@ -74,6 +74,7 @@ class Interleaf : public Core uint32_t m_Version; uint32_t m_BufferSize; uint32_t m_BufferCount; + uint32_t m_ObjectCount; std::map m_ObjectOffsetTable; std::map m_ObjectIDTable; diff --git a/lib/interleaf.cpp b/lib/interleaf.cpp index a469160..d96ad02 100644 --- a/lib/interleaf.cpp +++ b/lib/interleaf.cpp @@ -15,12 +15,14 @@ static const uint32_t kMinimumChunkSize = 8; Interleaf::Interleaf() { + m_ObjectCount = 0; } void Interleaf::Clear() { m_Info.clear(); m_BufferSize = 0; + m_ObjectCount = 0; m_JoiningProgress = 0; m_JoiningSize = 0; m_ObjectOffsetTable.clear(); @@ -114,6 +116,7 @@ Interleaf::Error Interleaf::ReadChunk(Core *parent, FileBase *f, Info *info) case RIFF::MxOf: { uint32_t offset_count = f->ReadU32(); + m_ObjectCount = offset_count; desc << "Count: " << offset_count; @@ -441,7 +444,7 @@ Interleaf::Error Interleaf::Write(FileBase *f) const // MxOf RIFF::Chk mxof = RIFF::BeginChunk(f, RIFF::MxOf); - f->WriteU32(GetChildCount()); + f->WriteU32(m_ObjectCount ? m_ObjectCount : uint32_t(GetChildCount())); offset_table_pos = f->pos(); @@ -464,7 +467,9 @@ Interleaf::Error Interleaf::Write(FileBase *f) const continue; } - size_t maxSz = child->CalculateMaximumDiskSize() + kMinimumChunkSize; + // MxSt header + object tree + the MxDa LIST header that follows it, + // all of which must not straddle a buffer boundary + size_t maxSz = child->CalculateMaximumDiskSize() + kMinimumChunkSize * 2 + 4; WritePaddingIfNecessary(f, maxSz); uint32_t mxst_offset = f->pos(); @@ -815,7 +820,16 @@ void Interleaf::WritePaddingIfNecessary(FileBase *f, size_t projectedWrite) cons size_t this_buf = f->pos()/m_BufferSize; size_t end_buf = projected_end/m_BufferSize; if (this_buf != end_buf) { - WritePadding(f, (end_buf * m_BufferSize) - f->pos()); + size_t pad = (end_buf * m_BufferSize) - f->pos(); + if (pad < kMinimumChunkSize) { + // Too small for a pad_ chunk; fill with zeroes, which readers skip as + // part of the buffer alignment rule + bytearray b(pad); + b.fill(0); + f->WriteBytes(b); + } else { + WritePadding(f, pad); + } } } diff --git a/lib/object.cpp b/lib/object.cpp index f41df61..cdb2d80 100644 --- a/lib/object.cpp +++ b/lib/object.cpp @@ -282,7 +282,8 @@ size_t Object::CalculateMaximumDiskSize() const { size_t s = 0; - s += 108; + // 108 bytes of fixed fields + 2 for RIFF odd-size padding of this chunk + s += 110; s += presenter_.size() + 1; s += name_.size() + 1; s += extra_.size(); @@ -297,7 +298,8 @@ size_t Object::CalculateMaximumDiskSize() const } if (this->HasChildren()) { - s += 16; + // Child LIST header + 2 for RIFF odd-size padding + s += 18; s += list_extension_.size(); for (size_t i = 0; i < this->GetChildCount(); i++) { From 1faec59cbe9cbc20da6405f272c911d7fa4f3ec6 Mon Sep 17 00:00:00 2001 From: Christian Semmler Date: Sat, 25 Jul 2026 08:34:06 -0700 Subject: [PATCH 3/3] ci: pass an absolute FFMPEG_ROOT on Windows The runner image's newer CMake resolves the relative root against the app/ subdirectory when find_package(FFMPEG) runs, so configuration fails with the include paths reported under app/. Resolve the path in the workflow instead of relying on version-specific behavior. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c48d0d..3067273 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,7 @@ jobs: - name: Build shell: bash run: | - cmake . -G Ninja -DCMAKE_BUILD_TYPE=Release -DFFMPEG_ROOT=$FFMPEG_DIR + cmake . -G Ninja -DCMAKE_BUILD_TYPE=Release -DFFMPEG_ROOT=$(cygpath -m "$PWD")/$FFMPEG_DIR ninja - name: Deploy