mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-23 18:45:57 +02:00
fix(music): play favorites sequentially in visible order
This commit is contained in:
1 parent
a1621ee19e
commit
63b747fd4a
6 files changed
+346
-21
No files matched your search
@@ -54,6 +54,10 @@ inline constexpr Fingerprint favorite_apply_contract{0x6f51e0, {0x48,0x89,0x5c,0
|
||||
inline constexpr Fingerprint playback_select_next_contract{0x14f8ed0, {0x48,0x89,0x54,0x24,0x10,0x55,0x53,0x56,0x57,0x41,0x54,0x41,0x55,0x41,0x56,0x41,0x57,0x48,0x8d,0x6c,0x24,0xc8,0x48,0x81,0xec,0x38,0x01,0x00,0x00,0x4c,0x8b,0xe1}};
|
||||
// Native playlist playback-mode setter (PlaylistPlaybackMode, not MusicPlaylistType).
|
||||
inline constexpr Fingerprint playback_mode_setter_contract{0x443aa0, {0x44,0x8b,0x91,0xa8,0x00,0x00,0x00,0x4c,0x8b,0x99,0xa0,0x00,0x00,0x00,0x44,0x8b,0xca,0x8b,0xc2,0x33,0xd2,0x49,0xf7,0xf2,0x8b,0xc2,0x49,0x8b,0x04,0xc3,0x48,0x85}};
|
||||
// Native channel-to-playlist binding entrypoint, used by the generated UI/script thunk.
|
||||
inline constexpr Fingerprint playback_channel_playlist_bind_contract{0x8ca6a0, {0x48,0x89,0x5c,0x24,0x08,0x57,0x48,0x83,0xec,0x20,0x48,0x8b,0xda,0x48,0x8b,0xf9,0xe8,0x2b,0xad,0xb6,0xff,0x48,0x85,0xc0,0x74,0x0e,0x4c,0x8b,0xc3,0x48,0x8b,0xd7}};
|
||||
// Candidate-admission callback called by the shared selector; arg4 is its actual channel-map key.
|
||||
inline constexpr Fingerprint playback_candidate_admission_contract{0x43c460, {0x48,0x89,0x5c,0x24,0x08,0x48,0x89,0x6c,0x24,0x10,0x56,0x57,0x41,0x54,0x41,0x56,0x41,0x57,0x48,0x83,0xec,0x30,0x44,0x8b,0x81,0xd0,0x00,0x00,0x00,0x4c,0x8b,0xfa}};
|
||||
inline constexpr std::array<Fingerprint, 7> music_ui_contracts{{
|
||||
music_ui_initialize_contract,
|
||||
{construct_playlist, {0x48,0x89,0x5c,0x24,0x08,0x57,0x48,0x83,0xec,0x20,0x48,0x8b,0xda,0x48,0x8b,0xf9,0x48,0x8d,0x15,0x21,0x95,0x32,0x04,0xe8,0x94,0x30,0x5e,0x00,0x33,0xc9,0x48,0x8d}},
|
||||
|
||||
@@ -12,6 +12,20 @@ int main() {
|
||||
check(policy::cursor_sync_sentinel == 0x7fffffffu,
|
||||
"native cursor synchronization sentinel is INT_MAX, distinct from queue -1");
|
||||
|
||||
check(policy::resolve_playlist_binding_id(0, 1, true) == 1,
|
||||
"a zero-ID dynamic Favorites binding resolves to its live native node");
|
||||
check(policy::resolve_playlist_binding_id(0, 1, false) == 0,
|
||||
"a zero-ID binding stays untouched when the Favorites node is unavailable");
|
||||
check(policy::resolve_playlist_binding_id(18, 1, true) == 18,
|
||||
"ordinary playlist bindings are not redirected to Favorites");
|
||||
|
||||
check(policy::ordered_member_index(0, 4, true) == 3 &&
|
||||
policy::ordered_member_index(3, 4, true) == 0,
|
||||
"Favorites sequential order follows the visible top-to-bottom rows");
|
||||
check(policy::ordered_member_index(0, 4, false) == 0 &&
|
||||
policy::ordered_member_index(4, 4, true) == 4,
|
||||
"native static order stays forward and invalid positions are rejected");
|
||||
|
||||
// The queue's global candidate asset pool can contain hundreds of songs while
|
||||
// the selected native playlist contains only its authored subset.
|
||||
std::array<std::uint8_t, 32> two_track_members{};
|
||||
@@ -67,6 +81,21 @@ int main() {
|
||||
policy::cursor_sync_sentinel, false, false, false, 0, 6);
|
||||
check(plan.cursor == 0, "an unresolvable current track safely restarts at the first entry");
|
||||
|
||||
plan = policy::plan_cursor(false, policy::native_default_mode,
|
||||
policy::cursor_sync_sentinel, true, false, false, 0, 3);
|
||||
check(plan.set_mode && plan.set_cursor && plan.cursor == 0,
|
||||
"a dynamic Favorites playlist can start sequentially without a static asset GUID");
|
||||
|
||||
plan = policy::plan_cursor(false, policy::native_sequential_mode, 3,
|
||||
false, false, false, 0, 3);
|
||||
check(plan.set_cursor && plan.cursor == 0,
|
||||
"a favorite removal that leaves the cursor at member count restarts on a valid member");
|
||||
|
||||
plan = policy::plan_cursor(false, policy::native_sequential_mode, 3,
|
||||
false, false, true, 1, 3);
|
||||
check(plan.set_cursor && plan.cursor == 2,
|
||||
"a stale dynamic cursor resumes after the active member when its GUID is available");
|
||||
|
||||
plan = policy::plan_cursor(false, policy::native_sequential_mode,
|
||||
policy::cursor_sync_sentinel, false, false, false, 0, 6);
|
||||
check(!plan.set_mode && plan.set_cursor && plan.cursor == 0,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "local_music_playback_policy.h"
|
||||
#include "Engine/Core/Hooks/hooks.h"
|
||||
#include "Engine/Core/Log/logging.h"
|
||||
#include "Engine/Core/Platform/memory.h"
|
||||
#include "Engine/Game/Build/20260929/local_music.h"
|
||||
#include "Engine/Game/Build/supported_build.h"
|
||||
@@ -9,6 +10,7 @@
|
||||
#include <Windows.h>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
@@ -17,12 +19,18 @@ namespace {
|
||||
using namespace dingosdk::game::build::v20260929;
|
||||
using local_music::playback_mode_setter_contract;
|
||||
using local_music::playback_select_next_contract;
|
||||
using local_music::playback_channel_playlist_bind_contract;
|
||||
using local_music::playback_candidate_admission_contract;
|
||||
using music_playback::CursorPlan;
|
||||
|
||||
std::atomic<std::uintptr_t> image_base{};
|
||||
std::atomic<bool> shuffle_enabled{};
|
||||
std::atomic<bool> playback_available{};
|
||||
MusicSelectNext select_next{};
|
||||
MusicChannelPlaylistBind bind_channel_playlist{};
|
||||
std::array<std::atomic<std::uint64_t>, 256> last_logged_channel_bindings{};
|
||||
MusicCandidateAdmission candidate_admission{};
|
||||
std::array<std::atomic<std::uint8_t>, 256> logged_candidate_admission_results{};
|
||||
using SetPlaybackMode = void (__fastcall *)(std::uintptr_t manager, std::uint32_t playlist_id,
|
||||
std::uint32_t mode);
|
||||
SetPlaybackMode set_playback_mode{};
|
||||
@@ -31,9 +39,16 @@ struct LastBinding {
|
||||
std::uintptr_t queue{};
|
||||
std::uintptr_t node{};
|
||||
std::uint32_t playlist_id{std::numeric_limits<std::uint32_t>::max()};
|
||||
std::uint32_t member_count{};
|
||||
};
|
||||
std::array<LastBinding, 256> last_binding_by_channel{}; // Protected by the binding-cache lock.
|
||||
SRWLOCK binding_cache_lock = SRWLOCK_INIT;
|
||||
std::atomic<std::uint32_t> logged_policy_events{};
|
||||
|
||||
void log_policy_event_once(std::uint32_t bit, const char* json) noexcept {
|
||||
if (!(logged_policy_events.fetch_or(bit, std::memory_order_relaxed) & bit))
|
||||
dingosdk::logging::event(dingosdk::logging::Channel::music, json);
|
||||
}
|
||||
|
||||
bool read_at(std::uintptr_t address, auto& value) noexcept {
|
||||
return dingosdk::memory::peek(address, value);
|
||||
@@ -59,6 +74,20 @@ bool valid_range(std::uintptr_t value, std::size_t size) noexcept {
|
||||
return value >= 0x10000 && value <= dingosdk::memory::highest_user_address - size;
|
||||
}
|
||||
|
||||
bool native_queue_channel(std::uintptr_t base, std::uintptr_t queue,
|
||||
std::uint32_t& channel) noexcept {
|
||||
std::uintptr_t vtable{}, getter{};
|
||||
if (!read_at(queue, vtable) || !valid_range(vtable, 0x20) ||
|
||||
!read_at(vtable + 0x18, getter) || getter < base ||
|
||||
getter - base >= supported_build::game_image_size) return false;
|
||||
|
||||
// The engine's candidate-admission callback uses the return from this same
|
||||
// virtual slot as its channel-map key (sub_1414F8ED0, call through vtable+0x18).
|
||||
using GetChannel = std::uint32_t (__fastcall *)(std::uintptr_t queue);
|
||||
channel = reinterpret_cast<GetChannel>(getter)(queue);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool find_channel_node(std::uintptr_t manager, std::uint32_t channel,
|
||||
std::uint32_t& playlist_id) noexcept {
|
||||
std::uintptr_t buckets{};
|
||||
@@ -114,6 +143,40 @@ bool member_count(std::uintptr_t node, std::uintptr_t& begin, std::uint32_t& cou
|
||||
return count > 0 && valid_range(begin, static_cast<std::size_t>(count) * 16);
|
||||
}
|
||||
|
||||
bool set_favorites_order(std::uintptr_t node, std::uint32_t member_total) noexcept {
|
||||
std::uintptr_t order_begin{}, order_end{}, order_capacity{};
|
||||
if (!member_total || member_total > 8192 ||
|
||||
!read_at(node + 0x38, order_begin) || !read_at(node + 0x40, order_end) ||
|
||||
!read_at(node + 0x48, order_capacity) || !order_begin || order_end < order_begin ||
|
||||
order_capacity < order_end || (order_end - order_begin) % sizeof(std::uint32_t) ||
|
||||
(order_end - order_begin) / sizeof(std::uint32_t) != member_total ||
|
||||
!valid_range(order_begin, static_cast<std::size_t>(member_total) * sizeof(std::uint32_t)))
|
||||
return false;
|
||||
|
||||
auto* order = reinterpret_cast<volatile std::uint32_t*>(order_begin);
|
||||
for (std::uint32_t position = 0; position < member_total; ++position)
|
||||
order[position] = music_playback::ordered_member_index(position, member_total, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool favorites_order_shape_matches(std::uintptr_t node, std::uint32_t member_total) noexcept {
|
||||
std::uintptr_t order_begin{}, order_end{}, order_capacity{};
|
||||
return member_total && member_total <= 8192 &&
|
||||
read_at(node + 0x38, order_begin) && read_at(node + 0x40, order_end) &&
|
||||
read_at(node + 0x48, order_capacity) && order_begin && order_end >= order_begin &&
|
||||
order_capacity >= order_end && (order_end - order_begin) % sizeof(std::uint32_t) == 0 &&
|
||||
(order_end - order_begin) / sizeof(std::uint32_t) == member_total &&
|
||||
valid_range(order_begin, static_cast<std::size_t>(member_total) * sizeof(std::uint32_t));
|
||||
}
|
||||
|
||||
bool favorites_node_ready(std::uintptr_t manager, std::uint32_t favorites_playlist) noexcept {
|
||||
if (!favorites_playlist) return false;
|
||||
const auto node = find_playlist_node(manager, favorites_playlist);
|
||||
std::uintptr_t members{};
|
||||
std::uint32_t count{};
|
||||
return node && member_count(node, members, count);
|
||||
}
|
||||
|
||||
bool current_guid(std::uintptr_t queue, std::uintptr_t playlist_asset,
|
||||
bool& found, std::array<unsigned char, 16>& guid) noexcept {
|
||||
found = false;
|
||||
@@ -172,55 +235,156 @@ bool pending_song_request(std::uintptr_t queue, bool& pending) noexcept {
|
||||
|
||||
void apply_policy(std::uintptr_t queue, std::uintptr_t playlist_asset) noexcept {
|
||||
const auto base = image_base.load(std::memory_order_acquire);
|
||||
if (!base || !queue || !playlist_asset) return;
|
||||
if (!base || !queue || !playlist_asset) {
|
||||
log_policy_event_once(1u << 0, "{\"event\":\"music_playback_policy_skipped\",\"reason\":\"queue_context_unavailable\"}");
|
||||
return;
|
||||
}
|
||||
std::uint32_t queue_type{}, channel{}, channel_playlist{};
|
||||
if (!read_at(playlist_asset + 0x70, queue_type) || queue_type != 4 ||
|
||||
!read_at(queue + 0x60, channel) || channel >= last_binding_by_channel.size()) return;
|
||||
if (!read_at(playlist_asset + 0x70, queue_type) || queue_type != 4) {
|
||||
log_policy_event_once(1u << 1, "{\"event\":\"music_playback_policy_skipped\",\"reason\":\"unsupported_playlist_type\"}");
|
||||
return;
|
||||
}
|
||||
if (!native_queue_channel(base, queue, channel) || channel >= last_binding_by_channel.size()) {
|
||||
log_policy_event_once(1u << 2, "{\"event\":\"music_playback_policy_skipped\",\"reason\":\"queue_channel_unavailable\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
std::uintptr_t manager{};
|
||||
if (!read_at(base + local_music::asset_manager, manager) || !manager ||
|
||||
!valid_range(manager, 0xd8)) {
|
||||
log_policy_event_once(1u << 3, "{\"event\":\"music_playback_policy_skipped\",\"reason\":\"asset_manager_unavailable\"}");
|
||||
return;
|
||||
}
|
||||
{
|
||||
CriticalSectionGuard manager_lock(manager + 0x20);
|
||||
std::uintptr_t current_manager{};
|
||||
if (!read_at(base + local_music::asset_manager, current_manager) || current_manager != manager ||
|
||||
!find_channel_node(manager, channel, channel_playlist)) {
|
||||
log_policy_event_once(1u << 4, "{\"event\":\"music_playback_policy_skipped\",\"reason\":\"channel_playlist_unavailable\"}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Favorites is a dynamic playlist. Its native node owns the member GUIDs,
|
||||
// while its playlist asset has no compiled entries graph at +0x60.
|
||||
std::uintptr_t ui_manager{};
|
||||
std::uint32_t favorites_playlist{};
|
||||
const bool has_favorites_playlist =
|
||||
read_at(base + local_music::ui_manager, ui_manager) && valid_range(ui_manager, 0x1fc) &&
|
||||
read_at(ui_manager + 0x1f8, favorites_playlist) && favorites_playlist != 0;
|
||||
const bool dynamic_favorites = has_favorites_playlist && channel_playlist == favorites_playlist;
|
||||
if (dynamic_favorites) {
|
||||
char message[192]{};
|
||||
std::snprintf(message, sizeof(message),
|
||||
"{\"event\":\"music_playback_dynamic_favorites\",\"source\":\"native_playlist_members\",\"channel\":%u,\"playlist\":%u}",
|
||||
channel, channel_playlist);
|
||||
log_policy_event_once(1u << 5, message);
|
||||
}
|
||||
|
||||
// Snapshot the queue-local state under its native lock, then release it before
|
||||
// taking the music manager lock. The selector can re-enter this queue lock.
|
||||
bool pending{}, has_current_guid{};
|
||||
std::array<unsigned char, 16> playing_guid{};
|
||||
std::uint32_t asset_songs{};
|
||||
{
|
||||
CriticalSectionGuard queue_lock(queue + 0x150);
|
||||
if (!pending_song_request(queue, pending) || !song_count(playlist_asset, asset_songs) ||
|
||||
!current_guid(queue, playlist_asset, has_current_guid, playing_guid)) return;
|
||||
std::uint32_t current_channel{};
|
||||
if (!native_queue_channel(base, queue, current_channel) || current_channel != channel ||
|
||||
!pending_song_request(queue, pending)) {
|
||||
log_policy_event_once(1u << 6, "{\"event\":\"music_playback_policy_skipped\",\"reason\":\"queue_snapshot_unavailable\"}");
|
||||
return;
|
||||
}
|
||||
if (!dynamic_favorites) {
|
||||
std::uint32_t asset_songs{};
|
||||
if (!song_count(playlist_asset, asset_songs)) {
|
||||
log_policy_event_once(1u << 7, "{\"event\":\"music_playback_policy_skipped\",\"reason\":\"playlist_asset_entries_unavailable\"}");
|
||||
return;
|
||||
}
|
||||
if (!current_guid(queue, playlist_asset, has_current_guid, playing_guid)) {
|
||||
log_policy_event_once(1u << 8, "{\"event\":\"music_playback_policy_skipped\",\"reason\":\"current_track_unavailable\"}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::uintptr_t manager{};
|
||||
if (!read_at(base + local_music::asset_manager, manager) || !manager ||
|
||||
!valid_range(manager, 0xd8)) return;
|
||||
CriticalSectionGuard manager_lock(manager + 0x20);
|
||||
std::uintptr_t current_manager{};
|
||||
std::uint32_t current_channel_playlist{};
|
||||
if (!read_at(base + local_music::asset_manager, current_manager) || current_manager != manager ||
|
||||
!find_channel_node(manager, channel, channel_playlist)) return;
|
||||
!find_channel_node(manager, channel, current_channel_playlist) ||
|
||||
current_channel_playlist != channel_playlist) {
|
||||
log_policy_event_once(1u << 9, "{\"event\":\"music_playback_policy_skipped\",\"reason\":\"channel_playlist_changed\"}");
|
||||
return;
|
||||
}
|
||||
const auto node = find_playlist_node(manager, channel_playlist);
|
||||
if (!node) return;
|
||||
if (!node) {
|
||||
char message[256]{};
|
||||
std::snprintf(message, sizeof(message),
|
||||
"{\"event\":\"music_playback_policy_skipped\",\"reason\":\"playlist_node_unavailable\",\"channel\":%u,\"playlist\":%u,\"favorites\":%u}",
|
||||
channel, channel_playlist, favorites_playlist);
|
||||
log_policy_event_once(1u << 10, message);
|
||||
return;
|
||||
}
|
||||
BindingCacheGuard cache_lock;
|
||||
|
||||
std::uint32_t current_mode{}, current_cursor{}, member_total{};
|
||||
std::uintptr_t members{};
|
||||
if (!read_at(node + 0x0c, current_mode) || !read_at(node + 0x30, current_cursor) ||
|
||||
!member_count(node, members, member_total)) return;
|
||||
!member_count(node, members, member_total)) {
|
||||
log_policy_event_once(1u << 11, "{\"event\":\"music_playback_policy_skipped\",\"reason\":\"playlist_members_unavailable\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& previous = last_binding_by_channel[channel];
|
||||
const bool new_group = previous.manager != manager || previous.queue != queue ||
|
||||
previous.node != node || previous.playlist_id != channel_playlist;
|
||||
const auto added_favorites = dynamic_favorites && !new_group &&
|
||||
member_total > previous.member_count ? member_total - previous.member_count : 0;
|
||||
const bool favorites_members_changed = dynamic_favorites && !new_group &&
|
||||
member_total != previous.member_count;
|
||||
bool current_found{};
|
||||
std::uint32_t current_position{};
|
||||
if (!member_index(members, member_total, playing_guid, has_current_guid, current_found, current_position)) return;
|
||||
if (!member_index(members, member_total, playing_guid, has_current_guid, current_found, current_position)) {
|
||||
log_policy_event_once(1u << 12, "{\"event\":\"music_playback_policy_skipped\",\"reason\":\"member_identity_unavailable\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto plan = music_playback::plan_cursor(shuffle_enabled.load(std::memory_order_acquire),
|
||||
const auto shuffle = shuffle_enabled.load(std::memory_order_acquire);
|
||||
auto plan = music_playback::plan_cursor(shuffle,
|
||||
current_mode, current_cursor, new_group, pending, current_found, current_position, member_total);
|
||||
if (plan.set_mode) {
|
||||
if (dynamic_favorites && !shuffle && !plan.set_cursor && current_mode == music_playback::native_sequential_mode &&
|
||||
added_favorites && current_cursor != music_playback::cursor_sync_sentinel && current_cursor > 0 &&
|
||||
current_cursor < member_total) {
|
||||
// New favorites appear at the top of the visible list. The reversed index
|
||||
// vector therefore grows at its front; advance the cursor by the insertion
|
||||
// count to keep the next selection after the currently playing member.
|
||||
plan.set_cursor = true;
|
||||
plan.cursor = current_cursor + added_favorites;
|
||||
if (plan.cursor >= member_total) plan.cursor = 0;
|
||||
}
|
||||
const bool rebuild_favorites_order = dynamic_favorites && !shuffle && !new_group &&
|
||||
current_mode == music_playback::native_sequential_mode &&
|
||||
(favorites_members_changed || !favorites_order_shape_matches(node, member_total));
|
||||
if (plan.set_mode || rebuild_favorites_order) {
|
||||
// Clear the rebuild latch only as an input. The native rebuild sets it on
|
||||
// success; preserving that output is required for later rebuild semantics.
|
||||
*reinterpret_cast<volatile std::uint8_t*>(node + 8) = 0;
|
||||
set_playback_mode(manager, channel_playlist, plan.mode);
|
||||
set_playback_mode(manager, channel_playlist,
|
||||
plan.set_mode ? plan.mode : music_playback::native_sequential_mode);
|
||||
}
|
||||
if (dynamic_favorites && !shuffle) {
|
||||
if (!set_favorites_order(node, member_total)) {
|
||||
log_policy_event_once(1u << 13,
|
||||
"{\"event\":\"music_playback_favorites_order_unavailable\"}");
|
||||
} else if (plan.set_mode || rebuild_favorites_order) {
|
||||
char message[144]{};
|
||||
std::snprintf(message, sizeof(message),
|
||||
"{\"event\":\"music_playback_favorites_order\",\"direction\":\"top_to_bottom\",\"members\":%u}",
|
||||
member_total);
|
||||
dingosdk::logging::event(dingosdk::logging::Channel::music, message);
|
||||
}
|
||||
}
|
||||
if (plan.set_cursor) *reinterpret_cast<volatile std::uint32_t*>(node + 0x30) = plan.cursor;
|
||||
last_binding_by_channel[channel] = {manager, queue, node, channel_playlist};
|
||||
last_binding_by_channel[channel] = {manager, queue, node, channel_playlist, member_total};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,6 +400,11 @@ bool initialize_music_playback(std::uintptr_t base, bool shuffle) noexcept {
|
||||
set_playback_mode = reinterpret_cast<SetPlaybackMode>(base + local_music::playback_mode_setter_contract.rva);
|
||||
shuffle_enabled.store(shuffle, std::memory_order_release);
|
||||
last_binding_by_channel = {};
|
||||
for (auto& binding : last_logged_channel_bindings)
|
||||
binding.store(std::numeric_limits<std::uint64_t>::max(), std::memory_order_relaxed);
|
||||
for (auto& results : logged_candidate_admission_results)
|
||||
results.store(0, std::memory_order_relaxed);
|
||||
logged_policy_events.store(0, std::memory_order_relaxed);
|
||||
playback_available.store(false, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
@@ -254,6 +423,82 @@ std::int32_t __fastcall music_select_next_hook(std::uintptr_t queue, std::uintpt
|
||||
return -1;
|
||||
}
|
||||
|
||||
MusicChannelPlaylistBind& music_channel_playlist_bind_original() noexcept { return bind_channel_playlist; }
|
||||
|
||||
std::uintptr_t __fastcall music_channel_playlist_bind_hook(std::uintptr_t channel_pointer,
|
||||
std::uintptr_t playlist_id_pointer) {
|
||||
if (!bind_channel_playlist) return 0;
|
||||
std::uint32_t channel{}, requested_playlist_id{}, favorites_playlist{};
|
||||
const bool have_binding = read_at(channel_pointer, channel) &&
|
||||
read_at(playlist_id_pointer, requested_playlist_id);
|
||||
auto playlist_id = requested_playlist_id;
|
||||
bool favorites_remapped{};
|
||||
if (have_binding && requested_playlist_id == 0) {
|
||||
const auto base = image_base.load(std::memory_order_acquire);
|
||||
std::uintptr_t manager{}, ui_manager{};
|
||||
const bool have_favorites = base &&
|
||||
read_at(base + local_music::asset_manager, manager) && manager && valid_range(manager, 0xd8) &&
|
||||
read_at(base + local_music::ui_manager, ui_manager) && valid_range(ui_manager, 0x1fc) &&
|
||||
read_at(ui_manager + 0x1f8, favorites_playlist);
|
||||
bool node_ready{};
|
||||
if (have_favorites && favorites_playlist) {
|
||||
CriticalSectionGuard manager_lock(manager + 0x20);
|
||||
std::uintptr_t current_manager{};
|
||||
node_ready = read_at(base + local_music::asset_manager, current_manager) &&
|
||||
current_manager == manager && favorites_node_ready(manager, favorites_playlist);
|
||||
}
|
||||
playlist_id = music_playback::resolve_playlist_binding_id(
|
||||
requested_playlist_id, favorites_playlist, node_ready);
|
||||
favorites_remapped = playlist_id != requested_playlist_id;
|
||||
}
|
||||
|
||||
const auto result = favorites_remapped
|
||||
? bind_channel_playlist(channel_pointer, reinterpret_cast<std::uintptr_t>(&playlist_id))
|
||||
: bind_channel_playlist(channel_pointer, playlist_id_pointer);
|
||||
|
||||
if (have_binding) {
|
||||
const auto signature = (static_cast<std::uint64_t>(channel) << 32) | playlist_id;
|
||||
bool should_log = true;
|
||||
if (channel < last_logged_channel_bindings.size()) {
|
||||
const auto previous = last_logged_channel_bindings[channel].exchange(
|
||||
signature, std::memory_order_relaxed);
|
||||
should_log = previous != signature;
|
||||
}
|
||||
if (should_log) {
|
||||
char message[224]{};
|
||||
std::snprintf(message, sizeof(message),
|
||||
"{\"event\":\"music_channel_playlist_binding\",\"channel\":%u,\"requested_playlist_id\":%u,\"playlist_id\":%u,\"favorites\":%u,\"favorites_remapped\":%s,\"applied\":%s}",
|
||||
channel, requested_playlist_id, playlist_id, favorites_playlist,
|
||||
favorites_remapped ? "true" : "false", result ? "true" : "false");
|
||||
dingosdk::logging::event(dingosdk::logging::Channel::music, message);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
MusicCandidateAdmission& music_candidate_admission_original() noexcept { return candidate_admission; }
|
||||
|
||||
bool __fastcall music_candidate_admission_hook(std::uintptr_t manager,
|
||||
std::uintptr_t candidate_guid, std::uintptr_t context, std::uint32_t channel_key) {
|
||||
if (!candidate_admission) return false;
|
||||
const bool accepted = candidate_admission(manager, candidate_guid, context, channel_key);
|
||||
const auto result_bit = static_cast<std::uint8_t>(accepted ? 2 : 1);
|
||||
bool should_log = true;
|
||||
if (channel_key < logged_candidate_admission_results.size()) {
|
||||
const auto previous = logged_candidate_admission_results[channel_key].fetch_or(
|
||||
result_bit, std::memory_order_relaxed);
|
||||
should_log = (previous & result_bit) == 0;
|
||||
}
|
||||
if (should_log) {
|
||||
char message[128]{};
|
||||
std::snprintf(message, sizeof(message),
|
||||
"{\"event\":\"music_candidate_admission\",\"channel_key\":%u,\"accepted\":%s}",
|
||||
channel_key, accepted ? "true" : "false");
|
||||
dingosdk::logging::event(dingosdk::logging::Channel::music, message);
|
||||
}
|
||||
return accepted;
|
||||
}
|
||||
|
||||
bool music_shuffle_enabled() noexcept { return shuffle_enabled.load(std::memory_order_acquire); }
|
||||
|
||||
void set_music_shuffle_enabled(bool enabled) noexcept {
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
|
||||
namespace dingosdk::profile_runtime {
|
||||
using MusicSelectNext = std::int32_t (__fastcall *)(std::uintptr_t queue, std::uintptr_t playlist_asset);
|
||||
using MusicChannelPlaylistBind = std::uintptr_t (__fastcall *)(std::uintptr_t channel_pointer,
|
||||
std::uintptr_t playlist_id_pointer);
|
||||
using MusicCandidateAdmission = bool (__fastcall *)(std::uintptr_t manager,
|
||||
std::uintptr_t candidate_guid, std::uintptr_t context, std::uint32_t channel_key);
|
||||
|
||||
// Validate the supported native selector/setter contracts and seed the lock-free
|
||||
// preference read used by the audio selection hook.
|
||||
@@ -13,6 +17,12 @@ void deactivate_music_playback() noexcept;
|
||||
bool music_playback_available() noexcept;
|
||||
MusicSelectNext& music_select_next_original() noexcept;
|
||||
std::int32_t __fastcall music_select_next_hook(std::uintptr_t queue, std::uintptr_t playlist_asset);
|
||||
MusicChannelPlaylistBind& music_channel_playlist_bind_original() noexcept;
|
||||
std::uintptr_t __fastcall music_channel_playlist_bind_hook(std::uintptr_t channel_pointer,
|
||||
std::uintptr_t playlist_id_pointer);
|
||||
MusicCandidateAdmission& music_candidate_admission_original() noexcept;
|
||||
bool __fastcall music_candidate_admission_hook(std::uintptr_t manager,
|
||||
std::uintptr_t candidate_guid, std::uintptr_t context, std::uint32_t channel_key);
|
||||
|
||||
bool music_shuffle_enabled() noexcept;
|
||||
void set_music_shuffle_enabled(bool enabled) noexcept;
|
||||
|
||||
@@ -8,6 +8,12 @@ inline constexpr std::uint32_t native_default_mode = 0;
|
||||
inline constexpr std::uint32_t native_sequential_mode = 1;
|
||||
inline constexpr std::uint32_t cursor_sync_sentinel = 0x7fffffffu;
|
||||
|
||||
constexpr std::uint32_t resolve_playlist_binding_id(std::uint32_t requested_id,
|
||||
std::uint32_t favorites_id, bool favorites_node_ready) noexcept {
|
||||
return requested_id == 0 && favorites_id != 0 && favorites_node_ready
|
||||
? favorites_id : requested_id;
|
||||
}
|
||||
|
||||
struct CursorPlan {
|
||||
std::uint32_t mode{};
|
||||
bool set_mode{};
|
||||
@@ -28,8 +34,15 @@ constexpr bool find_member_position(const std::uint8_t* members, std::uint32_t m
|
||||
return false;
|
||||
}
|
||||
|
||||
// Plan a native transition without touching the game's data. The current position
|
||||
// is in the mode-1 ordered-index vector after rebuilding it as identity order.
|
||||
constexpr std::uint32_t ordered_member_index(std::uint32_t position,
|
||||
std::uint32_t member_count,
|
||||
bool reverse) noexcept {
|
||||
if (position >= member_count) return member_count;
|
||||
return reverse ? member_count - position - 1 : position;
|
||||
}
|
||||
|
||||
// Plan a native transition without touching the game's data. Static playlist
|
||||
// positions use the native mode-1 order vector after it is rebuilt as identity.
|
||||
constexpr CursorPlan plan_cursor(bool shuffle, std::uint32_t current_mode,
|
||||
std::uint32_t current_cursor, bool new_group,
|
||||
bool explicit_request_pending, bool current_track_found,
|
||||
@@ -55,6 +68,14 @@ constexpr CursorPlan plan_cursor(bool shuffle, std::uint32_t current_mode,
|
||||
// starts at its first authored entry.
|
||||
plan.set_cursor = true;
|
||||
plan.cursor = 0;
|
||||
} else if (current_cursor >= order_count) {
|
||||
// Native favorite edits set the dynamic playlist cursor to its new member
|
||||
// count. That is outside the order vector and would make the selector
|
||||
// reject every candidate; resume after the current member when possible.
|
||||
plan.set_cursor = true;
|
||||
plan.cursor = current_track_found && order_count
|
||||
? (current_order_position + 1 == order_count ? 0 : current_order_position + 1)
|
||||
: 0;
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
@@ -544,10 +544,26 @@ bool initialize_local_profile(std::uintptr_t base, bool authored_offline,
|
||||
}
|
||||
const bool playback_ready = initialize_music_playback(base,
|
||||
local_preference("MusicShuffle").value_or(false));
|
||||
if (playback_ready)
|
||||
if (playback_ready) {
|
||||
std::array<unsigned char, 32> channel_bind_bytes{};
|
||||
if (read(base + addr::local_music::playback_channel_playlist_bind_contract.rva,
|
||||
channel_bind_bytes) &&
|
||||
channel_bind_bytes == addr::local_music::playback_channel_playlist_bind_contract.bytes)
|
||||
hook(addr::local_music::playback_channel_playlist_bind_contract,
|
||||
&music_channel_playlist_bind_hook, music_channel_playlist_bind_original());
|
||||
else dingosdk::logging::event(dingosdk::logging::Channel::music,
|
||||
"{\"event\":\"music_channel_playlist_binding_contract_mismatch\"}");
|
||||
std::array<unsigned char, 32> admission_bytes{};
|
||||
if (read(base + addr::local_music::playback_candidate_admission_contract.rva,
|
||||
admission_bytes) &&
|
||||
admission_bytes == addr::local_music::playback_candidate_admission_contract.bytes)
|
||||
hook(addr::local_music::playback_candidate_admission_contract,
|
||||
&music_candidate_admission_hook, music_candidate_admission_original());
|
||||
else dingosdk::logging::event(dingosdk::logging::Channel::music,
|
||||
"{\"event\":\"music_candidate_admission_contract_mismatch\"}");
|
||||
hook(addr::local_music::playback_select_next_contract,
|
||||
&music_select_next_hook, music_select_next_original());
|
||||
else dingosdk::logging::event(dingosdk::logging::Channel::music,
|
||||
} else dingosdk::logging::event(dingosdk::logging::Channel::music,
|
||||
"{\"event\":\"music_playback_order_contract_mismatch\"}");
|
||||
hook(buildkit_text_exists_contract, &buildkit_text_exists, buildkit_text_functions().exists);
|
||||
hook(buildkit_text_translate_contract, &buildkit_text_translate, buildkit_text_functions().translate);
|
||||
|
||||
Reference in new issue
Block a user