Files
ReSkate/Engine/Resource/Test/ebx_fixup_repair_tests.cpp
T
AyeZeeBB 2ec25df23b Mod merge: costume presets that crash the game's load are repaired, and material rows past the shader programs left out
Master Chief and TASM2 Spider-Man ship appearance presets whose EBX lists a type reference on the
slot of an import, which the game's load reads as a type index and crashes on (Skate.exe+0x3def285
and +0x3def262 in Sentry). The merge now checks every mod's own EBX, including bundles it would pass
through whole, and takes such entries (or a repeated one) out of the copy in the merged patch and of
the copies carried into other mods' bundles; the mod's original still feeds any combine, and the
merge notes say how many per mod and bundle. The check reads only the fixup lists.

A material lookup row pointing past the shader programs the game ships is left out of the merged
lookup, with a note, instead of reaching the shader registry. Tests for both.
2026-10-17 14:27:20 -05:00

106 lines
4.8 KiB
C++

// The EBX type-reference repair: an entry on another fixup's slot, or listed twice, comes out of
// the list, the chunk and RIFF sizes follow, and a document without one is left alone.
#include "Engine/Resource/ebx_fixup_repair.h"
#include <cstring>
#include <iostream>
#include <string_view>
#include <vector>
namespace fb = dingosdk::frostbite;
namespace {
int failures = 0;
void check(bool condition, const char* what) {
if (!condition) { std::cerr << "FAILED: " << what << "\n"; ++failures; }
}
void u32(std::vector<std::byte>& bytes, std::uint32_t value) {
const auto at = bytes.size();
bytes.resize(at + 4);
std::memcpy(bytes.data() + at, &value, 4);
}
std::uint32_t read(const std::vector<std::byte>& bytes, std::size_t at) {
std::uint32_t value{};
std::memcpy(&value, bytes.data() + at, 4);
return value;
}
void chunk(std::vector<std::byte>& riff, std::string_view id, const std::vector<std::byte>& data) {
for (const auto ch : id) riff.push_back(static_cast<std::byte>(ch));
u32(riff, static_cast<std::uint32_t>(data.size()));
riff.insert(riff.end(), data.begin(), data.end());
if (data.size() & 1) riff.push_back(std::byte{});
}
std::vector<std::uint32_t> list_at(const std::vector<std::byte>& bytes, std::size_t at) {
std::vector<std::uint32_t> out(read(bytes, at));
for (std::size_t index = 0; index < out.size(); ++index) out[index] = read(bytes, at + 4 + 4 * index);
return out;
}
// An EBX with one type, one instance at 0x10, a pointer at 0x40, one import relocated at 0x60,
// and the type references given. Returns where the type-reference count lies.
std::vector<std::byte> document(const std::vector<std::uint32_t>& types, std::size_t& typeList) {
std::vector<std::byte> efix(16); // the file guid
u32(efix, 1);
efix.resize(efix.size() + 16); // its type
u32(efix, 0); // no signatures
u32(efix, 1); // exported
u32(efix, 1);
u32(efix, 0x10); // the instance
u32(efix, 1);
u32(efix, 0x40); // a pointer
u32(efix, 0); // no resource references
u32(efix, 1);
efix.resize(efix.size() + 32); // an import
u32(efix, 1);
u32(efix, 0x60); // its relocation
const auto list = efix.size();
u32(efix, static_cast<std::uint32_t>(types.size()));
for (const auto type : types) u32(efix, type);
u32(efix, 0x80);
u32(efix, 0x80);
u32(efix, 0x80); // the sections
std::vector<std::byte> riff;
for (const auto ch : std::string_view("RIFF")) riff.push_back(static_cast<std::byte>(ch));
u32(riff, 0);
for (const auto ch : std::string_view("EBX\0", 4)) riff.push_back(static_cast<std::byte>(ch));
chunk(riff, "EBXD", std::vector<std::byte>(0x90));
typeList = riff.size() + 8 + list;
chunk(riff, "EFIX", efix);
chunk(riff, "EBXX", std::vector<std::byte>(8));
chunk(riff, "REFL", std::vector<std::byte>(5));
const auto size = static_cast<std::uint32_t>(riff.size() - 8);
std::memcpy(riff.data() + 4, &size, 4);
return riff;
}
} // namespace
int main() {
{
std::size_t list{};
const auto clean = document({0x20, 0x28}, list);
check(!fb::ebx::repair_type_fixups(clean), "a document without collisions is left alone");
}
{
// On the import's slot, inside the instance's header, and the same slot twice.
std::size_t list{};
const auto broken = document({0x20, 0x60, 0x18, 0x28, 0x20}, list);
const auto repaired = fb::ebx::repair_type_fixups(broken);
check(repaired.has_value(), "a document with collisions is repaired");
if (repaired) {
check(repaired->removed == 3, "three entries are removed");
check(repaired->bytes.size() == broken.size() - 12, "the document shrinks by those entries");
check(list_at(repaired->bytes, list) == std::vector<std::uint32_t>{0x20, 0x28}, "the clean entries stay, in order");
check(read(repaired->bytes, 4) == read(broken, 4) - 12, "the RIFF size follows");
constexpr std::size_t efixSize = 12 + 8 + 0x90 + 4; // after the header and EBXD
check(read(repaired->bytes, efixSize) == read(broken, efixSize) - 12, "the EFIX size follows");
// EBXX (8 + 8 bytes) and REFL (8 + 5, padded) end the document.
check(std::memcmp(repaired->bytes.data() + repaired->bytes.size() - 30, "EBXX", 4) == 0,
"the chunks after EFIX are carried along");
check(!fb::ebx::repair_type_fixups(repaired->bytes), "the repaired document needs nothing more");
}
}
check(!fb::ebx::repair_type_fixups(std::vector<std::byte>(4)), "something that is not an EBX is left alone");
if (failures) return 1;
std::cout << "ebx fixup repair tests passed\n";
return 0;
}