diff --git a/Engine/Vfs/Test/mod_merge_added_assets_tests.cpp b/Engine/Vfs/Test/mod_merge_added_assets_tests.cpp index 34571741..b80b6815 100644 --- a/Engine/Vfs/Test/mod_merge_added_assets_tests.cpp +++ b/Engine/Vfs/Test/mod_merge_added_assets_tests.cpp @@ -11,6 +11,7 @@ // wave the song names, the wave's sound-bank resource, and an asset nothing names. #include "Engine/Resource/binary_bundle.h" #include "Engine/Resource/cas_codec.h" +#include "Engine/Resource/ebx_document.h" #include "Engine/Resource/toc.h" #include "Engine/Vfs/mod_catalog.h" #include "Engine/Vfs/native_db.h" @@ -353,6 +354,34 @@ void same_name_from_two_mods(bool different) { expect(noted(report, std::string("map: ") + bundle_name + ": 3 asset(s) added by other mods, e.g. test/song"), label + ": the map's copy still receives the first mod's three\n" + describe(report)); } + +// Two music mods both change test/playlist to add their own songs. Both mods' songs, +// waves and resources must be carried into the map's copy of the bundle, not just the first mod's. +void two_music_mods_both_carried_into_maps_copy(int baseline) { + Fixture fixture("two-music-mods"); + const auto song1 = guid(10), wave1 = guid(11); + const auto song2 = guid(20), wave2 = guid(21); + fixture.add("music1", false, shared_toc, {}, + {{"test/playlist", 1, ebx_document(guid(1), {song1})}, + {"test/other", 0, ebx_document(guid(2), {})}, + {"test/song1", 0, ebx_document(song1, {wave1})}, + {"test/wave1", 0, ebx_document(wave1, {})}}, + {{"test/wave1", {std::byte{1}}}}); + fixture.add("music2", false, shared_toc, {}, + {{"test/playlist", 2, ebx_document(guid(1), {song2})}, + {"test/other", 0, ebx_document(guid(2), {})}, + {"test/song2", 0, ebx_document(song2, {wave2})}, + {"test/wave2", 0, ebx_document(wave2, {})}}, + {{"test/wave2", {std::byte{2}}}}); + fixture.add("map", true, map_toc, {map_superbundle}, game_copy()); + const auto report = mods::merge_mods(fixture.catalog); + expect(report.issue.empty() && report.built, "two music mods: the merge builds the patch\n" + describe(report)); + expect(noted(report, std::string("map: ") + bundle_name + ": 6 asset(s) added by other mods, e.g. test/song1"), + "two music mods: the map's copy receives all six assets from both mods\n" + describe(report)); + const auto files = fixture.merged_files(map_toc); + expect(files == baseline + 6, "two music mods: the map's copy gained 6 files (3 per music mod): " + + std::to_string(files) + " vs " + std::to_string(baseline + 6)); +} } // namespace int main() try { @@ -363,6 +392,7 @@ int main() try { copy_in_the_adders_toc_is_left_alone(); same_name_from_two_mods(true); same_name_from_two_mods(false); + two_music_mods_both_carried_into_maps_copy(baseline); if (failures) { std::cerr << failures << " failure(s)\n"; return 1; diff --git a/Engine/Vfs/mod_merge_overrides.cpp b/Engine/Vfs/mod_merge_overrides.cpp index 17e9adaf..5269a473 100644 --- a/Engine/Vfs/mod_merge_overrides.cpp +++ b/Engine/Vfs/mod_merge_overrides.cpp @@ -1,8 +1,11 @@ #include "mod_merge_internal.h" #include "Engine/Resource/cas_codec.h" #include "Engine/Resource/ebx_document.h" +#include "Engine/Resource/ebx_merge.h" +#include "Engine/Resource/ebx_writer.h" #include +#include #include #include @@ -68,15 +71,44 @@ AssetOverrides collect_asset_overrides(const std::vector& mods, candidates.push_back({lower(bundle.name), {mod->name, asset, payload(), lower(relative)}}); continue; } - auto& versions = out.changed[name]; - if (versions.contains(game_copy->second)) continue; - const auto& change = versions.emplace(game_copy->second, - AssetOverride{mod->name, asset.sha1, asset.originalSize, payload()}).first->second; - ++changed; + const auto encodedPayload = payload(); try { - for (const auto& reference : decoded(change.encoded).imports) + for (const auto& reference : decoded(encodedPayload).imports) imported.insert(reference.fileGuid); } catch (const std::exception&) {} + auto& versions = out.changed[name]; + const auto existing = versions.find(game_copy->second); + if (existing == versions.end()) { + versions.emplace(game_copy->second, + AssetOverride{mod->name, asset.sha1, asset.originalSize, encodedPayload}); + ++changed; + } else { + try { + std::size_t baseIndex = 0; + while (baseIndex < gameListing->manifest.ebx.size() && + lower(gameListing->manifest.ebx[baseIndex].name) != name) + ++baseIndex; + if (gameListing->first + baseIndex < gameListing->files.size()) { + const auto& baseFile = gameListing->files[gameListing->first + baseIndex]; + const auto baseBytes = store.read(baseRoot, baseFile.location, baseFile.offset, baseFile.size); + const auto baseDoc = decoded(baseBytes); + const auto existingDoc = decoded(existing->second.encoded); + const auto thisDoc = decoded(encodedPayload); + const std::array editDocs{&existingDoc, &thisDoc}; + fb::ebx::MergeSummary summary; + auto combined = fb::ebx::merge_documents(baseDoc, editDocs, &summary); + if (summary.instances || summary.arrayEntries) { + const auto rebuilt = fb::ebx::write_document(combined); + existing->second.sha1 = sha1_of(rebuilt); + existing->second.originalSize = rebuilt.size(); + existing->second.encoded = fb::encode_cas(rebuilt, {gameRoot}); + existing->second.mod += " + " + mod->name; + for (const auto& reference : combined.imports) + imported.insert(reference.fileGuid); + } + } + } catch (const std::exception&) {} + } } // Resources the mod adds, kept to go with an added EBX of the same name // (a wave's sound-bank resource registers the wave with the audio system).