From 8f3611e2835eebb914f7caa4c71a9fc6d869ffc3 Mon Sep 17 00:00:00 2001 From: hypnomalice <344329436+hypnomalice@users.noreply.github.com> Date: Sun, 25 Oct 2026 10:54:20 +0000 Subject: [PATCH] Shaders: validate retained graphs and preserve safe aliases --- Engine/Resource/Test/shader_lookup_tests.cpp | 34 +++++ Engine/Resource/shader_lookup.cpp | 48 +++++-- Engine/Resource/shader_lookup.h | 12 +- .../Vfs/Test/mod_merge_added_assets_tests.cpp | 131 +++++++++++++++++- Engine/Vfs/mod_merge_combine.cpp | 58 +++++--- cmake/Apps.cmake | 2 +- 6 files changed, 243 insertions(+), 42 deletions(-) diff --git a/Engine/Resource/Test/shader_lookup_tests.cpp b/Engine/Resource/Test/shader_lookup_tests.cpp index 45c80795..50517569 100644 --- a/Engine/Resource/Test/shader_lookup_tests.cpp +++ b/Engine/Resource/Test/shader_lookup_tests.cpp @@ -146,6 +146,11 @@ int main() { auto shortMeta = extension; shortMeta.meta.resize(8); check(!fb::shader::preserves_stock_programs(shortMeta.table(), stock.table()), "Short retained metadata is refused"); + auto inflatedCount = extension; + const std::uint32_t impossibleCount = 0xffffffffU; + std::memcpy(inflatedCount.resource.data(), &impossibleCount, sizeof(impossibleCount)); + check(!fb::shader::preserves_stock_programs(inflatedCount.table(), stock.table()), + "A declared private program count must fit inside the retained heap"); #ifdef _WIN32 SYSTEM_INFO system{}; GetSystemInfo(&system); @@ -199,6 +204,35 @@ int main() { "Stock aliases cannot address the selected graph's private program slots"); check(fb::shader::merge_program_lookup(extension.table(), stockEdits).outside == 1, "The stock exception requires the caller's actual game table"); + auto otherCustom = preserved_lookup(true, {{1, {0}}, {12, {0}}, {13, {1}}}); + otherCustom.resource[400] = std::byte{9}; // a different private record, leaving stock intact + const std::vector competing{otherCustom.table()}; + const auto sharedAliases = fb::shader::merge_program_lookup(extension.table(), competing, stock.table()); + check(sharedAliases.added == 1 && sharedAliases.outside == 1 && rows_of(sharedAliases).contains(12) && + !rows_of(sharedAliases).contains(13), "A non-retained custom graph contributes proven stock aliases, never its private indices"); + const auto stockRetained = fb::shader::merge_program_lookup(stock.table(), competing, stock.table()); + check(rows_of(stockRetained).contains(12) && !rows_of(stockRetained).contains(13), + "Stock aliases from an extended graph survive when stock is retained too"); + const auto clash = preserved_lookup(false, {{1, {0}}, {9, {0}}}); + const std::vector clashing{clash.table()}; + const auto graphOwnerWins = fb::shader::merge_program_lookup(extension.table(), clashing, stock.table()); + check(graphOwnerWins.conflicts == 1 && graphOwnerWins.added == 0 && + rows_of(graphOwnerWins).at(9) == std::vector{1}, + "Retained graph rows win clashes with a stock-alias winner, and the disagreement is reported"); + const auto sameGraphClash = lookup(5, {{1, {0}}, {9, {3}}}); + const std::vector graphClashes{sameGraphClash.table()}; + const auto selectedWins = fb::shader::merge_program_lookup(selected.table(), graphClashes); + check(selectedWins.conflicts == 1 && rows_of(selectedWins).at(9) == std::vector{4}, + "The selected contributor wins a same-graph clash with a lower-priority edit"); + const auto invalidWinner = lookup(5, {{1, {0}}, {99, {5}}}); + const auto filtered = fb::shader::merge_program_lookup(invalidWinner.table(), {}); + check(filtered.removed == 1 && filtered.outside == 1 && !rows_of(filtered).contains(99), + "The retained winner's rows must be inside its own program count"); + auto corruptEdit = extension; + corruptEdit.resource.resize(12); + const std::vector corrupt{corruptEdit.table()}; + const auto kept = fb::shader::merge_program_lookup(stock.table(), corrupt, stock.table()); + check(kept.outside == 1 && rows_of(kept).contains(1), "Malformed edits cannot discard an intact retained table"); if (failures) return 1; std::cout << "shader lookup tests passed\n"; return 0; diff --git a/Engine/Resource/shader_lookup.cpp b/Engine/Resource/shader_lookup.cpp index 03656000..a2ba98fb 100644 --- a/Engine/Resource/shader_lookup.cpp +++ b/Engine/Resource/shader_lookup.cpp @@ -73,6 +73,7 @@ bool preserves_stock(const Table& retained, const Table& stock) { if (!count || count > keptCount || stockSize < 12 || keptSize < stockSize || start < 12 || start > stockSize || recordBytes > stockSize - start || keptStart < 12 || keptStart > keptSize || recordBytes > keptSize - keptStart || + static_cast(keptCount) * 168 > keptSize - keptStart || !std::equal(stock.resourceMeta.begin() + 12, stock.resourceMeta.end(), retained.resourceMeta.begin() + 12) || !std::equal(stock.resource.begin() + 12, stock.resource.begin() + stockSize, retained.resource.begin() + 12) || !std::equal(stock.resource.begin() + static_cast(start), @@ -166,6 +167,10 @@ TextureRows read_texture_rows(const Table& table) { bool preserves_stock_programs(const Table& candidate, const Table& stock) { return preserves_stock(candidate, stock); } +bool same_program_graph(const Table& candidate, const Table& other) { + try { return same_graph(candidate, other); } + catch (const std::exception&) { return false; } +} bool only_stock_aliases(const Table& candidate, const Table& stock) { try { @@ -194,22 +199,37 @@ MergedTable merge_program_lookup(const Table& base, const std::span const bool stockPreserved = preserves_stock(base, stock); const auto stockRecords = stockPreserved ? read(stock.resource, 0) : 0; MergedTable merged; + for (auto at = rows.begin(); at != rows.end();) { + if (std::any_of(at->second.begin(), at->second.end(), [&](auto index) { return index >= records; })) { + at = rows.erase(at); + ++merged.outside; + ++merged.removed; + } else ++at; + } for (const auto& edit : edits) { - // Indices have meaning only within their owning graph. Keep the selected - // bundle's heap and relocations verbatim: importing another graph can - // make the game instantiate programs before their compiled files load. - const bool sameGraph = same_graph(base, edit); - const bool stockGraph = stockPreserved && same_graph(stock, edit); - const auto available = sameGraph ? records : stockRecords; - for (const auto& [key, values] : read_program_rows(edit)) { - if (baseRows.contains(key)) continue; - if ((!sameGraph && !stockGraph) || std::any_of(values.begin(), values.end(), [&](std::uint32_t index) { return index >= available; })) { - ++merged.outside; - continue; + try { + // Indices have meaning only within their owning graph. Keep the selected + // bundle's heap and relocations verbatim: importing another graph can + // make the game instantiate programs before their compiled files load. + const bool sameGraph = same_graph(base, edit); + const bool stockGraph = stockPreserved && (same_graph(stock, edit) || preserves_stock(edit, stock)); + const auto available = sameGraph ? records : stockRecords; + for (const auto& [key, values] : read_program_rows(edit)) { + if (const auto held = baseRows.find(key); held != baseRows.end()) { + if (held->second != values) ++merged.conflicts; + continue; + } + if ((!sameGraph && !stockGraph) || std::any_of(values.begin(), values.end(), [&](std::uint32_t index) { return index >= available; })) { + ++merged.outside; + continue; + } + const auto [row, inserted] = rows.try_emplace(key, values); + if (inserted) ++merged.added; + else if (row->second != values) ++merged.conflicts; } - const auto [row, inserted] = rows.try_emplace(key, values); - if (inserted) ++merged.added; - else if (row->second != values) ++merged.conflicts; + } catch (const std::exception&) { + // A malformed edit is not permission to discard a verified graph. + ++merged.outside; } } diff --git a/Engine/Resource/shader_lookup.h b/Engine/Resource/shader_lookup.h index 72a68004..ef0f1840 100644 --- a/Engine/Resource/shader_lookup.h +++ b/Engine/Resource/shader_lookup.h @@ -14,6 +14,8 @@ namespace dingosdk::frostbite::shader { inline constexpr std::uint32_t programLookupType = 0xE2E6955B; inline constexpr std::uint32_t textureLookupType = 0x2D254A89; +// Native ShaderProgram bytecode resource type, also used by Studio's native +// shader resource writer; confirmed on the pinned build's shaders/bytecode/*. inline constexpr std::uint32_t bytecodeType = 0x0E2B02F7; struct Table { @@ -25,18 +27,22 @@ struct MergedTable { std::vector resource; std::vector resourceMeta; std::size_t added{}; // rows taken from the edits - std::size_t conflicts{}; // rows two edits disagreed on; the first won + std::size_t conflicts{}; // disagreements: retained graph wins, then first edit std::size_t outside{}; // rows left out: they point past the base's programs + std::size_t removed{}; // invalid rows removed from the retained table itself }; // The selected bundle contributor owns the retained program graph. Merge only -// aliases whose program heap and relocation bytes match it exactly. No program +// private aliases whose program heap and relocation bytes match it exactly. No program // records or relocations are copied or rebased. An optional stock table allows -// stock aliases when its heap, records and relocations survive unchanged. `outside` includes +// stock aliases from other graphs that also preserve the stock graph. Retained +// graph rows win clashes, even against higher-priority alias contributors; other +// fresh keys use edit order. `outside` includes // rows from incompatible graphs as well as out-of-range indices. // Selection helpers for contributors to the SAME bundle. A caller selecting a // different contributor must also retain that contributor's compiled resources. [[nodiscard]] bool preserves_stock_programs(const Table& candidate, const Table& stock); +[[nodiscard]] bool same_program_graph(const Table& candidate, const Table& other); [[nodiscard]] bool only_stock_aliases(const Table& candidate, const Table& stock); [[nodiscard]] MergedTable merge_program_lookup(const Table& base, std::span edits, const Table& stock = {}); diff --git a/Engine/Vfs/Test/mod_merge_added_assets_tests.cpp b/Engine/Vfs/Test/mod_merge_added_assets_tests.cpp index 310e531e..3bc391ad 100644 --- a/Engine/Vfs/Test/mod_merge_added_assets_tests.cpp +++ b/Engine/Vfs/Test/mod_merge_added_assets_tests.cpp @@ -14,6 +14,7 @@ #include "Engine/Resource/cas_codec.h" #include "Engine/Resource/ebx_document.h" #include "Engine/Resource/toc.h" +#include "Engine/Resource/shader_lookup.h" #include "Engine/Vfs/game_archives.h" #include "Engine/Vfs/mod_catalog.h" #include "Engine/Vfs/mod_merge_internal.h" @@ -865,9 +866,137 @@ void two_merges_write_the_same_bytes() { expect(read(patch / "Win32" / "pkg" / "cas_01.cas") == archive, "same bytes: the patch's own archive differs"); expect(read(patch / "Win32" / fs::path(second_map_toc)) == toc, "same bytes: the second map's TOC differs"); } + +void shader_selection_checks_the_winner(bool delivered, bool changed_heap, bool alias_first, bool overwritten = false, + bool encoder_available = true) { + Fixture fixture("shader-selection-" + std::to_string(delivered) + std::to_string(changed_heap) + + std::to_string(alias_first) + std::to_string(overwritten)); + const auto table = [](bool extended) { + Resource resource{"test/program_lookup", Bytes(extended ? 544 : 208)}; + const std::uint32_t count = extended ? 2 : 1; + const std::uint64_t start = extended ? 208 : 16; + std::memcpy(resource.payload.data(), &count, 4); + std::memcpy(resource.payload.data() + 4, &start, 8); + resource.payload[192] = std::byte{7}; + put32(resource.payload, 4); // the header pointer's relocation + return resource; + }; + const auto rows = [](Resource resource, const std::map& values) { + put32(resource.payload, static_cast(values.size())); + for (const auto& [key, index] : values) { + for (int shift = 0; shift < 64; shift += 8) resource.payload.push_back(static_cast(key >> shift)); + put32(resource.payload, 1); put32(resource.payload, index); + } + return resource; + }; + auto stock = rows(table(false), {{1, 0}}); + auto aliases = rows(table(false), {{1, 0}, {2, 0}}); + auto custom = rows(table(true), {{1, 0}, {2, 0}, {5, 0}, {9, 1}, {99, 2}}); + if (changed_heap) custom.payload[192] = std::byte{8}; + const auto write_layer = [&](const fs::path& root, bool patch, const Resource& programs, bool code, unsigned code_version) { + fs::create_directories(root / "Win32" / "pkg"); + write(root / "layout.toc", layout_toc()); + fb::BinaryBundle manifest; + fb::BundleAsset asset; + asset.kind = fb::AssetKind::resource; asset.name = programs.name; asset.resourceId = 42; + asset.resourceType = fb::shader::programLookupType; + asset.sha1 = mods::detail::sha1_of(programs.payload); asset.originalSize = programs.payload.size(); + const auto size = programs.payload.size() > 544 ? 544U : 208U; + put32(asset.resourceMeta, size); put32(asset.resourceMeta, 4); + put32(asset.resourceMeta, static_cast(programs.payload.size() - size - 4)); put32(asset.resourceMeta, 0); + manifest.resources.push_back(asset); + const Bytes bytecode{static_cast(code_version)}; + if (code) { + asset.name = "shaders/bytecode/test"; asset.resourceId = 43; asset.resourceType = fb::shader::bytecodeType; + asset.sha1 = mods::detail::sha1_of(bytecode); asset.originalSize = bytecode.size(); asset.resourceMeta.assign(16, std::byte{0}); + manifest.resources.push_back(asset); + } + auto archive = fb::write_binary_bundle(manifest); + std::vector files{{{patch, package, 1}, 0, static_cast(archive.size())}}; + const auto append_payload = [&](const Bytes& payload) { + const auto bytes = encoded(payload); + files.push_back({{patch, package, 1}, static_cast(archive.size()), static_cast(bytes.size())}); + archive.insert(archive.end(), bytes.begin(), bytes.end()); + }; + append_payload(programs.payload); + if (code) append_payload(bytecode); + write(root / "Win32" / "pkg" / "cas_01.cas", archive); + const std::vector bundles{{bundle_name, fb::write_bundle_region(files), 1}}; + write(root / "Win32" / fs::path(shared_toc), fb::write_patch_toc(bundles)); + }; + write_layer(fixture.catalog.data_root / "Data", false, stock, false, 0); + write_layer(fixture.catalog.root / "custom", true, custom, delivered, 1); + write_layer(fixture.catalog.root / "aliases", true, aliases, overwritten, 2); + for (const auto* name : alias_first ? std::vector{"aliases", "custom"} : std::vector{"custom", "aliases"}) { + mods::Mod mod; mod.name = name; mod.directory = fixture.catalog.root / name; mod.provides_layout = true; + fixture.catalog.mods.push_back(std::move(mod)); + } + const auto report = mods::merge_mods(fixture.catalog); + expect(report.issue.empty() && report.built, "shader graph selection builds\n" + describe(report)); + Patch patch(fixture); + const auto toc = fb::read_toc(read(patch.root / "Win32" / fs::path(shared_toc))); + const auto listing = mods::detail::list_bundle(patch.store, patch.root, patch.base, toc.bundles.front(), patch.game); + expect(listing && !listing->manifest.resources.empty(), "shader result has its native resource manifest"); + if (!listing || listing->manifest.resources.empty()) return; + const auto program = std::find_if(listing->manifest.resources.begin(), listing->manifest.resources.end(), + [](const auto& resource) { return resource.resourceType == fb::shader::programLookupType; }); + expect(program != listing->manifest.resources.end(), "program lookup resource survives"); + if (program == listing->manifest.resources.end()) return; + const auto program_index = listing->manifest.ebx.size() + static_cast(program - listing->manifest.resources.begin()); + const auto payload = mods::detail::read_asset(patch.store, patch.root, patch.base, *listing, program_index, patch.game); + if (payload.size() < 12) { expect(false, "program lookup payload is complete"); return; } + if (!encoder_available) { + // This case starts before any codec has been loaded. A failed rebuild + // must retain the selected alias payload AND its matching metadata. + expect(payload == aliases.payload, "failed encoding keeps the selected payload"); + std::uint32_t kept_size{}; std::memcpy(&kept_size, program->resourceMeta.data(), 4); + expect(kept_size == 208, "failed encoding does not publish replacement graph metadata"); + expect(std::any_of(report.notes.begin(), report.notes.end(), [](const auto& note) { + return note.find("edits could not be combined") != std::string::npos; + }), "missing encoder exercises the failed-rebuild path"); + return; + } + std::uint32_t count{}; std::memcpy(&count, payload.data(), 4); + const bool private_graph = delivered && !changed_heap && !overwritten; + expect(count == (private_graph ? 2U : 1U), "winning and promoted custom graphs need both stock preservation and retained bundle-owned bytecode"); + const auto& meta = program->resourceMeta; + std::uint32_t size{}, fixups{}, number{}; + std::memcpy(&size, meta.data(), 4); std::memcpy(&fixups, meta.data() + 4, 4); + std::size_t at = static_cast(size) + fixups; + if (at + 4 > payload.size()) { expect(false, "program lookup section is bounded"); return; } + std::memcpy(&number, payload.data() + at, 4); at += 4; + std::set keys; + for (std::uint32_t row = 0; row < number; ++row) { + if (at + 12 > payload.size()) { expect(false, "program row header is bounded"); return; } + std::uint64_t key{}; std::uint32_t values{}; + std::memcpy(&key, payload.data() + at, 8); std::memcpy(&values, payload.data() + at + 8, 4); at += 12; + keys.insert(key); + for (std::uint32_t value = 0; value < values; ++value) { + if (at + 4 > payload.size()) { expect(false, "program row indices are bounded"); return; } + std::uint32_t index{}; std::memcpy(&index, payload.data() + at, 4); at += 4; + expect(index < count, "native output rows stay in the retained graph's range"); + } + } + expect(keys.contains(2) && !keys.contains(99), "safe stock aliases survive and invalid winning rows are actually written out"); + expect(keys.contains(9) == private_graph, "unproved graphs cannot leak private program indices into stock"); + expect(keys.contains(5) == !changed_heap, "a non-retained custom contributor still contributes a proven stock alias"); +} } // namespace -int main() try { +int main(int argc, char** argv) try { + // Rebuilt native resources use the game's Oodle encoder. The normal suite + // needs no game files; supplying a game root enables these archive checks. + if (argc > 1 && *argv[1]) { + (void)fb::encode_cas(Bytes{std::byte{1}}, {fs::path(argv[1])}); + shader_selection_checks_the_winner(true, false, false); + shader_selection_checks_the_winner(true, false, true); + shader_selection_checks_the_winner(false, false, false); + shader_selection_checks_the_winner(true, true, false); + shader_selection_checks_the_winner(true, false, true, true); + } else { + shader_selection_checks_the_winner(true, false, true, false, false); + std::cout << "Successful shader archive selection checks require a local game root; skipped\n"; + } replaced_while_the_game_runs(); const auto baseline = map_only(); expect(baseline > 0, "map only: the map's copy exists (" + std::to_string(baseline) + ")"); diff --git a/Engine/Vfs/mod_merge_combine.cpp b/Engine/Vfs/mod_merge_combine.cpp index 6ada5c8e..b1467e29 100644 --- a/Engine/Vfs/mod_merge_combine.cpp +++ b/Engine/Vfs/mod_merge_combine.cpp @@ -1113,6 +1113,7 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot, for (const auto* edit : edits) editBytes.push_back(decode(*edit)); std::vector rebuilt; + auto rebuiltMeta = entry.asset.resourceMeta; std::string what; if (refTable) { std::vector tables; @@ -1121,7 +1122,7 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot, const auto table = fb::bundle_ref::merge({baseBytes, baseCopy->resourceMeta}, tables); if (!table.added) continue; rebuilt = table.resource; - entry.asset.resourceMeta = table.resourceMeta; + rebuiltMeta = table.resourceMeta; what = std::to_string(table.added) + " preset(s)"; if (table.conflicts) what += ", " + std::to_string(table.conflicts) + " disagreed"; @@ -1153,15 +1154,13 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot, const auto* selected = retained; auto retainedBytes = decode(*retained); const fb::shader::Table stockTable{baseBytes, baseCopy->resourceMeta}; - if (entry.asset.resourceType == fb::shader::programLookupType && - fb::shader::only_stock_aliases({retainedBytes, retained->resourceMeta}, stockTable)) { - // A stock-alias edit owns no new programs. It must not - // erase a graph another contributor already delivers - // to this same bundle. Highest custom contributor wins. - for (std::size_t i = 0; i < edits.size(); ++i) { - const fb::shader::Table candidate{editBytes[i], edits[i]->resourceMeta}; - if (fb::shader::only_stock_aliases(candidate, stockTable) || - !fb::shader::preserves_stock_programs(candidate, stockTable)) continue; + if (entry.asset.resourceType == fb::shader::programLookupType) { + // Apply identical ownership checks to the winning and + // lower-priority custom graphs. Unproved custom heaps + // fall back to stock, keeping safe stock aliases usable. + const auto usable = [&](const fb::shader::Table& candidate, const Contribution* source) { + if (fb::shader::same_program_graph(candidate, stockTable)) return true; + if (!fb::shader::preserves_stock_programs(candidate, stockTable)) return false; bool dependenciesRetained = true; bool bytecodeDelivered = false; for (const auto& resource : state.assets) { @@ -1172,20 +1171,28 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot, const auto key = asset_key(resource.asset); const auto history = state.history.find(key); if (history == state.history.end()) continue; - const auto shipped = state.baseSha.find(key); - for (const auto& copy : history->second) { - if (copy.root != edits[i]->root || copy.base || - (shipped != state.baseSha.end() && copy.sha1 == shipped->second)) continue; + for (const auto& copy : history->second) if (!copy.base && copy.root == source->root) { if (copy.resourceType == fb::shader::bytecodeType) bytecodeDelivered = true; - if (copy.sha1 != resource.asset.sha1 || - copy.resourceMeta != resource.asset.resourceMeta || - copy.resourceId != resource.asset.resourceId || - copy.resourceType != resource.asset.resourceType) dependenciesRetained = false; + if (copy.sha1 != resource.asset.sha1 || copy.resourceMeta != resource.asset.resourceMeta || + copy.resourceId != resource.asset.resourceId || copy.resourceType != resource.asset.resourceType) + dependenciesRetained = false; } } - if (!dependenciesRetained || !bytecodeDelivered) continue; - retained = edits[i]; - retainedBytes = editBytes[i]; + return dependenciesRetained && bytecodeDelivered; + }; + if (!usable({retainedBytes, retained->resourceMeta}, retained)) { + report.notes.push_back(entry.asset.name + ": custom graph ownership or stock preservation could not be proved; retaining stock programs"); + retained = baseCopy; + retainedBytes = baseBytes; + } + if (fb::shader::only_stock_aliases({retainedBytes, retained->resourceMeta}, stockTable)) { + for (std::size_t i = 0; i < edits.size(); ++i) { + const fb::shader::Table candidate{editBytes[i], edits[i]->resourceMeta}; + if (fb::shader::same_program_graph(candidate, stockTable) || !usable(candidate, edits[i])) continue; + retained = edits[i]; + retainedBytes = editBytes[i]; + break; + } } } const fb::shader::Table baseTable = @@ -1199,9 +1206,13 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot, report.notes.push_back(entry.asset.name + ": " + std::to_string(table.outside) + " material row(s) left out: their graph or indices do not match the retained " "bundle shader database"); - if (!table.added && retained == selected) continue; + if (!table.added && !table.removed && retained == selected) { + if (table.conflicts) report.notes.push_back(entry.asset.name + ": " + std::to_string(table.conflicts) + + " material row(s) disagreed; retained graph rows win"); + continue; + } rebuilt = table.resource; - entry.asset.resourceMeta = table.resourceMeta; + rebuiltMeta = table.resourceMeta; what = std::to_string(table.added) + " material row(s)"; if (table.conflicts) what += ", " + std::to_string(table.conflicts) + " disagreed"; @@ -1317,6 +1328,7 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot, const auto placement = store.write(state.manifestChunk, manifestArchive, fb::encode_cas(rebuilt, {gameRoot})); entry.file = placement; + entry.asset.resourceMeta = std::move(rebuiltMeta); entry.asset.sha1 = sha1_of(rebuilt); entry.asset.originalSize = rebuilt.size(); used.emplace(placement.location.installChunk, placement.location.archive); diff --git a/cmake/Apps.cmake b/cmake/Apps.cmake index 3e4e99d2..1a66c134 100644 --- a/cmake/Apps.cmake +++ b/cmake/Apps.cmake @@ -127,7 +127,7 @@ if(DINGOSDK_BUILD_LAUNCHER_TESTS AND WIN32) add_executable(dingosdk_mod_merge_added_assets_tests Engine/Vfs/Test/mod_merge_added_assets_tests.cpp) target_link_libraries(dingosdk_mod_merge_added_assets_tests PRIVATE dingosdk_mods dingosdk_game_archives) target_include_directories(dingosdk_mod_merge_added_assets_tests PRIVATE "${PROJECT_SOURCE_DIR}") - add_test(NAME mod_merge_added_assets COMMAND dingosdk_mod_merge_added_assets_tests) + add_test(NAME mod_merge_added_assets COMMAND dingosdk_mod_merge_added_assets_tests "${DINGOSDK_TEST_GAME_ROOT}") add_executable(dingosdk_item_list_merge_tests Engine/Vfs/Test/item_list_merge_tests.cpp) target_link_libraries(dingosdk_item_list_merge_tests PRIVATE dingosdk_mods) target_include_directories(dingosdk_item_list_merge_tests PRIVATE "${PROJECT_SOURCE_DIR}")