mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-19 22:25:22 +02:00
Music: isolate native shelf implementation
This commit is contained in:
1 parent
916e281e69
commit
96cd684e20
7 files changed
+250
-218
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user