Music: release Mods shelf before map unloading

This commit is contained in:
ReSkate local committed 2026-10-06 02:07:26 +01:00
1 parent dcc5e31a67
commit 3db1ef7ca5
7 files changed
+188 -11

No files matched your search

@@ -0,0 +1,84 @@
#include "Extension/Music/local_music_shelf_lifetime.h"
#include <array>
#include <cstdio>
#include <stdexcept>
#include <vector>
namespace md = dingosdk::multiplayer::menu_data;
struct Ref { std::uint64_t record, handle; bool operator==(const Ref&) const = default; };
struct Model { std::uint64_t handle, type; };
void check(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
void shelf_transition(unsigned leaving, unsigned active) {
md::MenuLifetime lifetime;
md::OwnedMenuModels<Model> models;
lifetime.before_transition(13, [] {});
const std::vector<Ref> native{{11, 100}, {22, 200}, {33, 300}};
auto refs = native;
refs.insert(refs.begin() + 1, Ref{22, 401}); // Tagged anchor, same record as native Liked.
models.track({400, 1}); models.track({500, 2});
bool child_bound = true, assets_live = true;
unsigned cleanups{};
std::vector<std::uint64_t> destroyed;
const auto cleanup = [&] {
++cleanups;
check(lifetime.blocked(), "Block shelf creation and polling before detach callbacks");
models.release([&] {
refs = dingosdk::profile_runtime::music_shelf_detached(std::span<const Ref>{refs}, 400);
child_bound = false;
}, [&](Model model) {
check(assets_live, "Destroy widget-bearing rows before unloading their assets");
check(refs == native && !child_bound, "Detach shelf and child bindings before destruction");
destroyed.push_back(model.handle);
});
};
lifetime.before_transition(leaving, cleanup);
assets_live = false;
for (unsigned next : {leaving, 3U, 4U, 5U, 12U}) {
lifetime.before_transition(next, cleanup);
check(lifetime.blocked(), "Loading ticks must not recreate or republish the shelf");
}
check(cleanups == 1 && models.empty() && destroyed == std::vector<std::uint64_t>{500, 400},
"Release both roots exactly once, including the two-row tile list");
assets_live = true;
lifetime.before_transition(active, cleanup);
check(!lifetime.blocked(), "Allow rebuilding in the next active world");
models.track({600, 1});
lifetime.before_transition(24, cleanup);
assets_live = false;
lifetime.before_transition(24, cleanup);
lifetime.before_transition(active, cleanup);
check(lifetime.blocked() && models.empty() && destroyed.back() == 600 && cleanups == 2,
"Shutdown must release a rebuilt shelf and permanently suppress recreation");
}
void partial_generation() {
md::OwnedMenuModels<Model> models;
models.track({400, 1}); // Anchor created, tile-template preparation failed.
unsigned destroys{};
try { models.release([] { throw std::runtime_error("detach failed"); }, [&](Model) { ++destroys; }); }
catch (const std::runtime_error&) {}
check(!models.empty() && !destroys, "Keep failed cleanup ownership for a retry");
models.release([] {}, [&](Model) { ++destroys; });
check(models.empty() && destroys == 1, "Release partial builds as well as published shelves");
models.track({400, 1});
models.manager_replaced();
models.release([] {}, [](Model) { throw std::runtime_error("expired manager dereferenced"); });
}
int main() {
try {
for (unsigned leaving : {14U, 22U, 3U}) shelf_transition(leaving, leaving == 22 ? 21 : 13);
partial_generation();
const std::array<Ref, 1> native{{{1, 100}}};
check(dingosdk::profile_runtime::music_shelf_detached(std::span<const Ref>{native}, 400) ==
std::vector<Ref>{native.begin(), native.end()}, "Expired/missing shelf removal preserves native entries");
std::puts("Music shelf lifetime tests passed.");
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
}
+69 -11
View File
@@ -1,4 +1,5 @@
#include "Extension/Music/local_music_shelf.h"
#include "Extension/Music/local_music_shelf_lifetime.h"
#include "Extension/Music/local_music_assets.h"
#include "Engine/Core/Log/logging.h"
#include "Engine/Game/Abi/native_data.h"
@@ -27,11 +28,50 @@ std::string music_diag_hex(std::uint64_t value, int width) {
namespace md = dingosdk::multiplayer::menu_data;
struct ModsShelf {
std::uintptr_t manager{};
md::Value anchor{}, tiles{}, featured_anchor{};
md::Value anchor{}, tiles{}, featured_anchor{}, list{};
ULONGLONG next_poll{};
std::vector<std::byte> published_rows;
};
ModsShelf mods_shelf;
md::MenuLifetime music_shelf_lifetime;
md::OwnedMenuModels<md::Value> music_shelf_models;
bool music_shelf_cleaning{};
void music_release_shelf(std::uintptr_t base) {
if (music_shelf_models.empty()) { mods_shelf = {}; return; }
// The provider can disappear before this callback. Use the common UI model
// manager, as native-menu cleanup does, rather than requiring a live provider.
std::uintptr_t ui{}, manager{};
md::require(read(base + addr::engine::ui_manager, ui) && ui &&
read(ui + 0x140, manager) && manager, "Music shelf cleanup manager is unavailable.");
if (manager != mods_shelf.manager) {
music_shelf_models.manager_replaced();
mods_shelf = {};
return;
}
const bool was_cleaning = music_shelf_cleaning;
music_shelf_cleaning = true;
struct Restore { bool previous; ~Restore() { music_shelf_cleaning = previous; } } restore{was_cleaning};
game::ModelWriteLock lock(manager);
const md::Context context{base, manager};
music_shelf_models.release([&] {
if (mods_shelf.list.handle && context.type_of(mods_shelf.list.handle) == mods_shelf.list.type) {
const auto items = context.field(mods_shelf.list, 0x67223da7U);
unsigned count{}, stride{};
const auto bytes = context.array(items, 64, count, stride);
md::require(stride == sizeof(md::Ref), "Music shelf cleanup list differs.");
std::vector<md::Ref> refs(count);
if (!bytes.empty()) std::memcpy(refs.data(), bytes.data(), bytes.size());
const auto retained = music_shelf_detached(std::span<const md::Ref>{refs}, mods_shelf.anchor.handle);
if (retained.size() != refs.size()) context.array(items, retained);
}
// Unbind the child before reverse-creation-order destruction. Both roots
// must release their widget references while the outgoing assets live.
if (mods_shelf.anchor.handle && context.type_of(mods_shelf.anchor.handle) == mods_shelf.anchor.type)
context.set(context.path(mods_shelf.anchor, {0x214d4984U, 0x25e4d6c8U}), md::Ref{});
}, [&](md::Value value) { context.destroy(value); });
mods_shelf = {};
}
// Read inline typed fields without creating child handles during authored construction.
std::uintptr_t music_inline_path(const md::Context& context, std::uintptr_t data,
std::uintptr_t type, std::initializer_list<std::uint32_t> hashes) {
@@ -89,7 +129,8 @@ void music_sync_mods(const md::Context& context) {
dingosdk::Json{{"event", "music_mods_shelf_fill"}, {"count", mods}, {"featured", count}}.dump().c_str());
}
void music_poll_mods(std::uintptr_t base) {
if (!mods_shelf.manager || GetTickCount64() < mods_shelf.next_poll) return;
if (music_shelf_lifetime.blocked() || music_shelf_cleaning ||
!mods_shelf.manager || GetTickCount64() < mods_shelf.next_poll) return;
mods_shelf.next_poll = GetTickCount64() + 250;
std::uintptr_t provider{}, manager{};
if (!read(base + addr::local_music::ui_manager, provider) || !provider ||
@@ -138,7 +179,14 @@ std::string music_shelf_append_one(std::uintptr_t base, std::uintptr_t model, st
std::memcpy(&record_ref, bytes.data() + stride, 8);
std::memcpy(&source_vm, bytes.data() + stride + 8, 8);
const auto source_type = shelf.type_of(source_vm & ~std::uint64_t{1});
// A screen can be recreated in the same manager without changing maps.
// Detach and release the old generation, rather than dropping its handles.
music_release_shelf(base);
mods_shelf.manager = model;
mods_shelf.list = root.model;
auto new_vm = shelf.create(md::Schema{0x0e3be640U, 192}, (GetTickCount64() << 12) | 0x4dU);
mods_shelf.anchor = new_vm;
music_shelf_models.track(new_vm);
md::Value new_tiles{};
try {
const md::Value source_anchor{source_vm, source_type};
@@ -153,14 +201,15 @@ std::string music_shelf_append_one(std::uintptr_t base, std::uintptr_t model, st
tile_data = md::read<std::uintptr_t>(tile_ref.record + 0x20);
md::require(tile_data != 0, "Music tile template is not available.");
new_tiles = shelf.create(md::Schema{0x74b1ea1dU, 392}, (GetTickCount64() << 12) | 0x4eU);
mods_shelf.tiles = new_tiles;
music_shelf_models.track(new_tiles);
shelf.copy(new_tiles, tile_data);
shelf.text(shelf.path(new_tiles, {0xfaec2d05U, 0xcb79482dU, 0x4d8e01b9U}), "Mods");
// An independent list retains each native row's actions and artwork on publication.
shelf.array(shelf.field(new_tiles, 0x67223da7U), std::span<const std::byte>{}, 0);
shelf.set(shelf.path(new_vm, {0x214d4984U, 0x25e4d6c8U}), md::Ref{0, new_tiles.handle});
} catch (...) {
if (new_tiles.handle) shelf.destroy(new_tiles);
shelf.destroy(new_vm);
music_release_shelf(base);
throw;
}
std::vector<std::byte> element(stride);
@@ -169,12 +218,7 @@ std::string music_shelf_append_one(std::uintptr_t base, std::uintptr_t model, st
auto merged = bytes;
merged.insert(merged.begin(), element.begin(), element.end());
shelf.array(items, merged, count + 1);
const auto previous = mods_shelf;
mods_shelf = {model, new_vm, new_tiles, md::Value{target, shelf.type_of(target)}, 0};
// Release our previous generation after the new shelf has been published.
if (previous.manager == model) {
try { shelf.destroy(previous.anchor); shelf.destroy(previous.tiles); } catch (const std::exception&) {}
}
mods_shelf.featured_anchor = md::Value{target, shelf.type_of(target)};
try { music_sync_mods(shelf); } catch (const std::exception&) {}
unsigned after = 0, after_stride = 0;
shelf.array(items, 64, after, after_stride);
@@ -196,6 +240,7 @@ std::uint64_t music_model_construct_hook(std::uintptr_t manager, std::uint8_t mo
std::uint64_t id, std::uintptr_t type, bool flag, std::uintptr_t record) {
const auto handle = music_model_construct_original(manager, mode, id, type, flag, record);
PreserveError preserve;
if (music_shelf_lifetime.blocked() || music_shelf_cleaning) return handle;
try {
if (!handle || !type || md::read<std::uint32_t>(md::read<std::uintptr_t>(type)) != 0x48455d84U)
return handle;
@@ -229,7 +274,7 @@ std::uint64_t music_model_construct_hook(std::uintptr_t manager, std::uint8_t mo
for (const auto& playlist : mod_music_playlists())
if (!playlist.songs.empty()) { has_mod_playlists = true; break; }
if (!has_mod_playlists) {
mods_shelf = {};
music_release_shelf(local_runtime().base);
dingosdk::logging::event(dingosdk::logging::Channel::music, "{\"event\":\"music_mods_shelf_skipped\"}");
return handle;
}
@@ -240,6 +285,19 @@ std::uint64_t music_model_construct_hook(std::uintptr_t manager, std::uint8_t mo
return handle;
}
void music_shelf_before_level_transition(unsigned next) noexcept {
try {
music_shelf_lifetime.before_transition(next, [&] {
music_release_shelf(local_runtime().base);
dingosdk::logging::event(dingosdk::logging::Channel::music,
"{\"event\":\"music_mods_shelf_released\"}");
});
} catch (...) {
try { dingosdk::logging::event(dingosdk::logging::Channel::music,
"{\"event\":\"music_mods_shelf_cleanup_failed\"}"); } catch (...) {}
}
}
void update_music_shelf() {
auto& runtime = local_runtime();
if (!runtime.active.load(std::memory_order_acquire)) return;
+1
View File
@@ -12,4 +12,5 @@ extern MusicModelConstruct music_model_construct_original;
std::uint64_t music_model_construct_hook(std::uintptr_t manager, std::uint8_t mode,
std::uint64_t id, std::uintptr_t type, bool flag, std::uintptr_t record);
void update_music_shelf();
void music_shelf_before_level_transition(unsigned next) noexcept;
} // namespace dingosdk::profile_runtime
@@ -0,0 +1,18 @@
#pragma once
#include "Extension/UI/NativeMenu/native_menu_lifetime.h"
#include <cstdint>
#include <span>
#include <vector>
namespace dingosdk::profile_runtime {
// Remove only our independently owned anchor. Preserve native records, handles,
// and order, including when another shelf was inserted ahead of ours.
template<class Ref>
std::vector<Ref> music_shelf_detached(std::span<const Ref> refs, std::uint64_t anchor) {
std::vector<Ref> retained;
retained.reserve(refs.size());
for (const auto& ref : refs)
if ((ref.handle & ~std::uint64_t{1}) != anchor) retained.push_back(ref);
return retained;
}
}
@@ -340,6 +340,7 @@ void local_profile_before_level_transition(unsigned next) noexcept {
auto& s = local_runtime();
if (!s.active.load(std::memory_order_acquire)) return;
std::lock_guard lock(s.native_mutex);
music_shelf_before_level_transition(next);
news_runtime().pending.before_transition(next);
object_runtime().pending.before_transition(next);
auto& placements = placements_runtime();
+4
View File
@@ -52,6 +52,10 @@ if(DINGOSDK_BUILD_LAUNCHER_TESTS AND WIN32)
target_include_directories(dingosdk_music_safety_tests PRIVATE "${PROJECT_SOURCE_DIR}")
dingosdk_configure_target(dingosdk_music_safety_tests)
add_test(NAME music_safety COMMAND dingosdk_music_safety_tests)
add_executable(dingosdk_music_shelf_lifetime_tests Extension/Music/Test/local_music_shelf_lifetime_tests.cpp)
target_include_directories(dingosdk_music_shelf_lifetime_tests PRIVATE "${PROJECT_SOURCE_DIR}")
dingosdk_configure_target(dingosdk_music_shelf_lifetime_tests)
add_test(NAME music_shelf_lifetime COMMAND dingosdk_music_shelf_lifetime_tests)
add_executable(dingosdk_music_artwork_tests Extension/Music/Test/music_artwork_tests.cpp Extension/Music/music_artwork.cpp)
target_link_libraries(dingosdk_music_artwork_tests PRIVATE dingosdk_mods ws2_32 winhttp)
dingosdk_configure_target(dingosdk_music_artwork_tests)
+11
View File
@@ -273,6 +273,17 @@ when replacing its tracked generation in the same manager. Reopening and switchi
screens must be tested: a valid handle from one observation is not proof of its
validity after scene teardown.
The Mods shelf participates in the existing pre-level-transition callback. Before
leaving a level/sublevel or shutting down, it removes its anchor from the native
shelf list, clears the anchor's tile binding, and destroys both owned roots while
their widget assets are still loaded. Construction and row polling are suspended
during loading and resume when the next world becomes active. Recreating the
screen also releases the previous generation, including partially built roots.
This addresses the observed stale widget-reference crash at `0x143DC5EAA` during
map unloading. The installed Release build was confirmed in game to resolve the
reported shelf-creation/map-switch reproduction; the lifetime tests separately
cover ownership and transition ordering.
## Inspecting a screen yourself
ReSkate's console exposes the live model dumper: