Music: isolate native shelf implementation

This commit is contained in:
ReSkate local committed 2026-10-05 05:20:49 +01:00
1 parent 916e281e69
commit 96cd684e20
7 files changed
+250 -218

No files matched your search

+229
View File
@@ -0,0 +1,229 @@
#include "Extension/Music/local_music_shelf.h"
#include "Engine/Core/Log/logging.h"
#include "Engine/Game/Abi/native_data.h"
#include "Engine/Game/Build/addresses.h"
#include "Engine/Game/Build/20260929/engine.h"
#include "Engine/Game/Build/20260929/local_music.h"
#include "Extension/Profile/runtime_internal.h"
#include "Extension/UI/NativeMenu/native_menu_data.h"
#include <algorithm>
#include <cstring>
#include <exception>
#include <initializer_list>
#include <string>
#include <utility>
#include <vector>
namespace dingosdk::profile_runtime {
namespace {
// Format model handles for shelf publication logs.
std::string music_diag_hex(std::uint64_t value, int width) {
static constexpr char digits[] = "0123456789abcdef";
std::string out(static_cast<std::size_t>(width), '0');
for (int i = width - 1; i >= 0; --i, value >>= 4) out[static_cast<std::size_t>(i)] = digits[value & 15];
return out;
}
namespace md = dingosdk::multiplayer::menu_data;
struct ModsShelf {
std::uintptr_t manager{};
md::Value anchor{}, tiles{}, featured_anchor{};
ULONGLONG next_poll{};
std::vector<std::byte> published_rows;
};
ModsShelf 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) {
for (auto hash : hashes) {
const auto field = context.member(type, hash);
data += field.offset;
type = field.type;
}
return data;
}
md::Ref music_tile_ref(const md::Context& context, md::Value anchor) {
return md::read<md::Ref>(music_inline_path(context, context.address(anchor), anchor.type,
{0x214d4984U, 0x25e4d6c8U}));
}
void music_sync_mods(const md::Context& context) {
if (mods_shelf.manager != context.manager || !mods_shelf.tiles.handle) return;
const auto ref = music_tile_ref(context, mods_shelf.featured_anchor);
const auto handle = ref.handle & ~std::uint64_t{1};
if (!handle) return;
const md::Value featured{handle, context.type_of(handle)};
md::require(featured.type == mods_shelf.tiles.type, "Featured tile schema differs.");
const auto source_items = context.field(featured, 0x67223da7U);
unsigned count{}, stride{};
const auto source = context.array(source_items, 1024, count, stride);
const auto row_type = md::read<std::uintptr_t>(md::read<std::uintptr_t>(source_items.type) + 0x30);
md::require(stride == 1200 && md::read<std::uint32_t>(md::read<std::uintptr_t>(row_type)) == 0xdeb20e0fU,
"Music tile row schema differs.");
std::vector<std::byte> filtered;
unsigned mods{};
for (unsigned i = 0; i < count; ++i) {
const auto at = reinterpret_cast<std::uintptr_t>(source.data() + std::size_t{i} * stride);
const auto tile = md::read<md::Ref>(music_inline_path(context, at, row_type,
{0x214d4984U, 0x0fb0d794U, 0xa704272aU, 0xc52416efU, 0x0abf7c31U}));
const auto tile_handle = tile.handle & ~std::uint64_t{1};
if (!tile_handle) continue;
const md::Value tile_model{tile_handle, context.type_of(tile_handle)};
const auto playlist_handle = md::read<std::uint64_t>(music_inline_path(context,
context.address(tile_model), tile_model.type, {0xeb4f9b4cU})) & ~std::uint64_t{1};
if (!playlist_handle) continue;
const md::Value playlist{playlist_handle, context.type_of(playlist_handle)};
const auto id = context.text(context.field(playlist, 0xa5b84b8aU), 2048);
if (!id.starts_with("mod:")) continue;
filtered.insert(filtered.end(), source.begin() + std::size_t{i} * stride,
source.begin() + std::size_t{i + 1} * stride);
++mods;
}
const auto destination = context.field(mods_shelf.tiles, 0x67223da7U);
unsigned previous{}, previous_stride{};
context.array(destination, 1024, previous, previous_stride);
md::require(previous_stride == stride, "Mods tile row schema differs.");
if (previous == mods && mods_shelf.published_rows == filtered) return;
context.array(destination, filtered, mods);
mods_shelf.published_rows = std::move(filtered);
dingosdk::logging::event(dingosdk::logging::Channel::music,
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;
mods_shelf.next_poll = GetTickCount64() + 250;
std::uintptr_t provider{}, manager{};
if (!read(base + addr::local_music::ui_manager, provider) || !provider ||
!read(provider + 0x80, manager) || manager != mods_shelf.manager) return;
try {
game::ModelWriteLock lock(mods_shelf.manager);
music_sync_mods(md::Context{base, mods_shelf.manager});
} catch (const std::exception& error) {
static std::string last_error;
if (last_error != error.what()) {
last_error = error.what();
dingosdk::logging::event(dingosdk::logging::Channel::music,
dingosdk::Json{{"event", "music_mods_shelf_waiting"}, {"reason", last_error}}.dump().c_str());
}
}
}
// Publish the independent Mods shelf before native widgets bind.
std::string music_shelf_append_one(std::uintptr_t base, std::uintptr_t model, std::uint64_t exact_list = 0) {
namespace md = dingosdk::multiplayer::menu_data;
game::ModelWriteLock lock(model);
md::Context shelf{base, model};
std::string report = "shelf_append";
unsigned candidates = 0;
const auto roots = exact_list
? std::vector<md::Root>{{md::Value{exact_list, shelf.type_of(exact_list)}, 0}}
: shelf.roots({0x48455d84U});
for (const auto& root : roots) {
try {
const auto items = shelf.field(root.model, 0x67223da7U);
unsigned count = 0, stride = 0;
const auto bytes = shelf.array(items, 64, count, stride);
if (stride != 16 || count < 1 || count > 8) continue;
const auto first = shelf.element(items, 0);
const auto element_schema = first.type ? md::read<std::uint32_t>(md::read<std::uintptr_t>(first.type)) : 0U;
if (element_schema != 0x62088281U) continue;
std::uint64_t target = 0;
std::memcpy(&target, bytes.data() + 8, sizeof(target));
std::uint32_t target_schema = 0;
if (target) { try { const auto type = shelf.type_of(target & ~std::uint64_t{1}); target_schema = type ? md::read<std::uint32_t>(md::read<std::uintptr_t>(type)) : 0U; } catch (const std::exception&) {} }
++candidates;
if (target_schema != 0x0e3be640U) continue;
report += " list=0x" + music_diag_hex(root.model.handle, 16) + " count=" + std::to_string(count);
if (count >= 4) { report += " already"; break; }
if (count < 2) { report += " too_few"; break; }
std::uint64_t record_ref = 0, source_vm = 0;
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});
auto new_vm = shelf.create(md::Schema{0x0e3be640U, 192}, (GetTickCount64() << 12) | 0x4dU);
md::Value new_tiles{};
try {
const md::Value source_anchor{source_vm, source_type};
shelf.copy(new_vm, shelf.address(source_anchor));
const auto tile_ref = music_tile_ref(shelf, source_anchor);
auto tile_handle = tile_ref.handle & ~std::uint64_t{1};
const auto tile_type = shelf.type(md::Schema{0x74b1ea1dU, 392});
std::uintptr_t tile_data{};
if (tile_handle && shelf.type_of(tile_handle) == tile_type)
tile_data = shelf.address(md::Value{tile_handle, tile_type});
else if (tile_ref.record && md::read<std::uintptr_t>(tile_ref.record + 0x18) == tile_type)
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);
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);
throw;
}
std::vector<std::byte> element(stride);
std::memcpy(element.data(), &record_ref, 8);
std::memcpy(element.data() + 8, &new_vm.handle, 8);
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&) {}
}
try { music_sync_mods(shelf); } catch (const std::exception&) {}
unsigned after = 0, after_stride = 0;
shelf.array(items, 64, after, after_stride);
report += " new=0x" + music_diag_hex(new_vm.handle, 16) + " appended=" + std::to_string(count + 1) +
" after=" + std::to_string(after);
break;
} catch (const std::exception& error) { report += " error=" + std::string(error.what()); continue; }
}
report += " candidates=" + std::to_string(candidates);
return report;
}
}
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) {
const auto handle = music_model_construct_original(manager, mode, id, type, flag, record);
PreserveError preserve;
try {
if (!handle || !type || md::read<std::uint32_t>(md::read<std::uintptr_t>(type)) != 0x48455d84U)
return handle;
game::ModelWriteLock lock(manager);
md::Context context{local_runtime().base, manager};
const auto items = context.field(md::Value{handle, type}, 0x67223da7U);
unsigned count{}, stride{};
const auto bytes = context.array(items, 1024, count, stride);
if (count != 3 || stride != 16) return handle;
constexpr const char* records[] = {
"MusicPlaylistManager_Base_ContentResources/Featured_AnchoredContent_VM",
"MusicPlaylistManager_Base_ContentResources/Liked_AnchoredContent_VM",
"MusicPlaylistManager_Base_ContentResources/NewlyDiscovered_AnchoredContent_VM"};
for (unsigned item = 0; item < 3; ++item) {
std::uintptr_t item_record{};
std::memcpy(&item_record, bytes.data() + item * stride, sizeof(item_record));
// Named record header +0x28 is also used by the native UI dumper.
if (!item_record || md::string(md::read<std::uintptr_t>(item_record + 0x28), 256) != records[item])
return handle;
}
const auto report = music_shelf_append_one(local_runtime().base, manager, handle);
dingosdk::logging::event(dingosdk::logging::Channel::music,
dingosdk::Json{{"event", "music_shelf_append"}, {"report", report}}.dump().c_str());
} catch (...) {} // A shelf failure must not interrupt the native constructor.
return handle;
}
void update_music_shelf() {
auto& runtime = local_runtime();
if (!runtime.active.load(std::memory_order_acquire)) return;
PreserveError preserve;
music_poll_mods(runtime.base);
}
} // namespace dingosdk::profile_runtime
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include "Engine/Game/Build/fingerprint.h"
#include <cstdint>
namespace dingosdk::profile_runtime {
inline constexpr game::build::Fingerprint music_model_construct_contract{0x1912670,
{0x4c,0x89,0x44,0x24,0x18,0x55,0x56,0x41,0x55,0x41,0x56,0x48,0x8d,0x6c,0x24,0xd1,
0x48,0x81,0xec,0xf8,0x00,0x00,0x00,0x4d,0x8b,0xf1,0x44,0x0f,0xb6,0xea,0x48,0x8b}};
using MusicModelConstruct = std::uint64_t (*)(std::uintptr_t, std::uint8_t, std::uint64_t,
std::uintptr_t, bool, std::uintptr_t);
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();
} // namespace dingosdk::profile_runtime
-207
View File
@@ -10,9 +10,6 @@
#include "Engine/Game/Build/20260929/local_music.h"
#include "Engine/Game/World/location_travel.h"
#include "Extension/Profile/runtime_internal.h"
#include "Extension/UI/NativeMenu/native_menu_data.h"
#include <algorithm>
#include <utility>
namespace dingosdk::profile_runtime {
// Runtime-only music UI hydration: read_music_catalog copies actual registered
@@ -32,212 +29,9 @@ void music_release_weak(std::uintptr_t owner) noexcept {
MusicUiRuntime& music_ui_runtime() { static auto* r = new MusicUiRuntime; return *r; }
namespace {
// Format model handles for shelf publication logs.
std::string music_diag_hex(std::uint64_t value, int width) {
static constexpr char digits[] = "0123456789abcdef";
std::string out(static_cast<std::size_t>(width), '0');
for (int i = width - 1; i >= 0; --i, value >>= 4) out[static_cast<std::size_t>(i)] = digits[value & 15];
return out;
}
namespace md = dingosdk::multiplayer::menu_data;
struct ModsShelf {
std::uintptr_t manager{};
md::Value anchor{}, tiles{}, featured_anchor{};
ULONGLONG next_poll{};
std::vector<std::byte> published_rows;
};
ModsShelf 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) {
for (auto hash : hashes) {
const auto field = context.member(type, hash);
data += field.offset;
type = field.type;
}
return data;
}
md::Ref music_tile_ref(const md::Context& context, md::Value anchor) {
return md::read<md::Ref>(music_inline_path(context, context.address(anchor), anchor.type,
{0x214d4984U, 0x25e4d6c8U}));
}
void music_sync_mods(const md::Context& context) {
if (mods_shelf.manager != context.manager || !mods_shelf.tiles.handle) return;
const auto ref = music_tile_ref(context, mods_shelf.featured_anchor);
const auto handle = ref.handle & ~std::uint64_t{1};
if (!handle) return;
const md::Value featured{handle, context.type_of(handle)};
md::require(featured.type == mods_shelf.tiles.type, "Featured tile schema differs.");
const auto source_items = context.field(featured, 0x67223da7U);
unsigned count{}, stride{};
const auto source = context.array(source_items, 1024, count, stride);
const auto row_type = md::read<std::uintptr_t>(md::read<std::uintptr_t>(source_items.type) + 0x30);
md::require(stride == 1200 && md::read<std::uint32_t>(md::read<std::uintptr_t>(row_type)) == 0xdeb20e0fU,
"Music tile row schema differs.");
std::vector<std::byte> filtered;
unsigned mods{};
for (unsigned i = 0; i < count; ++i) {
const auto at = reinterpret_cast<std::uintptr_t>(source.data() + std::size_t{i} * stride);
const auto tile = md::read<md::Ref>(music_inline_path(context, at, row_type,
{0x214d4984U, 0x0fb0d794U, 0xa704272aU, 0xc52416efU, 0x0abf7c31U}));
const auto tile_handle = tile.handle & ~std::uint64_t{1};
if (!tile_handle) continue;
const md::Value tile_model{tile_handle, context.type_of(tile_handle)};
const auto playlist_handle = md::read<std::uint64_t>(music_inline_path(context,
context.address(tile_model), tile_model.type, {0xeb4f9b4cU})) & ~std::uint64_t{1};
if (!playlist_handle) continue;
const md::Value playlist{playlist_handle, context.type_of(playlist_handle)};
const auto id = context.text(context.field(playlist, 0xa5b84b8aU), 2048);
if (!id.starts_with("mod:")) continue;
filtered.insert(filtered.end(), source.begin() + std::size_t{i} * stride,
source.begin() + std::size_t{i + 1} * stride);
++mods;
}
const auto destination = context.field(mods_shelf.tiles, 0x67223da7U);
unsigned previous{}, previous_stride{};
context.array(destination, 1024, previous, previous_stride);
md::require(previous_stride == stride, "Mods tile row schema differs.");
if (previous == mods && mods_shelf.published_rows == filtered) return;
context.array(destination, filtered, mods);
mods_shelf.published_rows = std::move(filtered);
dingosdk::logging::event(dingosdk::logging::Channel::music,
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;
mods_shelf.next_poll = GetTickCount64() + 250;
std::uintptr_t provider{}, manager{};
if (!read(base + addr::local_music::ui_manager, provider) || !provider ||
!read(provider + 0x80, manager) || manager != mods_shelf.manager) return;
try {
game::ModelWriteLock lock(mods_shelf.manager);
music_sync_mods(md::Context{base, mods_shelf.manager});
} catch (const std::exception& error) {
static std::string last_error;
if (last_error != error.what()) {
last_error = error.what();
dingosdk::logging::event(dingosdk::logging::Channel::music,
dingosdk::Json{{"event", "music_mods_shelf_waiting"}, {"reason", last_error}}.dump().c_str());
}
}
}
// Publish the independent Mods shelf before native widgets bind.
std::string music_shelf_append_one(std::uintptr_t base, std::uintptr_t model, std::uint64_t exact_list = 0) {
namespace md = dingosdk::multiplayer::menu_data;
game::ModelWriteLock lock(model);
md::Context shelf{base, model};
std::string report = "shelf_append";
unsigned candidates = 0;
const auto roots = exact_list
? std::vector<md::Root>{{md::Value{exact_list, shelf.type_of(exact_list)}, 0}}
: shelf.roots({0x48455d84U});
for (const auto& root : roots) {
try {
const auto items = shelf.field(root.model, 0x67223da7U);
unsigned count = 0, stride = 0;
const auto bytes = shelf.array(items, 64, count, stride);
if (stride != 16 || count < 1 || count > 8) continue;
const auto first = shelf.element(items, 0);
const auto element_schema = first.type ? md::read<std::uint32_t>(md::read<std::uintptr_t>(first.type)) : 0U;
if (element_schema != 0x62088281U) continue;
std::uint64_t target = 0;
std::memcpy(&target, bytes.data() + 8, sizeof(target));
std::uint32_t target_schema = 0;
if (target) { try { const auto type = shelf.type_of(target & ~std::uint64_t{1}); target_schema = type ? md::read<std::uint32_t>(md::read<std::uintptr_t>(type)) : 0U; } catch (const std::exception&) {} }
++candidates;
if (target_schema != 0x0e3be640U) continue;
report += " list=0x" + music_diag_hex(root.model.handle, 16) + " count=" + std::to_string(count);
if (count >= 4) { report += " already"; break; }
if (count < 2) { report += " too_few"; break; }
std::uint64_t record_ref = 0, source_vm = 0;
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});
auto new_vm = shelf.create(md::Schema{0x0e3be640U, 192}, (GetTickCount64() << 12) | 0x4dU);
md::Value new_tiles{};
try {
const md::Value source_anchor{source_vm, source_type};
shelf.copy(new_vm, shelf.address(source_anchor));
const auto tile_ref = music_tile_ref(shelf, source_anchor);
auto tile_handle = tile_ref.handle & ~std::uint64_t{1};
const auto tile_type = shelf.type(md::Schema{0x74b1ea1dU, 392});
std::uintptr_t tile_data{};
if (tile_handle && shelf.type_of(tile_handle) == tile_type)
tile_data = shelf.address(md::Value{tile_handle, tile_type});
else if (tile_ref.record && md::read<std::uintptr_t>(tile_ref.record + 0x18) == tile_type)
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);
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);
throw;
}
std::vector<std::byte> element(stride);
std::memcpy(element.data(), &record_ref, 8);
std::memcpy(element.data() + 8, &new_vm.handle, 8);
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&) {}
}
try { music_sync_mods(shelf); } catch (const std::exception&) {}
unsigned after = 0, after_stride = 0;
shelf.array(items, 64, after, after_stride);
report += " new=0x" + music_diag_hex(new_vm.handle, 16) + " appended=" + std::to_string(count + 1) +
" after=" + std::to_string(after);
break;
} catch (const std::exception& error) { report += " error=" + std::string(error.what()); continue; }
}
report += " candidates=" + std::to_string(candidates);
return report;
}
}
// CONFIRMED: the authored loader calls the shared constructor at RVA 0x1912670.
// The return path exposes the shelf list before its native widgets bind.
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) {
const auto handle = music_model_construct_original(manager, mode, id, type, flag, record);
PreserveError preserve;
try {
if (!handle || !type || md::read<std::uint32_t>(md::read<std::uintptr_t>(type)) != 0x48455d84U)
return handle;
game::ModelWriteLock lock(manager);
md::Context context{local_runtime().base, manager};
const auto items = context.field(md::Value{handle, type}, 0x67223da7U);
unsigned count{}, stride{};
const auto bytes = context.array(items, 1024, count, stride);
if (count != 3 || stride != 16) return handle;
constexpr const char* records[] = {
"MusicPlaylistManager_Base_ContentResources/Featured_AnchoredContent_VM",
"MusicPlaylistManager_Base_ContentResources/Liked_AnchoredContent_VM",
"MusicPlaylistManager_Base_ContentResources/NewlyDiscovered_AnchoredContent_VM"};
for (unsigned item = 0; item < 3; ++item) {
std::uintptr_t item_record{};
std::memcpy(&item_record, bytes.data() + item * stride, sizeof(item_record));
// Named record header +0x28 is also used by the native UI dumper.
if (!item_record || md::string(md::read<std::uintptr_t>(item_record + 0x28), 256) != records[item])
return handle;
}
const auto report = music_shelf_append_one(local_runtime().base, manager, handle);
dingosdk::logging::event(dingosdk::logging::Channel::music,
dingosdk::Json{{"event", "music_shelf_append"}, {"report", report}}.dump().c_str());
} catch (...) {} // A shelf failure must not interrupt the native constructor.
return handle;
}
bool initialize_music_functions(std::uintptr_t base) {
namespace music = addr::local_music;
for (const auto& fp : music::music_ui_contracts) {
@@ -421,7 +215,6 @@ void update_music_catalog() {
auto& s = local_runtime(); auto& ui = music_ui_runtime();
if (!s.active.load(std::memory_order_acquire) || ui.updating || cosmetic_runtime().update_thread != GetCurrentThreadId()) return;
PreserveError preserve; std::lock_guard lock(s.native_mutex);
music_poll_mods(s.base);
if (!ui.pending) return;
if (ui.pending->thread != GetCurrentThreadId() || !music_ui_current(*ui.pending)) { ui.pending.reset(); return; }
const auto now = GetTickCount64(); if (now < ui.pending->next_poll) return;
-9
View File
@@ -83,13 +83,4 @@ void music_ui_initialize_hook(std::uint64_t all, std::uint64_t hidden, std::uint
void update_music_catalog();
// Shared authored-model constructor, ABI verified at Skate.exe +0x1912670 and +0x191868f.
inline constexpr game::build::Fingerprint music_model_construct_contract{0x1912670,
{0x4c,0x89,0x44,0x24,0x18,0x55,0x56,0x41,0x55,0x41,0x56,0x48,0x8d,0x6c,0x24,0xd1,
0x48,0x81,0xec,0xf8,0x00,0x00,0x00,0x4d,0x8b,0xf1,0x44,0x0f,0xb6,0xea,0x48,0x8b}};
using MusicModelConstruct = std::uint64_t (*)(std::uintptr_t, std::uint8_t, std::uint64_t,
std::uintptr_t, bool, std::uintptr_t);
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);
}
@@ -5,6 +5,7 @@
#include "Extension/Customization/local_customization_runtime.h"
#include "Extension/Customization/local_player_card_runtime.h"
#include "Extension/Music/local_music_ui.h"
#include "Extension/Music/local_music_shelf.h"
#include "Extension/News/local_news_runtime.h"
#include "Extension/Objects/local_buildkit_labels.h"
#include "Extension/Objects/local_buildkit_limits.h"
@@ -325,6 +326,7 @@ void update_local_customization() noexcept {
try { update_object_categories(); }
catch (...) { dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_object_categories_failed\",\"operation\":\"update\"}"); }
update_music_catalog();
update_music_shelf();
try {
if (cosmetic_runtime().update_thread == GetCurrentThreadId() && !cosmetic_runtime().items.empty())
update_player_card();
+1
View File
@@ -142,6 +142,7 @@ add_library(dingosdk_runtime SHARED
Extension/Objects/local_object_runtime.cpp
Extension/Music/local_music_assets.cpp
Extension/Music/local_music_ui.cpp
Extension/Music/local_music_shelf.cpp
Extension/Objects/local_buildkit_labels.cpp
Extension/Objects/local_buildkit_limits.cpp
Extension/Progression/local_rip_score_runtime.cpp
+3 -2
View File
@@ -5,7 +5,8 @@ existing Music Playlist Manager. It is a worked example of changing native UI
through its data models, while reusing the game's layout, playlist tiles and
selection behavior.
The implementation lives in [local_music_ui.cpp](../Extension/Music/local_music_ui.cpp).
The shelf hook and model management live in [local_music_shelf.cpp](../Extension/Music/local_music_shelf.cpp).
The playlist and song UI publication remains in [local_music_ui.cpp](../Extension/Music/local_music_ui.cpp).
These are C++ runtime changes. A music mod can supply playlists through the
existing music-mod pipeline; adding a new native shelf requires runtime code
such as this hook. There is no general declarative UI-mod API established here.
@@ -171,7 +172,7 @@ The shelf hook calls the original constructor first. On its return, it recognize
the exact music list and inserts the new entry before the native caller continues
to bind widgets. It returns the original handle unchanged. Installation checks
an exact 32-byte executable fingerprint; a mismatch leaves this hook uninstalled.
See [local_music_ui.h](../Extension/Music/local_music_ui.h) for the contract and
See [local_music_shelf.h](../Extension/Music/local_music_shelf.h) for the contract and
[local_profile_runtime.cpp](../Extension/Profile/local_profile_runtime.cpp) for
installation. That profile-runtime change only installs the music hook; it does
not change cosmetic ownership or progression.