diff --git a/Engine/Game/Build/20260929/local_music.h b/Engine/Game/Build/20260929/local_music.h index 25c07805..87cc5e4c 100644 --- a/Engine/Game/Build/20260929/local_music.h +++ b/Engine/Game/Build/20260929/local_music.h @@ -72,6 +72,11 @@ inline constexpr Fingerprint favorite_apply_contract{0x6f51e0, {0x48,0x89,0x5c,0 // Queue selector shared by UI Next and natural end-of-track selection. The queue // supplies its own channel and registered playlist asset; do not hard-code IDs. inline constexpr Fingerprint playback_select_next_contract{0x14f8ed0, {0x48,0x89,0x54,0x24,0x10,0x55,0x53,0x56,0x57,0x41,0x54,0x41,0x55,0x41,0x56,0x41,0x57,0x48,0x8d,0x6c,0x24,0xc8,0x48,0x81,0xec,0x38,0x01,0x00,0x00,0x4c,0x8b,0xe1}}; +// The callback the queue selector hands the game's song scorer for a playlist asset with a +// selector: (u32 place, u32 *out, context) -> *out, a number of the song at that place of the +// asset's songs (song + 0x7c), or 0 where the place holds none. The whole function (32 bytes): +// it does not look at how many songs the asset has. +inline constexpr Fingerprint playback_song_score_contract{0x14f23a0, {0x49,0x8b,0x00,0x44,0x8b,0xc9,0x48,0x8b,0x08,0x4a,0x8b,0x04,0xc9,0x48,0x83,0xe0,0xfb,0x74,0x06,0x8b,0x40,0x7c,0x89,0x02,0xc3,0xc7,0x02,0x00,0x00,0x00,0x00,0xc3}}; // Native playlist playback-mode setter (PlaylistPlaybackMode, not MusicPlaylistType). inline constexpr Fingerprint playback_mode_setter_contract{0x443aa0, {0x44,0x8b,0x91,0xa8,0x00,0x00,0x00,0x4c,0x8b,0x99,0xa0,0x00,0x00,0x00,0x44,0x8b,0xca,0x8b,0xc2,0x33,0xd2,0x49,0xf7,0xf2,0x8b,0xc2,0x49,0x8b,0x04,0xc3,0x48,0x85}}; // Native channel-to-playlist binding entrypoint, used by the generated UI/script thunk. diff --git a/Engine/Resource/shader_lookup.cpp b/Engine/Resource/shader_lookup.cpp index c3f8fc6f..3cf801b0 100644 --- a/Engine/Resource/shader_lookup.cpp +++ b/Engine/Resource/shader_lookup.cpp @@ -1,9 +1,6 @@ #include "shader_lookup.h" -#include -#include #include -#include #include #include #include @@ -80,176 +77,32 @@ TextureRows read_texture_rows(const Table& table) { } // namespace -namespace { -constexpr std::size_t programStride = 168; -struct ProgramBlob { - Table source; - std::span payload; - std::uint64_t records{}; - std::uint32_t count{}; - std::vector relocations; -}; -ProgramBlob program_blob(const Table& source) { - const auto end = lookup_offset(source); - const auto size = read(source.resourceMeta, 0); - if ((end - size) % 4) throw std::runtime_error("Program relocations are not aligned"); - ProgramBlob blob{source, source.resource.first(size)}; - blob.count = read(blob.payload, 0); - blob.records = read(blob.payload, 4); - if (blob.records > size || blob.count > (size - blob.records) / programStride) - throw std::runtime_error("Program records exceed their payload"); - for (auto at = std::size_t{size}; at < end; at += 4) { - const auto pointer = read(source.resource, at); - if (read(blob.payload, pointer) > size) - throw std::runtime_error("Program relocation points outside its payload"); - blob.relocations.push_back(pointer); - } - return blob; -} -struct ProgramArray { std::size_t pointer, stride; }; -constexpr std::array programArrays{ - ProgramArray{0x10, 16}, ProgramArray{0x1c, 8}, ProgramArray{0x28, 8}, ProgramArray{0x34, 16}, - ProgramArray{0x40, 6}, ProgramArray{0x4c, 8}, ProgramArray{0x58, 2}, - ProgramArray{0x64, 8}, ProgramArray{0x70, 8}, ProgramArray{0x80, 16}, ProgramArray{0x8c, 32} -}; -std::size_t record_at(const ProgramBlob& blob, std::uint32_t index) { - if (index >= blob.count) throw std::runtime_error("Material references an absent shader program"); - return static_cast(blob.records) + index * programStride; -} -std::span program_array(const ProgramBlob& blob, std::size_t record, ProgramArray array) { - const auto count = read(blob.payload, record + array.pointer - 4); - const auto pointer = read(blob.payload, record + array.pointer); - if (pointer > blob.payload.size() || count > (blob.payload.size() - pointer) / array.stride) - throw std::runtime_error("Shader program array exceeds its payload"); - return blob.payload.subspan(static_cast(pointer), count * array.stride); -} -bool same_program(const ProgramBlob& a, std::uint32_t ai, const ProgramBlob& b, std::uint32_t bi) { - const auto aa = record_at(a, ai), ba = record_at(b, bi); - for (std::size_t at = 0; at < programStride; ++at) { - if (std::ranges::any_of(programArrays, [&](auto array) { - return at >= array.pointer && at < array.pointer + 8; })) continue; - if (a.payload[aa + at] != b.payload[ba + at]) return false; - } - for (const auto array : programArrays) - if (!std::ranges::equal(program_array(a, aa, array), program_array(b, ba, array))) return false; - return true; -} -std::uint32_t native_offset(std::size_t offset) { - if (offset > std::numeric_limits::max()) - throw std::runtime_error("Merged program database exceeds the native address range"); - return static_cast(offset); -} -} // namespace - MergedTable merge_program_lookup(const Table& base, const std::span edits) { - std::vector blobs{program_blob(base)}; - for (const auto& edit : edits) blobs.push_back(program_blob(edit)); - struct Reference { std::size_t blob; std::uint32_t program; }; - std::map> references; + const auto start = lookup_offset(base); auto rows = read_program_rows(base); - for (const auto& [key, values] : rows) for (const auto index : values) { - record_at(blobs.front(), index); - references[key].push_back({0, index}); - } const auto baseRows = rows; + MergedTable merged; - for (std::size_t b = 1; b < blobs.size(); ++b) { - for (const auto& [key, values] : read_program_rows(blobs[b].source)) { - for (const auto index : values) record_at(blobs[b], index); + for (const auto& edit : edits) { + for (const auto& [key, values] : read_program_rows(edit)) { if (baseRows.contains(key)) continue; - if (const auto previous = references.find(key); previous != references.end()) { - bool same = previous->second.size() == values.size(); - for (std::size_t i = 0; same && i < values.size(); ++i) { - const auto ref = previous->second[i]; - same = same_program(blobs[ref.blob], ref.program, blobs[b], values[i]); - } - if (!same) ++merged.conflicts; - continue; - } - for (const auto index : values) references[key].push_back({b, index}); - ++merged.added; + const auto [row, inserted] = rows.try_emplace(key, values); + if (inserted) ++merged.added; + else if (row->second != values) ++merged.conflicts; } } - // Preserve the exact native prefix when edits only add aliases to stock programs. - // Private programs need their own records and relocated backing data as well. - using Source = std::pair; - std::map remapped; - std::vector additions; - for (const auto& [key, refs] : references) { - auto& row = rows[key]; - row.clear(); - for (const auto ref : refs) { - auto index = ref.program; - if (ref.blob && (index >= blobs.front().count || - !same_program(blobs.front(), index, blobs[ref.blob], index))) { - const Source source{ref.blob, index}; - auto [entry, inserted] = remapped.try_emplace(source, - native_offset(std::size_t{blobs.front().count} + additions.size())); - if (inserted) additions.push_back(source); - index = entry->second; - } - row.push_back(index); - } - } - merged.resourceMeta.assign(base.resourceMeta.begin(), base.resourceMeta.end()); - if (additions.empty()) { - merged.resource.assign(base.resource.begin(), base.resource.begin() + lookup_offset(base)); - } else { - merged.resource.assign(blobs.front().payload.begin(), blobs.front().payload.end()); - auto relocations = blobs.front().relocations; - std::map offsets; - for (const auto& [b, index] : additions) { - if (offsets.contains(b)) continue; - merged.resource.resize((merged.resource.size() + 15) & ~std::size_t{15}); - const auto offset = merged.resource.size(); - offsets[b] = offset; - const auto& blob = blobs[b]; - // Copy the complete backing graph, including opaque resource bindings. - // This keeps runtime-mutated GUID arrays separate across mods. - merged.resource.insert(merged.resource.end(), blob.payload.begin(), blob.payload.end()); - for (const auto pointer : blob.relocations) { - const auto destination = native_offset(offset + pointer); - const auto target = read(blob.payload, pointer) + offset; - std::memcpy(merged.resource.data() + destination, &target, 8); - relocations.push_back(destination); - } - } - merged.resource.resize((merged.resource.size() + 7) & ~std::size_t{7}); - const auto table = static_cast(merged.resource.size()); - const auto count = native_offset(std::size_t{blobs.front().count} + additions.size()); - merged.resource.resize(native_offset(static_cast(table) + std::size_t{count} * programStride)); - const auto copy_record = [&](const ProgramBlob& blob, std::uint32_t index, - std::size_t offset, std::uint32_t destination) { - const auto source = record_at(blob, index); - const auto target = static_cast(table) + destination * programStride; - std::memcpy(merged.resource.data() + target, merged.resource.data() + offset + source, programStride); - for (const auto pointer : blob.relocations) - if (pointer >= source && pointer < source + programStride) - relocations.push_back(native_offset(target + pointer - source)); - }; - for (std::uint32_t i = 0; i < blobs.front().count; ++i) copy_record(blobs.front(), i, 0, i); - for (const auto& source : additions) - copy_record(blobs[source.first], source.second, offsets.at(source.first), remapped.at(source)); - std::memcpy(merged.resource.data(), &count, 4); - std::memcpy(merged.resource.data() + 4, &table, 8); - relocations.push_back(4); - std::ranges::sort(relocations); - relocations.erase(std::ranges::unique(relocations).begin(), relocations.end()); - const auto size = native_offset(merged.resource.size()); - for (const auto pointer : relocations) append(merged.resource, pointer); - const auto relocationSize = native_offset(relocations.size() * 4); - std::memcpy(merged.resourceMeta.data(), &size, 4); - std::memcpy(merged.resourceMeta.data() + 4, &relocationSize, 4); - } - const auto lookup = merged.resource.size(); - append(merged.resource, native_offset(rows.size())); + + merged.resource.assign(base.resource.begin(), + base.resource.begin() + static_cast(start)); + append(merged.resource, static_cast(rows.size())); for (const auto& [key, values] : rows) { append(merged.resource, key); - append(merged.resource, native_offset(values.size())); + append(merged.resource, static_cast(values.size())); for (const auto value : values) append(merged.resource, value); } - const auto size = native_offset(merged.resource.size() - lookup); - std::memcpy(merged.resourceMeta.data() + 8, &size, 4); + merged.resourceMeta.assign(base.resourceMeta.begin(), base.resourceMeta.end()); + const auto size = static_cast(merged.resource.size() - start); + std::memcpy(merged.resourceMeta.data() + 8, &size, sizeof(size)); return merged; } diff --git a/Engine/Resource/shader_lookup.h b/Engine/Resource/shader_lookup.h index fe140280..94179403 100644 --- a/Engine/Resource/shader_lookup.h +++ b/Engine/Resource/shader_lookup.h @@ -1,8 +1,9 @@ #pragma once -// Shader lookup tables carry material rows and compiled program records. -// Mods can alias stock programs or add private programs with their own -// permutations and backing arrays; both must survive a merge. +// The two shader lookup tables a level superbundle carries. A map mod adds a +// row per material it introduces, aliasing a shader program and texture set the +// base already ships, so two mods' tables differ from the base only by the rows +// each added and can be combined exactly. #include #include @@ -26,9 +27,9 @@ struct MergedTable { std::size_t conflicts{}; // rows two edits disagreed on; the first won }; -// `resourceMeta` is 16 bytes: program size, relocation size, lookup size. -// Private program records and their relocated backing data are copied from -// each edit, and material indices are remapped into the combined table. +// `resourceMeta` is 16 bytes: program size, relocation size, lookup size. The +// program blob and its relocations are identical across copies because a mod +// only ever points new keys at programs that are already there. [[nodiscard]] MergedTable merge_program_lookup(const Table& base, std::span edits); [[nodiscard]] MergedTable merge_texture_lookup(const Table& base, std::span edits); diff --git a/Engine/Vfs/mod_merge_combine.cpp b/Engine/Vfs/mod_merge_combine.cpp index 6b95fe26..0ff70ca9 100644 --- a/Engine/Vfs/mod_merge_combine.cpp +++ b/Engine/Vfs/mod_merge_combine.cpp @@ -788,10 +788,7 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot, // holds one asset per name, so the lower-priority mod's is gone, // and whatever of that mod's still names it by guid finds nothing // (an item list entry is dropped for that further down). - const bool shaderTable = entry.asset.kind == fb::AssetKind::resource && - (entry.asset.resourceType == fb::shader::programLookupType || - entry.asset.resourceType == fb::shader::textureLookupType); - if (entry.asset.kind != fb::AssetKind::chunk && !shaderTable && !rewritten && previous.mod && + if (entry.asset.kind != fb::AssetKind::chunk && !rewritten && previous.mod && previous.mod != owner && previous.asset.sha1 != entry.asset.sha1 && !state.baseSha.contains(at->first)) { auto& [count, example] = replaced[*previous.mod]; @@ -849,11 +846,6 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot, const Contribution* baseCopy{}; for (const auto& contribution : found->second) if (contribution.base) { baseCopy = &contribution; break; } - // Custom cosmetic bundles also carry the global shader tables, - // although the game's copy of that bundle does not. Use the - // highest-priority contributor as the starting table there. - const bool addedShaderTable = !baseCopy && shaderTable; - if (addedShaderTable) baseCopy = &found->second.back(); if (!baseCopy) continue; // Both mods rebuild the whole bundle, so only the assets whose // content actually moved away from the base are contested. @@ -864,8 +856,7 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot, // An item list is looked at even when one mod alone changed it: // what its entries name has to be in the bundle either way. const auto named = entry.asset.kind == fb::AssetKind::ebx && item_list_name(entry.asset.name); - if (edits.size() < 2 && !(named && edits.size() == 1) && - !(addedShaderTable && !edits.empty())) continue; + if (edits.size() < 2 && !(named && edits.size() == 1)) continue; try { const auto baseBytes = decode(*baseCopy); std::vector> editBytes; @@ -898,8 +889,9 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot, std::to_string(clash.first) + " preset(s) share a short name with another mod's, e.g. " + clash.second + "; they are registered by their full path only"); } else if (shaderTable) { - // Keep each private program and its backing arrays, then - // remap material rows into the combined program table. + // Each mod aliases new material keys onto shader programs + // and texture sets the base already ships, so the union of + // the rows they added is exactly right. std::vector tables; for (std::size_t index = 0; index < editBytes.size(); ++index) tables.push_back({editBytes[index], edits[index]->resourceMeta}); diff --git a/Extension/Music/local_music_playback.cpp b/Extension/Music/local_music_playback.cpp index 66a9a688..b9e32052 100644 --- a/Extension/Music/local_music_playback.cpp +++ b/Extension/Music/local_music_playback.cpp @@ -465,6 +465,34 @@ bool music_playback_available() noexcept { return playback_available.load(std::m MusicSelectNext& music_select_next_original() noexcept { return select_next; } +// The scorer asks for places the asset has no song at (selector_overruns, which sees only what +// the queue holds before the game is asked: 2.0.9 still crashed here with places such as 0xd570, +// far past any list the queue had). The game's callback reads whatever lies there as a song. +// This is that callback with the one check it lacks: a place past the end has no song, which +// the game's own answer for is 0. It replaces the game's whole function, which is these few +// reads. +MusicSongScore song_score{}; +MusicSongScore& music_song_score_original() noexcept { return song_score; } +void __fastcall music_song_score_hook(std::uint32_t place, std::uint32_t* out, std::uintptr_t** context) { + const auto* const songs = *context ? reinterpret_cast(**context) : nullptr; + // (The count is the u32 before the first song, its top bit apart: song_count.) + const auto count = songs ? *(reinterpret_cast(songs) - 1) & 0x7fffffffu : 0u; + if (place >= count) { + *out = 0; + static std::atomic said{}; + if (said.fetch_add(1, std::memory_order_relaxed) < 8) { + try { + dingosdk::logging::log(dingosdk::logging::Level::warning, dingosdk::logging::Channel::music, + "Next song: the game asked for song {} of a playlist asset that has {}, where it would have crashed; " + "it was told there is none there.", place, count); + } catch (...) {} + } + return; + } + const auto song = songs[place] & ~std::uintptr_t{4}; + *out = song ? *reinterpret_cast(song + 0x7c) : 0; +} + // When each channel was last bound to a playlist. std::array, 256> channel_bound_at{}; diff --git a/Extension/Music/local_music_playback.h b/Extension/Music/local_music_playback.h index 7aa9dd45..a4bb0b69 100644 --- a/Extension/Music/local_music_playback.h +++ b/Extension/Music/local_music_playback.h @@ -14,6 +14,10 @@ void activate_music_playback() noexcept; void deactivate_music_playback() noexcept; bool music_playback_available() noexcept; MusicSelectNext& music_select_next_original() noexcept; +// The game's read of one song's number for its scorer, kept within the asset's songs. +using MusicSongScore = void (__fastcall *)(std::uint32_t place, std::uint32_t* out, std::uintptr_t** context); +MusicSongScore& music_song_score_original() noexcept; +void __fastcall music_song_score_hook(std::uint32_t place, std::uint32_t* out, std::uintptr_t** context); std::int32_t __fastcall music_select_next_hook(std::uintptr_t queue, std::uintptr_t playlist_asset); MusicChannelPlaylistBind& music_channel_playlist_bind_original() noexcept; std::uintptr_t __fastcall music_channel_playlist_bind_hook(std::uintptr_t channel_pointer, diff --git a/Extension/Profile/local_profile_runtime.cpp b/Extension/Profile/local_profile_runtime.cpp index 6f468b78..265f9ec4 100644 --- a/Extension/Profile/local_profile_runtime.cpp +++ b/Extension/Profile/local_profile_runtime.cpp @@ -557,6 +557,8 @@ bool initialize_local_profile(std::uintptr_t base, bool authored_offline, "{\"event\":\"music_channel_playlist_binding_contract_mismatch\"}"); hook(addr::local_music::playback_select_next_contract, &music_select_next_hook, music_select_next_original()); + hook(addr::local_music::playback_song_score_contract, + &music_song_score_hook, music_song_score_original()); } else dingosdk::logging::event(dingosdk::logging::Channel::music, "{\"event\":\"music_playback_order_contract_mismatch\"}"); hook(buildkit_text_exists_contract, &buildkit_text_exists, buildkit_text_functions().exists);