#include "puppet_cost.h" #include "native_skater_internal.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/20260929/engine.h" #include "Engine/Game/Build/20260929/puppet_cost.h" #include #include #include #include // Native work a remote puppet does not need (Engine/Game/Build/20260929/puppet_cost.h): // - driven_placement: AntDriven no longer re-places the skater entity every frame; // - ecs_retry: the ECS attach that never completes on a remote actor runs once a second; // - far_components: past 200 m the crowd agent, stimulus and pedestrian collider update // 4 times a second, with the elapsed game time. // Within 200 m of the local skater or the camera every native update stays as it was. namespace dingosdk::multiplayer { using namespace native_skater_detail; namespace { namespace cost = addr::puppet_cost; using Address = std::uintptr_t; using ComponentUpdate = void (*)(Address component, const void *info); using ManagerRegister = void (*)(Address manager, Address item, std::uint16_t interval); using ManagerUnregister = void (*)(Address manager, Address item); // The far component rate applies past live_distance. constexpr float live_distance = 200.0f; constexpr float far_update_period = 0.25f; // game seconds between far component updates constexpr ULONGLONG ecs_retry_ms = 1000, far_linger_ms = 1000; constexpr unsigned bit(PuppetSaving saving) noexcept { return 1u << static_cast(saving); } constexpr unsigned all_savings = bit(PuppetSaving::driven_placement) | bit(PuppetSaving::ecs_retry) | bit(PuppetSaving::far_components); std::atomic &enabled_savings() noexcept { static std::atomic value{all_savings}; return value; } bool on(PuppetSaving saving) noexcept { return (enabled_savings().load(std::memory_order_relaxed) & bit(saving)) != 0; } enum Kind : std::size_t { navigator, stimulus, collider, ecs, kinds }; constexpr std::array contracts{ &cost::navigator_update, &cost::stim_update, &cost::collider_update, &cost::ecs_update}; // The class vtable slot each handler must occupy. constexpr std::array, kinds> handler_slots{{ {cost::navigator_vtable, cost::presim_handler_slot}, {cost::stim_vtable, cost::presim_handler_slot}, {cost::collider_vtable, cost::postsim_handler_slot}, {cost::ecs_vtable, cost::presim_handler_slot}, }}; // Read by the update hooks on whichever thread runs the pass. struct UpdateHookSlot { std::atomic distant{}; // past live_distance with far_components on // Game seconds not yet handed to the navigator, stimulus and collider updates. std::array, 3> deferred{}; // Earliest next ECS attach attempt for the skater and the skateboard. std::array, 2> next_ecs{}; }; struct UpdateHooks { std::array, kinds> original{}; std::atomic installed{}; std::atomic far_slots{}; // After the last player comes back within range the hooks keep resolving owners a // little longer, so each update hands over its deferred time once. std::atomic linger_until{}; std::array slots{}; }; UpdateHooks &update_hooks() noexcept { static auto *h = new UpdateHooks{}; return *h; } // What this module applied to the current slot's actor. Client thread. struct Applied { Address driven{}, driven_manager{}; // unregistered AntDriven component and its manager bool driven_done{}; // tried for this actor (success or not) unsigned failed{}; // savings disabled for this actor after an error }; Applied &applied() noexcept { static auto *a = new PeerStorage; return a->current(); } std::atomic undriven_actors{}; void set_far(bool value) noexcept { auto &h = update_hooks(); if (h.slots[peer_slot].distant.exchange(value, std::memory_order_acq_rel) == value) return; h.far_slots.fetch_add(value ? 1 : -1, std::memory_order_acq_rel); if (!value) h.linger_until.store(GetTickCount64() + far_linger_ms, std::memory_order_release); } // The slot whose skater (or skateboard, when boards) owns component. Two reads and a scan of // the watched keys: the only work the hooks add for every other actor. std::size_t owner_slot(Address component, bool boards, bool *board = nullptr) noexcept { Address collection{}, owner{}; if (!memory::peek(component + 0x18, collection) || !memory::peek(collection, owner) || !owner) return max_remote_players; auto &keys = watched(); const auto bound = std::min(keys.bound.load(std::memory_order_acquire), max_remote_players); for (std::size_t slot = 0; slot < bound; ++slot) { if (keys.entity[slot].load(std::memory_order_acquire) == owner) return slot; if (boards && keys.board[slot].load(std::memory_order_acquire) == owner) { if (board) *board = true; return slot; } } return max_remote_players; } // Navigator, stimulus and pedestrian collider updates (component, const UpdateInfo *): past // 200 m they run every far_update_period with the game time elapsed since their last run, // so the collider's velocity (position change / dt) and the timers stay right. template void far_update_hook(Address component, const void *info) noexcept { auto &h = update_hooks(); const auto original = h.original[K].load(std::memory_order_acquire); if (h.far_slots.load(std::memory_order_acquire) <= 0 && GetTickCount64() >= h.linger_until.load(std::memory_order_acquire)) { original(component, info); return; } const auto error = GetLastError(); const auto slot = owner_slot(component, false); const auto at = reinterpret_cast
(info); float dt{}; if (slot >= max_remote_players || !memory::peek(at + cost::update_info_dt, dt) || !std::isfinite(dt) || dt < 0.0f) { SetLastError(error); original(component, info); return; } auto &state = h.slots[slot]; auto &deferred = state.deferred[K]; const float total = deferred.load(std::memory_order_relaxed) + dt; if (state.distant.load(std::memory_order_acquire) && on(PuppetSaving::far_components) && total < far_update_period) { deferred.store(total, std::memory_order_relaxed); SetLastError(error); return; } deferred.store(0.0f, std::memory_order_relaxed); alignas(16) std::array copy{}; if (total == dt || !memory::peek_bytes(at, copy.data(), copy.size())) { SetLastError(error); original(component, info); return; } std::memcpy(copy.data() + cost::update_info_dt, &total, sizeof(total)); SetLastError(error); original(component, copy.data()); } // The ECS attach retry of a remote skater or skateboard: at most once per ecs_retry_ms. // The native handler itself only queues the attempt while the attach is incomplete. void ecs_update_hook(Address component, const void *info) noexcept { auto &h = update_hooks(); const auto original = h.original[ecs].load(std::memory_order_acquire); if (!on(PuppetSaving::ecs_retry) || !watched().bound.load(std::memory_order_acquire)) { original(component, info); return; } const auto error = GetLastError(); bool board{}; const auto slot = owner_slot(component, true, &board); if (slot < max_remote_players) { auto &next = h.slots[slot].next_ecs[board ? 1 : 0]; const auto now = GetTickCount64(); if (now < next.load(std::memory_order_relaxed)) { SetLastError(error); return; } next.store(now + ecs_retry_ms, std::memory_order_relaxed); } SetLastError(error); original(component, info); } void *replacement(std::size_t kind) noexcept { switch (kind) { case navigator: return reinterpret_cast(&far_update_hook); case stimulus: return reinterpret_cast(&far_update_hook); case collider: return reinterpret_cast(&far_update_hook); default: return reinterpret_cast(&ecs_update_hook); } } constexpr std::array saving_names{"AntDriven placement removal", "ECS retry throttle", "far component throttle"}; constexpr std::array hook_names{"navigator", "stimulus broadcast", "pedestrian collider", "ECS attach"}; bool matches(Address base, const game::build::Fingerprint &contract) noexcept { std::array bytes{}; return readable(base + contract.rva, bytes.data(), bytes.size()) && bytes == contract.bytes; } // AntDriven re-places the skater entity from a joint of the pose buffer every frame; the // SDK already places it (place_actor) with the same network pose. Its own deinit uses the // same unregister call; register restores it. void set_undriven(Address base, Remote &r, Applied &a, bool want) { if (want && !a.driven_done) { a.driven_done = true; require(matches(base, cost::manager_register) && matches(base, cost::manager_unregister), "Native component manager functions differ."); const auto component = read_native_component(readable, r.entity, base + cost::ant_driven_vtable); const auto manager = ptr(component, cost::ant_driven_manager); if (!manager) return; require(ptr(manager) == base + addr::engine::source_manager_vtable && ptr(manager, cost::manager_hooks) == manager + cost::manager_hooks_storage && ptr(manager, cost::manager_hooks_storage) == base + cost::ant_driven_hooks_vtable && read(manager, cost::manager_kind) == cost::ant_driven_kind, "AntDriven registration differs."); reinterpret_cast(base + cost::manager_unregister.rva)(manager, component + cost::ant_driven_item); a.driven = component; a.driven_manager = manager; undriven_actors.fetch_add(1, std::memory_order_relaxed); } else if (!want && a.driven_done) { const auto component = std::exchange(a.driven, 0), manager = std::exchange(a.driven_manager, 0); a.driven_done = false; if (!component) return; undriven_actors.fetch_add(-1, std::memory_order_relaxed); // Registered again with its native interval (1), unless its own deinit released it. if (manager && ptr(component, cost::ant_driven_manager) == manager) reinterpret_cast(base + cost::manager_register.rva)( manager, component + cost::ant_driven_item, 1); } } void report(Applied &a, PuppetSaving saving, const std::exception &e) noexcept { if (a.failed & bit(saving)) return; a.failed |= bit(saving); logging::log(logging::Level::warning, logging::Channel::runtime, "Multiplayer: {} unavailable for the player in slot {}: {}", saving_names[static_cast(saving)], peer_slot, e.what()); } } // namespace namespace puppet_cost { PreparedHooks prepare_hooks(std::uintptr_t base) noexcept { auto &h = update_hooks(); PreparedHooks prepared; if (h.installed.load(std::memory_order_acquire)) return prepared; for (std::size_t kind = 0; kind < kinds; ++kind) { const auto &contract = *contracts[kind]; Address slot{}; if (!matches(base, contract) || !readable(base + handler_slots[kind].first + handler_slots[kind].second, &slot, sizeof(slot)) || slot != base + contract.rva) { logging::log(logging::Level::warning, logging::Channel::runtime, "Multiplayer: the {} update differs from the supported build; its saving stays off.", hook_names[kind]); continue; } auto *target = reinterpret_cast(base + contract.rva); void *original{}; if (const auto status = hook_prepare(target, replacement(kind), &original); status != HookOk || !original) { if (status == HookOk) hook_remove(target); logging::log(logging::Level::warning, logging::Channel::runtime, "Multiplayer: cannot prepare the {} update hook ({}).", hook_names[kind], hook_status_string(status)); continue; } h.original[kind].store(reinterpret_cast(original), std::memory_order_release); prepared.targets[prepared.count++] = target; } return prepared; } void hooks_applied(const PreparedHooks &prepared, bool enabled) noexcept { update_hooks().installed.store(enabled && prepared.count != 0, std::memory_order_release); } void forget_board() noexcept { update_hooks().slots[peer_slot].next_ecs[1].store(0, std::memory_order_relaxed); } void forget_skater() noexcept { forget_board(); auto &a = applied(); if (a.driven) undriven_actors.fetch_add(-1, std::memory_order_relaxed); a = {}; set_far(false); auto &slot = update_hooks().slots[peer_slot]; for (auto &deferred : slot.deferred) deferred.store(0.0f, std::memory_order_relaxed); slot.next_ecs[0].store(0, std::memory_order_relaxed); } } // namespace puppet_cost void set_puppet_saving(PuppetSaving saving, bool enabled) noexcept { if (enabled) enabled_savings().fetch_or(bit(saving), std::memory_order_relaxed); else enabled_savings().fetch_and(~bit(saving), std::memory_order_relaxed); } bool puppet_saving(PuppetSaving saving) noexcept { return on(saving); } void note_remote_distance(float metres) noexcept { auto &r = remote(); auto &a = applied(); auto &s = shared(); if (!s.hooks || !r.entity || GetCurrentThreadId() != s.engine_thread) { set_far(false); return; } // An unknown distance counts as near. const bool known = std::isfinite(metres) && metres >= 0.0f; set_far(known && metres > live_distance && on(PuppetSaving::far_components)); { std::lock_guard lock(r.mutex); if (r.failed) return; } // After an error a saving is not applied again to this actor, but whatever it already // applied is still undone when no longer wanted. try { set_undriven(s.base, r, a, on(PuppetSaving::driven_placement) && !(a.failed & bit(PuppetSaving::driven_placement))); } catch (const std::exception &e) { report(a, PuppetSaving::driven_placement, e); } } std::string puppet_saving_status() { const auto state = [](PuppetSaving saving) { return on(saving) ? "on" : "off"; }; return std::format("Far-player savings: AntDriven placement removal {} ({} players), ECS retry throttle {}, " "far components {} ({} players past 200 m); update hooks {}.", state(PuppetSaving::driven_placement), undriven_actors.load(), state(PuppetSaving::ecs_retry), state(PuppetSaving::far_components), update_hooks().far_slots.load(), update_hooks().installed.load() ? "installed" : "not installed"); } } // namespace dingosdk::multiplayer