mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 12:57:01 +02:00
Music: release Mods shelf before map unloading
This commit is contained in:
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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user