diff --git a/Engine/Game/Build/20260929/shader_permutations.h b/Engine/Game/Build/20260929/shader_permutations.h new file mode 100644 index 0000000..7dc8cf5 --- /dev/null +++ b/Engine/Game/Build/20260929/shader_permutations.h @@ -0,0 +1,19 @@ +#pragma once +#include "Engine/Game/Build/fingerprint.h" +#include +#include + +// The game's expression shaders (Extension/World/shader_permutation_guard.cpp). +namespace dingosdk::game::build::v20260929::shader_permutations { +// prepare(shader, renderer, state, bool, bool): run on a job worker (from 0x3f72b90, for every +// shader each view has registered, and from 0x3f79b70 for one). The shader keeps its compiled +// permutations as an array of pointers (begin at +0x50, end at +0x58). The function asks the +// first one for its kind (its first virtual function: ExpressionShaderRasterPermutation or +// ExpressionShaderRaytracePermutation) without checking it is there, then hands each to +// 0x46aa370 or 0x46aac20: +// mov rax,[rcx+0x50]; ...; mov rcx,[rax]; mov rax,[rcx] <- 0x3f86da6, rcx null +inline constexpr Fingerprint prepare{0x3f86d80, { + 0x48,0x89,0x5c,0x24,0x18,0x48,0x89,0x6c,0x24,0x20,0x56,0x57,0x41,0x54,0x48,0x83, + 0xec,0x40,0x48,0x8b,0x41,0x50,0x48,0x8b,0xf1,0x45,0x0f,0xb6,0xe1,0x49,0x8b,0xd8}}; +inline constexpr std::uintptr_t permutations_begin = 0x50, permutations_end = 0x58; +} diff --git a/Extension/Multiplayer/Session/session_receive.cpp b/Extension/Multiplayer/Session/session_receive.cpp index ecb82eb..47cbd47 100644 --- a/Extension/Multiplayer/Session/session_receive.cpp +++ b/Extension/Multiplayer/Session/session_receive.cpp @@ -1,8 +1,10 @@ #include "session_internal.h" #include "Extension/Profile/local_profile_runtime.h" #include "Extension/Multiplayer/Net/wire_codec.h" +#include "Engine/Core/Log/logging.h" #include "Engine/Game/Build/supported_build.h" #include +#include #include #include #include @@ -593,6 +595,20 @@ void networking(Session &s, const NativeFrame &local, std::uint64_t now) { continue; if (!decoded || decoded->session != s.secret || ((s.mode == Mode::host || direct_link) && decoded->source != message.peer)) { + // Which of the three it was, and what the message looked like: the status line + // says only that one of them failed (2026-10-13: joins to updated servers ended + // here with nothing to tell them apart). + try { + std::string start; + for (std::size_t i = 0; i < message.bytes.size() && i < 16; ++i) start += std::format("{:02x}", static_cast(message.bytes[i])); + dingosdk::logging::log(dingosdk::logging::Level::warning, dingosdk::logging::Channel::runtime, + "Multiplayer: a message from {} was turned down: {}. {} byte(s), starting {}; {} link{}{}.", message.peer, + !decoded ? std::string("it could not be read") + : decoded->session != s.secret ? std::format("it is of another session (kind {})", static_cast(decoded->kind)) + : std::format("it names another sender, {} (kind {})", decoded->source, static_cast(decoded->kind)), + message.bytes.size(), start, direct_link ? "a direct" : "the host's", batch.empty() ? "" : ", one of a batch", + s.mode == Mode::join && dedicated_host(s) ? ", a dedicated server" : ""); + } catch (...) {} disconnect(s, message.peer, "Join code, sender identity, or multiplayer protocol did not match."); if (s.mode == Mode::off) return; diff --git a/Extension/Music/local_music_assets.cpp b/Extension/Music/local_music_assets.cpp index cc1899c..fee3e79 100644 --- a/Extension/Music/local_music_assets.cpp +++ b/Extension/Music/local_music_assets.cpp @@ -98,16 +98,41 @@ bool read_music_catalog(MusicCatalog& result) { // For each station, its rows' ids and the Spotify track each is (none for a row that only // says what picking the playlist does). std::vector>> spotify_row_ids; + // The music screen takes only so many songs, and so many places in playlists, and with more + // is given none at all (music_ui_catalog_valid). Rows are registered while there is room, + // under those by a margin for the game's own; the rest of a playlist is left out of its rows + // (it still plays through: only its list is shorter). + std::uint32_t registered{}; + read(manager + 0x84, registered); + std::size_t room = registered + 200 < music_ui_most_songs ? music_ui_most_songs - 200 - registered : 0; + std::size_t places = music_ui_most_places / 2, left_out{}; for (const auto& theirs : spotify_playlists) { auto& rows = spotify_row_ids.emplace_back(); - for (const auto& track : theirs.tracks) - if (music_ui_text(track.artist, 200) && music_ui_text(track.title, 200)) - if (auto id = spotify_rows::ensure(base, manager, track.artist, track.title); !id.empty() && - std::none_of(rows.begin(), rows.end(), [&](const auto& row) { return row.first == id; })) - rows.emplace_back(std::move(id), track.uri); + for (const auto& track : theirs.tracks) { + // (Its id, "artist - title", has to be one the screen takes as well.) + if (!music_ui_text(track.artist, 200) || !music_ui_text(track.title, 200) || track.artist.size() + track.title.size() + 3 > 255) continue; + const bool known = spotify_rows::known(track.artist, track.title); + if (!places || (!known && !room)) { + ++left_out; + continue; + } + if (auto id = spotify_rows::ensure(base, manager, track.artist, track.title); !id.empty() && + std::none_of(rows.begin(), rows.end(), [&](const auto& row) { return row.first == id; })) { + rows.emplace_back(std::move(id), track.uri); + --places; + if (!known) --room; + } + } if (theirs.id == spotify_station::plain_station) rows.emplace_back(spotify_rows::ensure(base, manager, "Spotify", "Start Radio"), std::string()); else if (rows.empty() && music_ui_text(theirs.name, 200)) rows.emplace_back(theirs.id == external_audio::station ? spotify_rows::ensure(base, manager, external_audio::station_name, "Start") : spotify_rows::ensure(base, manager, "Spotify", "Play " + theirs.name), std::string()); } + if (static std::size_t said{}; left_out != said) { + said = left_out; + if (left_out) + dingosdk::logging::log(dingosdk::logging::Level::warning, dingosdk::logging::Channel::music, + "Music screen: {} song(s) of your playlists are not listed: it has room for about {} songs in all. They still play; fewer or shorter playlists list them all.", + left_out, music_ui_most_songs); + } native.lock(manager + 0x20); struct Unlock { game::NativeModelFunctions::Lock function; diff --git a/Extension/Music/local_music_ui.cpp b/Extension/Music/local_music_ui.cpp index 870eb21..b120df0 100644 --- a/Extension/Music/local_music_ui.cpp +++ b/Extension/Music/local_music_ui.cpp @@ -31,9 +31,9 @@ std::string music_artwork_url(std::string_view value) { // MusicGraphAsset metadata/TagRefs. No generated catalog, guessed memberships, // artwork or audio-residency gate. Favorite identities are saved separately in -// the local profile and applied after song publication. Bounds: 1024 songs, +// the local profile and applied after song publication. Bounds: 2000 songs, -// 256 authored groups, 8192 membership edges. Include after assets/news/model helpers. +// 256 authored groups, 16384 membership edges (music_ui_most_songs, music_ui_most_places). Include after assets/news/model helpers. void music_release_weak(std::uintptr_t owner) noexcept { if (owner && InterlockedDecrement(reinterpret_cast(owner + 12)) == 0) { @@ -117,19 +117,19 @@ bool music_ui_text(std::string_view value, std::size_t limit ) { } bool music_ui_catalog_valid(const MusicCatalog& catalog, const std::string& favorites) { - if (catalog.songs.empty() || catalog.songs.size() > 1024 || catalog.playlists.empty() || catalog.playlists.size() > 256) return false; + if (catalog.songs.empty() || catalog.songs.size() > music_ui_most_songs || catalog.playlists.empty() || catalog.playlists.size() > 256) return false; std::set songs, playlists; std::set> declared, grouped; for (const auto& song : catalog.songs) { if (!music_ui_text(song.id) || !music_ui_text(song.artist, 512) || !music_ui_text(song.title, 512) || song.id != song.artist + " - " + song.title || !songs.insert(song.id).second) return false; for (const auto& playlist : song.playlists) - if (!music_ui_text(playlist) || !declared.emplace(playlist, song.id).second || declared.size() > 8192) return false; + if (!music_ui_text(playlist) || !declared.emplace(playlist, song.id).second || declared.size() > music_ui_most_places) return false; } for (const auto& playlist : catalog.playlists) { if (!music_ui_text(playlist.id) || playlist.id == favorites || !playlists.insert(playlist.id).second) return false; for (const auto& id : playlist.songs) - if (!songs.contains(id) || !grouped.emplace(playlist.id, id).second || grouped.size() > 8192) return false; + if (!songs.contains(id) || !grouped.emplace(playlist.id, id).second || grouped.size() > music_ui_most_places) return false; } return declared == grouped; } @@ -333,7 +333,18 @@ void update_music_catalog() { if (!music_ui_current(*pending)) { retry = false; return; } std::string favorite_id; if (!identifier(reinterpret_cast(pending->manager + 8), favorite_id) || - !music_ui_catalog_valid(catalog, favorite_id)) throw std::runtime_error("Invalid runtime music catalog"); + !music_ui_catalog_valid(catalog, favorite_id)) { + // (Said once, with what it held: an empty music screen has this to go on.) + static bool said{}; + if (!std::exchange(said, true)) { + std::size_t places{}; + for (const auto& playlist : catalog.playlists) places += playlist.songs.size(); + dingosdk::logging::log(dingosdk::logging::Level::warning, dingosdk::logging::Channel::music, + "Music screen: its list of songs was turned down, and it is empty: {} song(s) (the most is {}), {} playlist(s) (256), {} place(s) in playlists ({}).", + catalog.songs.size(), music_ui_most_songs, catalog.playlists.size(), places, music_ui_most_places); + } + throw std::runtime_error("Invalid runtime music catalog"); + } std::uintptr_t arena{}, vtable{}; if (!read(s.base + addr::engine::default_arena, arena) || !arena || !read(arena, vtable) || vtable < s.base || vtable >= s.base + supported_build::game_image_size) return; ui.functions.allocator[1] = arena; diff --git a/Extension/Music/local_music_ui.h b/Extension/Music/local_music_ui.h index 92bb93f..886bf79 100644 --- a/Extension/Music/local_music_ui.h +++ b/Extension/Music/local_music_ui.h @@ -62,6 +62,12 @@ bool music_ui_current(const MusicUiPending& pending); CosmeticShared music_ui_lease(const MusicUiPending& pending); bool music_ui_text(std::string_view value, std::size_t limit = 255); +// The most songs the music screen is given, and the most places songs have in playlists: over +// either, music_ui_catalog_valid turns the whole list down and the screen is empty. The rows of +// the player's own playlists (Spotify, YouTube Music, their files) are held under them +// (local_music_assets.cpp): a few large playlists went over the old thousand (2026-10-13). +// (Two thousand songs: the screen's own table of songs is read only up to 2048, music_ui_context.) +inline constexpr std::size_t music_ui_most_songs = 2000, music_ui_most_places = 16384; bool music_ui_catalog_valid(const MusicCatalog& catalog, const std::string& favorites); diff --git a/Extension/Music/spotify_device.cpp b/Extension/Music/spotify_device.cpp index 6d0bc59..39d48ab 100644 --- a/Extension/Music/spotify_device.cpp +++ b/Extension/Music/spotify_device.cpp @@ -79,7 +79,10 @@ void told(const char* text) { if (parts.empty()) return; if (parts[0] == "LOG") { static std::atomic logged{}; - if (parts.size() > 1 && logged.fetch_add(1, std::memory_order_relaxed) < 300) say(logging::Level::debug, "(librespot) " + parts[1]); + // (What it says went wrong is for everyone's log: a player whose Spotify stays silent + // has it to send. The rest is for working on it.) + const bool wrong = parts.size() > 1 && (parts[1].starts_with("ERROR") || parts[1].starts_with("WARN")); + if (parts.size() > 1 && logged.fetch_add(1, std::memory_order_relaxed) < 300) say(wrong ? logging::Level::warning : logging::Level::debug, "(librespot) " + parts[1]); // (What it says just before it gives up on an account that is not Premium.) if (parts.size() > 1 && parts[1].find("does not support") != std::string::npos && parts[1].find("accounts") != std::string::npos) not_premium.store(true); } else if (parts[0] == "READY") { diff --git a/Extension/Music/spotify_rows.cpp b/Extension/Music/spotify_rows.cpp index 623a350..e177241 100644 --- a/Extension/Music/spotify_rows.cpp +++ b/Extension/Music/spotify_rows.cpp @@ -248,6 +248,11 @@ bool install(std::uintptr_t base) noexcept { return true; } +bool known(const std::string& artist, const std::string& title) { + std::lock_guard lock(mutex); + return rows.contains(artist + " - " + title); +} + std::string ensure(std::uintptr_t base, std::uintptr_t manager, const std::string& artist, const std::string& title) { if (!base || !manager || artist.empty() || title.empty()) return {}; if (!matches(base, lm::song_register_contract) || !matches(base, lm::song_lookup_contract)) { diff --git a/Extension/Music/spotify_rows.h b/Extension/Music/spotify_rows.h index f7ba44c..3b9a8ce 100644 --- a/Extension/Music/spotify_rows.h +++ b/Extension/Music/spotify_rows.h @@ -12,6 +12,9 @@ namespace dingosdk::spotify_rows { // yet: its id in the music screen ("artist - title"), or empty when it could not be made. // The thread that reads the catalog; not with the manager's lock held. std::string ensure(std::uintptr_t base, std::uintptr_t manager, const std::string& artist, const std::string& title); +// Whether a row of that artist and title has been registered already (it then costs the music +// screen nothing more to use it again). +bool known(const std::string& artist, const std::string& title); // The id ("artist - title") of the row known to the game by these 16 bytes, or empty. std::string id_of(const unsigned char* row); // Watches for the game beginning to play one of these (the player picked its row), which diff --git a/Extension/World/shader_permutation_guard.cpp b/Extension/World/shader_permutation_guard.cpp new file mode 100644 index 0000000..3c3c93a --- /dev/null +++ b/Extension/World/shader_permutation_guard.cpp @@ -0,0 +1,72 @@ +#include "shader_permutation_guard.h" +#include "Engine/Core/Hooks/hooks.h" +#include "Engine/Core/Log/logging.h" +#include "Engine/Core/Platform/memory.h" +#include "Engine/Game/Build/addresses.h" +#include "Engine/Game/Build/20260929/shader_permutations.h" +#include +#include + +namespace dingosdk::shader_permutation_guard { +namespace { +namespace sp = addr::shader_permutations; +using Prepare = void (*)(std::uintptr_t, void*, void*, std::uint8_t, std::uint8_t); +Prepare original{}; +std::atomic installed{}; +std::atomic skips{}; + +// Whether every permutation the game is about to use is there. The game reads the first one +// whatever the array's length, so an array without storage counts as missing too. This is +// asked for every shader of every view: two reads and a short walk, nothing more. +bool complete(std::uintptr_t shader) noexcept { + if (!shader) return true; // (not this guard's: the game's own fault, left as it is) + const auto *const begin = *reinterpret_cast(shader + sp::permutations_begin); + const auto *const end = *reinterpret_cast(shader + sp::permutations_end); + if (!begin || !*begin) return false; + if (end < begin || end - begin > 4096) return true; + for (const auto *at = begin; at != end; ++at) + if (!*at) return false; + return true; +} +// Said for the first few, then every thousandth: a shader that is passed over is passed over +// on every pass. +__declspec(noinline) void note_skip(std::uintptr_t shader) noexcept { + const auto count = skips.fetch_add(1, std::memory_order_relaxed) + 1; + if (count > 8 && count % 1000) return; + try { + logging::log(logging::Level::warning, logging::Channel::runtime, + "Shaders: a shader ({:#x}) has a compiled permutation missing, where the game would have crashed; it is not " + "prepared, and what uses it may not be drawn ({} time(s) since the game started). A mod's material that names a " + "shader the game could not build does this.", shader, count); + } catch (...) {} +} +void guarded(std::uintptr_t shader, void* renderer, void* state, std::uint8_t fourth, std::uint8_t fifth) { + if (!complete(shader)) { + note_skip(shader); + return; + } + original(shader, renderer, state, fourth, fifth); +} +} // namespace + +bool install(std::uintptr_t base) noexcept { + if (installed.exchange(true)) return original != nullptr; + if (!base) return false; + std::array head{}; + if (!memory::peek(base + sp::prepare.rva, head) || head != sp::prepare.bytes) { + logging::log(logging::Level::warning, logging::Channel::runtime, + "Shader permutation guard: this build's shader code differs; it is left as it is."); + return false; + } + const auto target = reinterpret_cast(base + sp::prepare.rva); + if (hook_prepare(target, reinterpret_cast(&guarded), reinterpret_cast(&original)) != HookOk || + hook_enable(target) != HookOk) { + original = nullptr; + logging::log(logging::Level::warning, logging::Channel::runtime, "Shader permutation guard: the game's function could not be hooked."); + return false; + } + return true; +} + +std::uint64_t skipped() noexcept { return skips.load(std::memory_order_relaxed); } +} // namespace dingosdk::shader_permutation_guard diff --git a/Extension/World/shader_permutation_guard.h b/Extension/World/shader_permutation_guard.h new file mode 100644 index 0000000..5ed62a4 --- /dev/null +++ b/Extension/World/shader_permutation_guard.h @@ -0,0 +1,16 @@ +#pragma once +#include + +// The most reported crash of 2.0.8 (Sentry, 2026-10-13: Skate.exe+0x3f86da6 on some thirty +// machines within the hour): as the game prepares its shaders on a job worker, it reads the +// first compiled permutation of each without checking there is one +// (Engine/Game/Build//shader_permutations.h). A shader whose permutation never came to +// be (a material that names a shader program the game could not build) holds null there. The +// guard passes such a shader over: it is not prepared, and what it would have drawn may be +// missing, where the game would have closed. +namespace dingosdk::shader_permutation_guard { +// Checks this build's function and hooks it; once. False when the build differs (no guard). +bool install(std::uintptr_t base) noexcept; +// How many times a shader was passed over since the game started. +std::uint64_t skipped() noexcept; +} diff --git a/Runtime/runtime_init.cpp b/Runtime/runtime_init.cpp index e22d1d1..383ed04 100644 --- a/Runtime/runtime_init.cpp +++ b/Runtime/runtime_init.cpp @@ -8,6 +8,7 @@ #include "Extension/Settings/replay_guard.h" #include "Extension/World/load_error_guard.h" #include "Extension/World/mesh_unload_guard.h" +#include "Extension/World/shader_permutation_guard.h" #include "Extension/World/stack_check_report.h" #include "Extension/Music/music_inject.h" #include "Extension/Music/spotify_web.h" @@ -327,6 +328,7 @@ extern "C" __declspec(dllexport) BOOL WINAPI DingoSDKDebugInitialize() { (void)dingosdk::engine_tweaks::install(r.base); (void)dingosdk::replay_guard::install(r.base); (void)dingosdk::mesh_unload_guard::install(r.base); + (void)dingosdk::shader_permutation_guard::install(r.base); (void)dingosdk::load_error_guard::install(r.base); (void)dingosdk::texture_handle_limit::install(r.base); (void)dingosdk::stack_check_report::install(r.base); diff --git a/cmake/Runtime.cmake b/cmake/Runtime.cmake index eb23e16..1ce5edf 100644 --- a/cmake/Runtime.cmake +++ b/cmake/Runtime.cmake @@ -84,6 +84,7 @@ add_library(dingosdk_runtime SHARED Extension/Music/app_audio_capture.cpp Extension/Music/music_inject.cpp Extension/World/mesh_unload_guard.cpp + Extension/World/shader_permutation_guard.cpp Extension/World/stack_check_report.cpp Extension/World/texture_handle_limit.cpp Extension/Settings/replay_drafts.cpp