mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-18 00:27:06 +02:00
355 lines
16 KiB
C++
355 lines
16 KiB
C++
#include "native_audio.h"
|
|
#include "native_audio_layout.h"
|
|
#include "native_audio_binding.h"
|
|
#include "audio_capture.h"
|
|
#include "native_pose_layout.h"
|
|
#include "Engine/Core/Hooks/hooks.h"
|
|
#include "Engine/Core/Log/logging.h"
|
|
#include "Engine/Core/Platform/memory.h"
|
|
#include "Engine/Game/Build/addresses.h"
|
|
#include "Engine/Game/Build/20260929/engine.h"
|
|
#include "Engine/Game/Build/20260929/native_audio.h"
|
|
#include <Windows.h>
|
|
#include "Extension/Multiplayer/Session/monotonic_clock.h"
|
|
#include "Extension/Multiplayer/Session/peer_slots.h"
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <mutex>
|
|
#include <memory>
|
|
#include <format>
|
|
#include <cstring>
|
|
|
|
namespace dingosdk::multiplayer {
|
|
namespace {
|
|
namespace sound = addr::native_audio;
|
|
using Address = std::uintptr_t;
|
|
bool readable(Address p, void *out, std::size_t size) {
|
|
if (p < 0x10000 || size >= memory::highest_user_address || p > memory::highest_user_address - size)
|
|
return false;
|
|
__try {
|
|
std::memcpy(out, reinterpret_cast<const void *>(p), size);
|
|
return true;
|
|
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
|
return false;
|
|
}
|
|
}
|
|
template <class T> T get(Address p, std::size_t off = 0) {
|
|
return native_pose_detail::value<T>(readable, native_pose_detail::add(p, off), "skater audio");
|
|
}
|
|
Address ptr(Address p, std::size_t off = 0) { return get<Address>(p, off); }
|
|
void require(bool valid, const char *why) { native_pose_detail::require(valid, why); }
|
|
struct State {
|
|
Address base{};
|
|
DWORD client_thread{};
|
|
std::atomic<Address> local_component{}, local_entity{};
|
|
std::atomic<std::uint64_t> captured{};
|
|
std::uint64_t next_local_audit{};
|
|
void (*submit)(Address, const NativeAudioFrame *){};
|
|
bool attempted{}, installed{};
|
|
std::mutex mutex;
|
|
std::unique_ptr<NativeAudioFrame> accumulated = std::make_unique<NativeAudioFrame>();
|
|
AudioCaptureBuffer queue;
|
|
std::string capture_issue;
|
|
// Each slot's remote sound component, contiguous so submit_hook (every skater's
|
|
// sound update) scans a few cache lines up to `bound`, the highest slot that
|
|
// has ever stored one plus one. The bound grows before the component is stored.
|
|
std::array<std::atomic<Address>, max_remote_players> components{};
|
|
std::atomic<std::size_t> bound{};
|
|
};
|
|
State &state() {
|
|
static auto *s = new State;
|
|
return *s;
|
|
}
|
|
struct RemoteAudio {
|
|
Address manager{};
|
|
std::atomic<Address> remote_entity{};
|
|
std::atomic<std::uint64_t> played{};
|
|
bool playing{};
|
|
std::uint16_t last_handle{}; // Diagnostic only; owned by the native component.
|
|
std::string playback_issue;
|
|
// Owners of the last full binding resolve (read_native_audio_binding walks both
|
|
// actors' component collections). Updates between the 1 Hz resolves re-check
|
|
// these with a few reads and read only the handle again. Client thread.
|
|
Address source{}, binding_local{}, binding_context{};
|
|
std::uint64_t binding_actor{}; // remote_skater_generation() of the actor resolved
|
|
ULONGLONG next_binding{};
|
|
};
|
|
RemoteAudio &remote() {
|
|
static auto *r = new PeerStorage<RemoteAudio>;
|
|
return r->current();
|
|
}
|
|
std::atomic<Address> &remote_component() { return state().components[peer_slot]; }
|
|
void set_remote_component(Address component) noexcept {
|
|
auto &s = state();
|
|
if (component) {
|
|
const auto needed = peer_slot + 1;
|
|
for (auto current = s.bound.load(std::memory_order_acquire);
|
|
current < needed && !s.bound.compare_exchange_weak(current, needed, std::memory_order_acq_rel);) {
|
|
}
|
|
}
|
|
remote_component().store(component, std::memory_order_release);
|
|
}
|
|
void submit_hook(Address handle, const NativeAudioFrame *frame) {
|
|
auto &s = state();
|
|
// The visual-only actor submits default inputs after animation evaluation.
|
|
// Suppress those updates so they cannot overwrite our buffered peer inputs.
|
|
// Handle creation, description changes and destruction still run natively.
|
|
const auto error_before = GetLastError();
|
|
bool suppress{};
|
|
const auto component = handle > 0x74 ? handle - 0x74 : 0;
|
|
const auto bound = component ? std::min(s.bound.load(std::memory_order_acquire), max_remote_players)
|
|
: std::size_t{};
|
|
// A stale slot can still hold a destroyed actor's component address: keep
|
|
// looking until one slot's actor owns it.
|
|
for (std::size_t slot = 0; slot < bound && !suppress; ++slot) {
|
|
if (s.components[slot].load(std::memory_order_acquire) != component)
|
|
continue;
|
|
const PeerScope scope(slot);
|
|
try {
|
|
const auto actor = remote().remote_entity.load();
|
|
if (actor && actor == remote_skater_entity() &&
|
|
ptr(component) == s.base + addr::engine::skater_audio_component_vtable &&
|
|
ptr(ptr(component, 0x18)) == actor && !ptr(actor, 0xf8)) {
|
|
suppress = true;
|
|
}
|
|
} catch (...) {
|
|
}
|
|
}
|
|
SetLastError(error_before);
|
|
if (suppress)
|
|
return;
|
|
s.submit(handle, frame); // Original local audio is always forwarded unchanged.
|
|
const auto c = s.local_component.load(std::memory_order_acquire);
|
|
if (!c || handle != c + 0x74)
|
|
return;
|
|
const auto error = GetLastError();
|
|
std::lock_guard lock(s.mutex);
|
|
try {
|
|
if (c != s.local_component.load() || ptr(c) != s.base + addr::engine::skater_audio_component_vtable ||
|
|
ptr(ptr(c, 0x18)) != s.local_entity.load() || get<std::uint16_t>(handle) < 2)
|
|
throw std::runtime_error("Local sound capture ownership changed.");
|
|
NativeAudioFrame snapshot;
|
|
require(readable(reinterpret_cast<Address>(frame), snapshot.bytes.data(), 0x218),
|
|
"Native sound update is unreadable.");
|
|
const std::array<std::uint32_t, 4> masks{
|
|
audio_value<std::uint32_t>(snapshot, 0x164), audio_value<std::uint32_t>(snapshot, 0x130),
|
|
audio_value<std::uint32_t>(snapshot, 0x124), audio_value<std::uint32_t>(snapshot, 0xc0)};
|
|
require(!(masks[3] & ~0x07ffffffU), "Native sound update fields differ.");
|
|
// Pure native field copy merges partial updates into owned storage.
|
|
// It contains only primitive fields and does not allocate or call out.
|
|
reinterpret_cast<void (*)(NativeAudioFrame *, const NativeAudioFrame *, const void *)>(
|
|
s.base + sound::frame_copy)(s.accumulated.get(), &snapshot, masks.data());
|
|
s.queue.push(audio_state(*s.accumulated), now_us());
|
|
++s.captured;
|
|
s.capture_issue.clear();
|
|
} catch (const std::exception &e) {
|
|
s.capture_issue = e.what();
|
|
}
|
|
SetLastError(error);
|
|
}
|
|
template <std::size_t N>
|
|
void fingerprint(Address base, Address rva, const std::array<std::uint8_t, N> &bytes) {
|
|
std::array<std::uint8_t, 32> actual{};
|
|
require(bytes.size() <= actual.size() && readable(base + rva, actual.data(), bytes.size()) &&
|
|
std::equal(bytes.begin(), bytes.end(), actual.begin()),
|
|
"Native skater sound function differs.");
|
|
}
|
|
void install(Address base) {
|
|
auto &s = state();
|
|
require(!s.attempted, "Sound hook installation requires restarting ReSkate.");
|
|
fingerprint(base, sound::submit, sound::submit_prefix);
|
|
fingerprint(base, sound::frame_copy, sound::frame_copy_prefix);
|
|
fingerprint(base, sound::frame_reset, sound::frame_reset_prefix);
|
|
fingerprint(base, sound::stop_sound, sound::stop_sound_prefix);
|
|
s.attempted = true;
|
|
s.base = base;
|
|
s.client_thread = GetCurrentThreadId();
|
|
void *original{};
|
|
auto *target = reinterpret_cast<void *>(base + sound::submit);
|
|
require(hook_prepare(target, reinterpret_cast<void *>(&submit_hook), &original) == HookOk,
|
|
"Cannot prepare skater sound capture.");
|
|
s.submit = reinterpret_cast<decltype(s.submit)>(original);
|
|
require(hook_enable(target) == HookOk, "Cannot enable skater sound capture.");
|
|
s.installed = true;
|
|
}
|
|
// The remote actor's sound binding for this update. A full resolve walks both
|
|
// actors' component collections; it runs at 1 Hz, or when the owners found last
|
|
// time no longer check out. Between, the handle is the only field read again.
|
|
NativeAudioBinding resolve_binding(Address base, const NativeFrame &local, Address actor) {
|
|
auto &r = remote();
|
|
const auto now = GetTickCount64();
|
|
if (const auto component = remote_component().load(std::memory_order_acquire);
|
|
component && now < r.next_binding && r.binding_actor == remote_skater_generation() &&
|
|
r.remote_entity.load() == actor && r.binding_local == local.entity &&
|
|
r.binding_context == local.context && r.source &&
|
|
ptr(component) == base + addr::engine::skater_audio_component_vtable &&
|
|
ptr(ptr(component, 0x18)) == actor && ptr(base, addr::engine::audio_manager) == r.manager) {
|
|
NativeAudioBinding out{component, r.manager, get<std::uint16_t>(component, 0x74), r.source};
|
|
// The same handle rules as read_native_audio_binding.
|
|
require(!out.handle || (out.handle >= 2 && out.handle != get<std::uint16_t>(r.source, 0x74) &&
|
|
get<std::uint8_t>(component, 0x76) == 0),
|
|
"Remote sound handle ownership differs.");
|
|
return out;
|
|
}
|
|
const auto out = read_native_audio_binding(readable, base, local.entity, actor, local.context);
|
|
r.binding_actor = remote_skater_generation();
|
|
r.source = out.source;
|
|
r.binding_local = local.entity;
|
|
r.binding_context = local.context;
|
|
r.next_binding = now + 1000;
|
|
return out;
|
|
}
|
|
} // namespace
|
|
void prepare_remote_audio(Address base) noexcept {
|
|
try {
|
|
if (!state().attempted) install(base);
|
|
} catch (...) {}
|
|
}
|
|
void prepare_audio_capture(Address base, const NativeFrame &local) noexcept {
|
|
auto &s = state();
|
|
try {
|
|
if (!s.installed)
|
|
install(base);
|
|
require(base == s.base && GetCurrentThreadId() == s.client_thread,
|
|
"Skater sound must update on the client thread.");
|
|
const auto now = now_us();
|
|
if (s.local_entity.load(std::memory_order_acquire) == local.entity &&
|
|
s.local_component.load(std::memory_order_acquire) && now < s.next_local_audit)
|
|
return;
|
|
const auto component =
|
|
read_native_component(readable, local.entity, base + addr::engine::skater_audio_component_vtable);
|
|
require(ptr(local.entity) == base + addr::engine::skater_entity_vtable && ptr(local.entity, 0xf8),
|
|
"Local sound actor ownership differs.");
|
|
if (component != s.local_component.load() || local.entity != s.local_entity.load()) {
|
|
s.local_component.store(0, std::memory_order_release);
|
|
std::lock_guard lock(s.mutex);
|
|
s.queue.clear();
|
|
*s.accumulated = {};
|
|
reinterpret_cast<void (*)(void *)>(base + sound::frame_reset)(s.accumulated.get());
|
|
s.local_entity.store(local.entity);
|
|
s.local_component.store(component, std::memory_order_release);
|
|
s.capture_issue.clear();
|
|
}
|
|
s.next_local_audit = now + 1000000;
|
|
} catch (const std::exception &e) {
|
|
std::lock_guard lock(s.mutex);
|
|
s.capture_issue = e.what();
|
|
}
|
|
}
|
|
std::vector<AudioSample> drain_audio_capture(std::uint64_t now) {
|
|
auto &s = state();
|
|
std::lock_guard lock(s.mutex);
|
|
return s.queue.drain(now);
|
|
}
|
|
void stop_remote_audio() noexcept {
|
|
auto &s = state();
|
|
remote().next_binding = 0; // resolve in full next time, whatever was playing
|
|
if (!remote().playing)
|
|
return;
|
|
remote().playing = false;
|
|
try {
|
|
const auto actor = remote().remote_entity.load();
|
|
if (s.installed && GetCurrentThreadId() == s.client_thread && actor &&
|
|
actor == remote_skater_entity()) {
|
|
const auto binding =
|
|
read_native_audio_binding(readable, s.base, s.local_entity.load(), actor, ptr(actor, 0x20));
|
|
if (binding.component == remote_component().load() &&
|
|
binding.manager == remote().manager && binding.handle) {
|
|
// Use the component's native stop operation, including clearing
|
|
// its handle. Never release a cached ID that could be recycled.
|
|
reinterpret_cast<void (*)(Address)>(s.base + sound::stop_sound)(binding.component + 0x74);
|
|
logging::log(logging::Level::debug, logging::Channel::runtime,
|
|
"Multiplayer: native peer sound stopped; actor={:#x}, handle={}.", actor,
|
|
binding.handle);
|
|
}
|
|
}
|
|
} catch (...) { // Entity/manager teardown may already have destroyed the voice.
|
|
}
|
|
remote().last_handle = 0;
|
|
remote().manager = 0;
|
|
remote().next_binding = 0;
|
|
// Keep suppressing this actor's default submissions while sound is stale.
|
|
// A subsequent native update may create an idle handle, but it receives no
|
|
// locomotion inputs until fresh peer data arrives. Entity destruction owns it.
|
|
}
|
|
void reset_audio() noexcept {
|
|
auto &s = state();
|
|
each_peer([&] {
|
|
stop_remote_audio();
|
|
set_remote_component(0);
|
|
remote().remote_entity.store(0);
|
|
remote().next_binding = 0;
|
|
remote().playback_issue.clear();
|
|
});
|
|
s.local_component.store(0, std::memory_order_release);
|
|
s.local_entity.store(0);
|
|
s.next_local_audit = 0;
|
|
std::lock_guard lock(s.mutex);
|
|
s.queue.clear();
|
|
s.capture_issue.clear();
|
|
remote().playback_issue.clear();
|
|
}
|
|
void update_remote_audio(Address base, const NativeFrame &local, const Pose &pose,
|
|
const AudioState &audio) noexcept {
|
|
auto &s = state();
|
|
try {
|
|
require(s.installed && base == s.base && GetCurrentThreadId() == s.client_thread,
|
|
"Native sound playback is unavailable.");
|
|
const auto actor = remote_skater_entity();
|
|
const auto binding = resolve_binding(base, local, actor);
|
|
if (remote().remote_entity.load() != actor || remote_component().load() != binding.component ||
|
|
remote().manager != binding.manager) {
|
|
stop_remote_audio();
|
|
set_remote_component(0);
|
|
remote().remote_entity.store(actor);
|
|
set_remote_component(binding.component);
|
|
remote().manager = binding.manager;
|
|
}
|
|
if (!binding.handle) {
|
|
remote().playing = false;
|
|
std::lock_guard lock(s.mutex);
|
|
remote().playback_issue = "Waiting for the remote actor's native sound handle.";
|
|
return;
|
|
}
|
|
const auto frame = audio_frame(audio, pose);
|
|
// Call the original submit directly. The hook only suppresses the
|
|
// engine's visual-only defaults; all sound synthesis stays native.
|
|
s.submit(binding.component + 0x74, &frame);
|
|
remote().playing = true;
|
|
if (remote().last_handle != binding.handle) {
|
|
remote().last_handle = binding.handle;
|
|
logging::log(logging::Level::debug, logging::Channel::runtime,
|
|
"Multiplayer: native peer sound bound; actor={:#x}, component={:#x}, handle={}, "
|
|
"manager={:#x}.",
|
|
actor, binding.component, binding.handle, binding.manager);
|
|
}
|
|
++remote().played;
|
|
// Only this thread writes the issue; the lock is for status readers.
|
|
if (!remote().playback_issue.empty()) {
|
|
std::lock_guard lock(s.mutex);
|
|
remote().playback_issue.clear();
|
|
}
|
|
} catch (const std::exception &e) {
|
|
stop_remote_audio();
|
|
remote().next_binding = 0; // resolve in full next time
|
|
std::lock_guard lock(s.mutex);
|
|
remote().playback_issue = e.what();
|
|
}
|
|
}
|
|
std::string native_audio_status() {
|
|
auto &s = state();
|
|
std::lock_guard lock(s.mutex);
|
|
if (!s.capture_issue.empty())
|
|
return "Sound: " + s.capture_issue;
|
|
if (!remote().playback_issue.empty())
|
|
return "Sound: " + remote().playback_issue;
|
|
if (!s.captured.load())
|
|
return "Sound: waiting for native skater audio updates.";
|
|
return remote().playing ? "Sound: native spatial skater and skateboard playback active."
|
|
: "Sound: captured; waiting for peer playback.";
|
|
}
|
|
std::uint64_t captured_audio_frames() noexcept { return state().captured.load(); }
|
|
std::uint64_t played_audio_frames() noexcept { return remote().played.load(); }
|
|
} // namespace dingosdk::multiplayer
|