#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 #include "Extension/Multiplayer/Session/monotonic_clock.h" #include "Extension/Multiplayer/Session/peer_slots.h" #include #include #include #include #include #include #include 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(p), size); return true; } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } } template T get(Address p, std::size_t off = 0) { return native_pose_detail::value(readable, native_pose_detail::add(p, off), "skater audio"); } Address ptr(Address p, std::size_t off = 0) { return get
(p, off); } void require(bool valid, const char *why) { native_pose_detail::require(valid, why); } struct State { Address base{}; DWORD client_thread{}; std::atomic
local_component{}, local_entity{}; std::atomic captured{}; std::uint64_t next_local_audit{}; void (*submit)(Address, const NativeAudioFrame *){}; bool attempted{}, installed{}; std::mutex mutex; std::unique_ptr accumulated = std::make_unique(); 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, max_remote_players> components{}; std::atomic bound{}; }; State &state() { static auto *s = new State; return *s; } struct RemoteAudio { Address manager{}; std::atomic
remote_entity{}; std::atomic 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; return r->current(); } std::atomic
&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(handle) < 2) throw std::runtime_error("Local sound capture ownership changed."); NativeAudioFrame snapshot; require(readable(reinterpret_cast
(frame), snapshot.bytes.data(), 0x218), "Native sound update is unreadable."); const std::array masks{ audio_value(snapshot, 0x164), audio_value(snapshot, 0x130), audio_value(snapshot, 0x124), audio_value(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( 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 void fingerprint(Address base, Address rva, const std::array &bytes) { std::array 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(base + sound::submit); require(hook_prepare(target, reinterpret_cast(&submit_hook), &original) == HookOk, "Cannot prepare skater sound capture."); s.submit = reinterpret_cast(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(component, 0x74), r.source}; // The same handle rules as read_native_audio_binding. require(!out.handle || (out.handle >= 2 && out.handle != get(r.source, 0x74) && get(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(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 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(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