#include "native_player_ui.h" #include "Extension/Multiplayer/Session/peer_slots.h" #include "native_indicators.h" #include "native_party.h" #include "Engine/Core/Hooks/hooks.h" #include "Engine/Core/Platform/memory.h" #include "Engine/Game/Build/addresses.h" #include "Engine/Game/Build/20260929/native_player_ui.h" #include #include #include #include #include #include #include #include #include namespace dingosdk::multiplayer { namespace { namespace player_ui = addr::native_player_ui; // Map display, location manager and render message records: guarded copies, not a // system call per read (the map tick and its copy run every frame the map ticks). template T read(std::uintptr_t address) { T result{}; if (!memory::peek(address, result)) throw std::runtime_error("Native player UI memory unavailable."); return result; } void require(bool ok, const char *message) { if (!ok) throw std::runtime_error(message); } struct Location { std::array position{}; std::uint8_t fields = 1; std::array padding{}; }; struct MapEntry { std::uintptr_t manager{}; std::uint16_t handle{}; }; struct Shared { std::recursive_mutex maps_mutex; std::uintptr_t base{}; std::atomic installed{}; void (*map_tick)(std::uintptr_t){}; std::uintptr_t (*copy)(std::uintptr_t, std::uintptr_t){}; std::uintptr_t (*destroy)(std::uintptr_t, unsigned){}; void (*shutdown)(std::uint8_t){}; bool attempted{}; }; Shared &shared() { static auto *s = new Shared; return *s; } struct UiState { std::mutex snapshot_mutex; // visible, readable without the lock: a map tick skips a slot that shows // nothing and has no map entry to release. std::atomic shown{}; bool visible{}; ULONGLONG sampled{}, next_projection{}; std::array position{}; std::string name, status = "Waiting for the native player map."; std::map maps; std::atomic map_updates{}; }; UiState &state() { static auto *s = new PeerStorage; return s->current(); } using MapSelection = std::array; thread_local MapSelection current_maps{}; void status(std::string message) { auto &s = state(); std::lock_guard lock(s.snapshot_mutex); s.status = std::move(message); } bool manager_matches(std::uintptr_t manager) { const auto base = shared().base; return manager && read(base + player_ui::location_manager) == manager && read(manager) == base + player_ui::location_manager_vtable; } void release(MapEntry &entry) { if (entry.handle && manager_matches(entry.manager)) reinterpret_cast(shared().base + player_ui::location_release)(entry.manager, entry.handle); entry = {}; } // display: the map display's type checked out (tick_hook checks it once per tick). MapEntry *update_map(std::uintptr_t interface_address, bool display) { auto &s = state(); if (s.maps.empty() && !s.shown.load(std::memory_order_acquire)) return nullptr; MapEntry *selected{}; try { const auto object = interface_address - 0x40; require(display, "Native map display type differs."); std::array position{}; bool visible{}; { std::lock_guard snapshot(s.snapshot_mutex); visible = s.visible && GetTickCount64() - s.sampled < 1000 && !native_party_map_icon_visible(peer_slot); position = s.position; } auto found = s.maps.find(object); if (!visible) { if (found != s.maps.end()) { release(found->second); s.maps.erase(found); } } else { const auto now = GetTickCount64(); if (found != s.maps.end() && now < s.next_projection) return &found->second; s.next_projection = now + 50; require(found != s.maps.end() || s.maps.size() < 8, "Native player map instance limit reached."); auto &entry = s.maps[object]; const auto manager = read(shared().base + player_ui::location_manager); require(manager_matches(manager), "Native player-location manager is unavailable."); if (entry.manager != manager) entry = {}; // Same map projection inputs the native map tick uses for native players. const auto size = read>(object + 0x5320); const auto origin = read>(object + 0x5310); require(std::isfinite(size[0]) && std::isfinite(size[1]) && size[0] > 0 && size[1] > 0 && size[0] < 100000 && size[1] < 100000 && std::isfinite(origin[0]) && std::isfinite(origin[1]) && std::isfinite(origin[2]) && std::abs(origin[1]) > .01f && std::abs(origin[1] - position[1]) > .01f, "Waiting for native map projection."); alignas(16) const std::array world{position[0], position[1], position[2], 0}; Location location; reinterpret_cast( shared().base + player_ui::map_project)(location.position.data(), world.data(), size.data(), origin.data()); require(std::isfinite(location.position[0]) && std::isfinite(location.position[1]) && std::abs(location.position[0]) < 10000000 && std::abs(location.position[1]) < 10000000, "Native player map projection is invalid."); if (!entry.handle) { const std::uint64_t key = 0x5265536b6174654dULL + peer_slot; const auto scope = read(object + 0x5370); entry.manager = manager; entry.handle = reinterpret_cast(shared().base + player_ui::location_create)( manager, &key, &location, scope); if (entry.handle < 2) { entry = {}; throw std::runtime_error("Native player map allocation failed."); } } else { reinterpret_cast( shared().base + player_ui::location_update)(manager, entry.handle, &location); } selected = &entry; } } catch (const std::exception &e) { status(e.what()); } return selected; } void tick_hook(std::uintptr_t interface_address) { if (!shared().installed.load(std::memory_order_acquire)) { shared().map_tick(interface_address); return; } std::lock_guard lock(shared().maps_mutex); const auto previous = current_maps; // The display's type is the same for every slot: checked once per tick. std::uintptr_t display_type{}, interface_type{}; const bool display = memory::peek(interface_address - 0x40, display_type) && memory::peek(interface_address, interface_type) && display_type == shared().base + player_ui::map_display_vtable && interface_type == shared().base + player_ui::map_display_interface_vtable; each_peer([&] { current_maps[peer_slot] = update_map(interface_address, display); }); shared().map_tick(interface_address); current_maps = previous; } std::uintptr_t copy_hook(std::uintptr_t destination, std::uintptr_t source) { const auto caller = reinterpret_cast(_ReturnAddress()); const auto result = shared().copy(destination, source); // This first-256-byte move helper is folded across many UI types. Only the // exact player-map call may touch the remaining, type-specific fields. if (caller != shared().base + player_ui::copy_return) return result; each_peer([&] { auto &s = state(); if (!current_maps[peer_slot]) return; try { auto &entry = *current_maps[peer_slot]; if (!entry.handle || !manager_matches(entry.manager) || !(read(source + 0x114) & 1)) return; const auto array = read(source + 0x108); const auto count = read(array - 4) & 0x7fffffff; require(count < 128, "Native player map list exceeds bound."); // One read of the whole list (4-byte entries, handle in the low half). std::array handles{}; require(!count || memory::peek_bytes(array, handles.data(), count * sizeof(std::uint32_t)), "Native player UI memory unavailable."); for (unsigned i = 0; i < count; ++i) if (static_cast(handles[i]) == entry.handle) return; auto *slot = reinterpret_cast( shared().base + player_ui::list_append)(source + 0x108, 0); require(slot != nullptr, "Native player map list allocation failed."); // The caller moves this owned array into its render message after the // common prefix. Native message destruction releases this added ref. reinterpret_cast(shared().base + player_ui::location_retain)(entry.manager, entry.handle); *slot = entry.handle; ++s.map_updates; status("Native player map icon receiving positions."); } catch (const std::exception &e) { status(e.what()); } }); return result; } std::uintptr_t destroy_hook(std::uintptr_t object, unsigned flags) { { std::lock_guard lock(shared().maps_mutex); each_peer([&] { auto &s = state(); const auto found = s.maps.find(object); if (found != s.maps.end()) { try { release(found->second); } catch (...) { } s.maps.erase(found); } }); } return shared().destroy(object, flags); } void shutdown_hook(std::uint8_t unregister) { { std::lock_guard lock(shared().maps_mutex); each_peer([&] { auto &s = state(); for (auto &[object, entry] : s.maps) { try { release(entry); } catch (...) { } } s.maps.clear(); }); } shared().shutdown(unregister); } void install(std::uintptr_t base) { require(!shared().attempted, "Native player UI installation requires a restart."); shared().attempted = true; shared().base = base; auto prefix = [base](std::uintptr_t rva, const auto &bytes) { std::array actual{}; require(bytes.size() <= actual.size() && memory::read_bytes(base + rva, actual.data(), bytes.size()) && std::equal(bytes.begin(), bytes.end(), actual.begin()), "Native player UI function fingerprint differs."); }; prefix(player_ui::map_tick, player_ui::map_tick_prefix); prefix(player_ui::ui_copy, player_ui::ui_copy_prefix); prefix(player_ui::map_destroy, player_ui::map_destroy_prefix); prefix(player_ui::location_shutdown, player_ui::location_shutdown_prefix); prefix(player_ui::location_create, player_ui::location_create_prefix); prefix(player_ui::location_update, player_ui::location_update_prefix); prefix(player_ui::location_release, player_ui::location_release_prefix); prefix(player_ui::location_retain, player_ui::location_retain_prefix); prefix(player_ui::list_append, player_ui::list_append_prefix); prefix(player_ui::map_project, player_ui::map_project_prefix); prefix(player_ui::copy_call, player_ui::copy_call_prefix); require(read(base + player_ui::map_display_interface_vtable + 8) == base + player_ui::map_tick && read(base + player_ui::location_manager_vtable) == base + player_ui::location_retain && read(base + player_ui::location_manager_vtable + 0x18) == base + player_ui::location_update, "Native player map method table differs."); struct Hook { std::uintptr_t rva; void *replacement; void **original; }; const std::array hooks{ Hook{player_ui::map_tick, reinterpret_cast(&tick_hook), reinterpret_cast(&shared().map_tick)}, Hook{player_ui::ui_copy, reinterpret_cast(©_hook), reinterpret_cast(&shared().copy)}, Hook{player_ui::map_destroy, reinterpret_cast(&destroy_hook), reinterpret_cast(&shared().destroy)}, Hook{player_ui::location_shutdown, reinterpret_cast(&shutdown_hook), reinterpret_cast(&shared().shutdown)}}; std::size_t prepared{}; for (const auto &hook : hooks) { if (hook_prepare(reinterpret_cast(base + hook.rva), hook.replacement, hook.original) != HookOk) { while (prepared) hook_remove(reinterpret_cast(base + hooks[--prepared].rva)); throw std::runtime_error("Cannot prepare native player map hooks."); } ++prepared; } // Keep forwarding trampolines on uncertain enable results, with no map // mutations until every hook, including manager teardown, is active. // One transaction (one suspension of every game thread) for all of them. for (const auto &hook : hooks) require(hook_queue_enable(reinterpret_cast(base + hook.rva)) == HookOk, "Cannot enable native player map hooks; restart ReSkate."); require(hook_apply_queued() == HookOk, "Cannot enable native player map hooks; restart ReSkate."); shared().installed.store(true, std::memory_order_release); } } // namespace void prepare_player_ui(std::uintptr_t base) noexcept { try { if (!shared().attempted) install(base); } catch (const std::exception &e) { status(e.what()); } } void update_player_ui(std::uintptr_t base, const Pose *pose, std::string name) noexcept { update_party_position(pose); update_native_indicators(base, pose, name); auto &s = state(); try { { std::lock_guard lock(s.snapshot_mutex); // The map consumes only root position. Full pose validation occurs // once when the packet enters PoseBuffer. s.visible = pose && valid_transform(pose->root); s.shown.store(s.visible, std::memory_order_release); s.sampled = GetTickCount64(); s.name = std::move(name); if (s.visible) s.position = pose->root.position; } if (pose && !shared().attempted) install(base); } catch (const std::exception &e) { status(e.what()); } } std::string player_ui_status() { const auto indicators = native_indicators_status(); auto &s = state(); std::lock_guard lock(s.snapshot_mutex); return indicators + "\n" + s.status + "\n" + native_party_status(); } std::uint64_t player_map_updates() noexcept { return state().map_updates.load(); } } // namespace dingosdk::multiplayer