mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 18:37:07 +02:00
With several cosmetic mods installed the game crashed as soon as an equipped custom item came into view, and so on every launch after it was equipped. Cosmetic presets (Engine/Resource/bundle_ref_table.*) - The character bundle-reference table says which bundle holds each preset, under the preset's full path and under its leaf name, one row per hash. Mods number their presets alike in different folders (cust_tops/1_ap, cust_bottoms/1_ap), so two mods can share a leaf name. Adding the second preset threw "collides with an existing lookup", the merge fell back to the highest-priority mod's copy of the table, and every other cosmetic mod's presets were unregistered. The game then built the skater with a preset it could not find and called through a bad pointer. - A preset whose leaf name is taken is now added under its full path only; the leaf name stays with the preset that had it. Only a clash of full paths still fails. MergedTable::shadowed lists each such preset with the one that holds its name. Merge log (Engine/Vfs/mod_merge_combine.cpp) - Each mod with shadowed presets gets a line naming how many and an example pair, so an item that shows the wrong preset can be traced to the mods that clash. Checked against 39 installed mods, 15 of them cosmetic: the table combines 274 presets where it used to keep one mod's, all reachable by full path and 267 by leaf name, with the game's own 6264 rows unchanged. The seven left without a leaf row are a naming clash between the mods themselves (two Carhartt packs, a Zero tee and deck); whether the game finds those by full path is not known. bundle_ref_table_tests takes an optional second mod and checks that two mods' presets are all added, in either order, and that a shared leaf name is reported with the preset that holds it.
88 lines
5.1 KiB
C++
88 lines
5.1 KiB
C++
// Merges the game's character bundle-reference table with a cosmetic mod's
|
|
// copy of it. Arguments: <Skate folder> <cosmetic mod folder> [<another
|
|
// cosmetic mod folder>]; skipped when either of the first two is missing.
|
|
#include "Engine/Vfs/mod_merge_internal.h"
|
|
#include "Engine/Resource/bundle_ref_table.h"
|
|
#include "Engine/Resource/cas_codec.h"
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <iostream>
|
|
#include <stdexcept>
|
|
|
|
namespace fs = std::filesystem;
|
|
namespace fb = dingosdk::frostbite;
|
|
using namespace dingosdk::mods::detail;
|
|
|
|
namespace {
|
|
int failures = 0;
|
|
void check(bool condition, const char* what) {
|
|
if (!condition) { std::cerr << "FAILED: " << what << "\n"; ++failures; }
|
|
}
|
|
struct Copy { std::vector<std::byte> bytes, meta; };
|
|
Copy read_table(const CasStore& store, const fs::path& root, const fs::path& base_root, const fs::path& game_root) {
|
|
const auto toc = fb::read_toc(read_file(root / L"Win32" / L"items.toc"));
|
|
for (const auto& bundle : toc.bundles) {
|
|
if (lower(bundle.name) != "win32/characters/customization/configs/cas_main_sharedbundle") continue;
|
|
const auto listing = list_bundle(store, root, base_root, bundle, game_root);
|
|
if (!listing) break;
|
|
for (std::size_t i = 0; i < listing->manifest.resources.size(); ++i) {
|
|
const auto& resource = listing->manifest.resources[i];
|
|
if (lower(resource.name) != "characters/customization/configs/cas_main_bundlereftable") continue;
|
|
const auto& file = listing->files.at(listing->first + listing->manifest.ebx.size() + i);
|
|
return {fb::decode_cas(store.read(file.location.patch ? root : base_root, file.location, file.offset, file.size),
|
|
{game_root}),
|
|
resource.resourceMeta};
|
|
}
|
|
}
|
|
throw std::runtime_error("no cas_main_bundlereftable in " + root.string());
|
|
}
|
|
} // namespace
|
|
|
|
int main(int argc, char** argv) {
|
|
if (argc < 3 || !fs::exists(argv[1]) || !fs::exists(fs::path(argv[2]) / L"Win32" / L"items.toc")) {
|
|
std::cout << "No game or cosmetic mod given; skipped.\n";
|
|
return 0;
|
|
}
|
|
try {
|
|
const fs::path game_root = argv[1], mod = argv[2], base_root = game_root / L"Data";
|
|
const auto layout = dingosdk::vfs::read_layout(mod / L"layout.toc");
|
|
const CasStore store(base_root, fs::temp_directory_path() / L"reskate-bundle-ref-test", layout.root);
|
|
const auto base = read_table(store, base_root, base_root, game_root);
|
|
const auto copy = read_table(store, mod, base_root, game_root);
|
|
const fb::bundle_ref::Table base_table{base.bytes, base.meta}, mod_table{copy.bytes, copy.meta};
|
|
const auto once = fb::bundle_ref::merge(base_table, std::span(&mod_table, 1));
|
|
check(once.added > 0, "the mod's presets are added");
|
|
const std::array twice_tables{mod_table, mod_table};
|
|
const auto twice = fb::bundle_ref::merge(base_table, twice_tables);
|
|
check(twice.added == once.added && !twice.conflicts, "the same preset from two mods is added once");
|
|
const fb::bundle_ref::Table merged{once.resource, once.resourceMeta};
|
|
const auto again = fb::bundle_ref::merge(merged, std::span(&mod_table, 1));
|
|
check(again.added == 0, "the merged table already holds every preset");
|
|
std::cout << "base " << base.bytes.size() << " bytes, mod " << copy.bytes.size() << " bytes, merged "
|
|
<< once.resource.size() << " bytes; " << once.added << " preset(s) added.\n";
|
|
if (argc > 3 && fs::exists(fs::path(argv[3]) / L"Win32" / L"items.toc")) {
|
|
// Mods number their presets alike (cust_tops/1_ap, cust_bottoms/1_ap), so two
|
|
// mods can share leaf names. Every preset of both still has to go in.
|
|
const auto other = read_table(store, argv[3], base_root, game_root);
|
|
const fb::bundle_ref::Table other_table{other.bytes, other.meta};
|
|
const auto alone = fb::bundle_ref::merge(base_table, std::span(&other_table, 1));
|
|
const std::array both_tables{mod_table, other_table}, reversed_tables{other_table, mod_table};
|
|
const auto both = fb::bundle_ref::merge(base_table, both_tables);
|
|
const auto reversed = fb::bundle_ref::merge(base_table, reversed_tables);
|
|
check(both.added >= std::max(once.added, alone.added) && both.added <= once.added + alone.added,
|
|
"two mods' presets are all added");
|
|
check(reversed.added == both.added && reversed.shadowed.size() == both.shadowed.size(),
|
|
"the order of the mods changes which preset keeps a shared leaf name, not what is added");
|
|
for (const auto& clash : both.shadowed)
|
|
check(clash.edit == 1 && !clash.path.empty() && !clash.holder.empty() && clash.path != clash.holder,
|
|
"a shared leaf name is reported with the preset that holds it");
|
|
std::cout << "two mods: " << both.added << " preset(s) added, " << both.shadowed.size()
|
|
<< " under their full path only.\n";
|
|
}
|
|
} catch (const std::exception& error) {
|
|
std::cerr << "FAILED: " << error.what() << "\n";
|
|
return 1;
|
|
}
|
|
return failures ? 1 : 0;
|
|
}
|