Music screen: large playlists no longer empty it, and two logs say why things fail

Music screen: large playlists no longer empty it, and two logs say why things fail
Music screen
- The screen's list was turned down whole, and came up empty (the game's own
  Featured row too), once it held more than 1,024 songs: a few large YouTube
  Music, Spotify or local playlists went over that. The most is now 2,000
  songs and 16,384 places in playlists (music_ui_most_songs,
  music_ui_most_places); 2,000 because the screen's own table of songs is
  read only up to 2,048.
- The rows of the player's playlists are counted as they are registered and
  held under that: with no room left, the rest of a playlist is left out of
  its list (it still plays) and the screen is never empty for it.
- A row whose "artist - title" is longer than the screen takes is left out;
  one such row turned the whole list down as well.
- Said in the log: how many songs were left unlisted, and, should the list
  still be turned down, what it held.
Logs
- Spotify device: librespot's own warnings and errors are written at the
  warning level again; only the rest of what it says stays at debug. A
  player whose Spotify connects and stays silent had nothing to send.
- Multiplayer: a message turned down with "Join code, sender identity, or
  multiplayer protocol did not match" now says which of the three it was,
  its size and first bytes, and whether it came from a dedicated server.
This commit is contained in:
AyeZeeBB committed 2026-10-13 17:38:50 -05:00
1 parent 2e40aeb6e2
commit f356c1eafd
12 files changed
+191 -12

No files matched your search

@@ -0,0 +1,19 @@
#pragma once
#include "Engine/Game/Build/fingerprint.h"
#include <array>
#include <cstdint>
// 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;
}
@@ -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 <algorithm>
#include <format>
#include <iterator>
#include <stdexcept>
#include <utility>
@@ -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<unsigned>(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<int>(decoded->kind))
: std::format("it names another sender, {} (kind {})", decoded->source, static_cast<int>(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;
+30 -5
View File
@@ -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<std::vector<std::pair<std::string, std::string>>> 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;
+17 -6
View File
@@ -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<volatile LONG*>(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<std::string> songs, playlists;
std::set<std::pair<std::string, std::string>> 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<const void*>(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;
+6
View File
@@ -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);
+4 -1
View File
@@ -79,7 +79,10 @@ void told(const char* text) {
if (parts.empty()) return;
if (parts[0] == "LOG") {
static std::atomic<int> 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") {
+5
View File
@@ -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)) {
+3
View File
@@ -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
@@ -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 <array>
#include <atomic>
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<bool> installed{};
std::atomic<std::uint64_t> 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<const std::uintptr_t *const *>(shader + sp::permutations_begin);
const auto *const end = *reinterpret_cast<const std::uintptr_t *const *>(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<unsigned char, 32> 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<void*>(base + sp::prepare.rva);
if (hook_prepare(target, reinterpret_cast<void*>(&guarded), reinterpret_cast<void**>(&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
@@ -0,0 +1,16 @@
#pragma once
#include <cstdint>
// 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/<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;
}
+2
View File
@@ -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);
+1
View File
@@ -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