diff --git a/Extension/Customization/local_player_card_runtime.cpp b/Extension/Customization/local_player_card_runtime.cpp index 85d968ac..d06eec12 100644 --- a/Extension/Customization/local_player_card_runtime.cpp +++ b/Extension/Customization/local_player_card_runtime.cpp @@ -115,6 +115,8 @@ void apply_player_card(std::uintptr_t manager, const profile::CosmeticLoadout& v } std::uint64_t local_player_info_hook() { + if (const auto other = player_info_override.load(std::memory_order_relaxed)) + if (const auto info = other()) return info; auto& s = local_runtime(); auto& p = player_card_runtime(); const auto original = p.functions.get_local_info(); if (!s.active.load(std::memory_order_acquire) || original) return original; diff --git a/Extension/Customization/local_player_card_runtime.h b/Extension/Customization/local_player_card_runtime.h index a68bae33..dd72ec1e 100644 --- a/Extension/Customization/local_player_card_runtime.h +++ b/Extension/Customization/local_player_card_runtime.h @@ -45,6 +45,11 @@ bool player_card_owned(const profile::CosmeticLoadout& value); void apply_player_card(std::uintptr_t manager, const profile::CosmeticLoadout& value); std::uint64_t local_player_info_hook(); +// Whose info the local-player lookup gives instead, for a graph that asks for "the local +// player" and means someone else (the placed-object prompt: Extension/Objects/object_prompt.h). +// 0 from it is the local player as usual. Set once by the multiplayer HUD; it is called on +// every lookup, so it must answer 0 at once when no such graph is running. +inline std::atomic player_info_override{}; bool publish_player_card(const profile::CosmeticLoadout& value); diff --git a/Extension/Multiplayer/Hud/native_party_hooks.cpp b/Extension/Multiplayer/Hud/native_party_hooks.cpp index f74c4192..003b7c48 100644 --- a/Extension/Multiplayer/Hud/native_party_hooks.cpp +++ b/Extension/Multiplayer/Hud/native_party_hooks.cpp @@ -6,6 +6,9 @@ #include "Extension/Throwdowns/throwdown_relay.h" #include "Engine/Core/Platform/memory.h" #include "follow_camera.h" +#include "Extension/Customization/local_player_card_runtime.h" +#include "Extension/Objects/network_object_runtime.h" +#include "Extension/Objects/object_prompt.h" #include "Engine/Game/Build/20260929/profile.h" #include #include @@ -679,6 +682,24 @@ void clear_stale_cooldowns() { // the SDK started pressed no Coop button here, so IsCoop still says solo and the row never comes // (analysis/coop-challenges-re/coop_criteria_hud.md). It is set just before either graph runs. constexpr std::uint32_t graph_objective_list = 0xeae864ca, graph_challenge_rewards = 0x1e01dd61; +// Above zero on the thread that is running the placed-object prompt's graph (object_prompt.h). +thread_local int object_prompt_running{}; +// profile_runtime::player_info_override: while that prompt runs, the info of whoever placed the +// object nearest the skater, when that is another player the party HUD has a record for. +std::uint64_t object_owner_info() noexcept { + if (!object_prompt_running) return 0; + try { + auto &s = state(); + const auto position = object_prompt::viewer_position(); + if (!position || !s.installed.load(std::memory_order_acquire)) return 0; + const auto owner = profile_runtime::nearest_network_object_owner(*position, object_prompt::reach); + Address ui{}; + if (!owner || !memory::peek(s.base + engine::ui_manager, ui) || !ui) return 0; + return get_info_hook(ui, owner); + } catch (...) { + return 0; + } +} void show_coop_objectives() { auto &s = state(); const auto published = s.published.load(std::memory_order_acquire); @@ -716,6 +737,11 @@ void run_expression(Address vm, std::uint32_t pc, Address profiler, void (*runne // The common path avoids RPM, schema scans and locks for unrelated scripts. const auto resource = *reinterpret_cast(vm + 0x38); const auto hash = *reinterpret_cast(resource + 0x10); + struct Prompt { + bool on; + explicit Prompt(bool running) : on(running) { if (on) ++object_prompt_running; } + ~Prompt() { if (on) --object_prompt_running; } + } prompt(hash == object_prompt::graph); if ((hash == graph_objective_list || hash == graph_challenge_rewards) && state().challenge_coop.load(std::memory_order_acquire)) { try { show_coop_objectives(); } catch (const std::exception &e) { status(e.what()); } @@ -784,6 +810,7 @@ void install(Address base) { "Cannot enable native party hooks; restart ReSkate."); require(hook_apply_queued() == HookOk, "Cannot enable native party hooks; restart ReSkate."); s.installed.store(true, std::memory_order_release); + profile_runtime::player_info_override.store(&object_owner_info, std::memory_order_release); try { install_requests(base); } catch (const std::exception &e) { status(e.what()); } } } // namespace native_party_detail diff --git a/Extension/Multiplayer/Session/session.cpp b/Extension/Multiplayer/Session/session.cpp index 806a93c4..31f13aeb 100644 --- a/Extension/Multiplayer/Session/session.cpp +++ b/Extension/Multiplayer/Session/session.cpp @@ -1,5 +1,6 @@ #include "session_internal.h" #include "Engine/Core/Debug/trace.h" +#include "Extension/Objects/object_prompt.h" #include "Extension/Multiplayer/Hud/native_player_ui.h" #include "Extension/Multiplayer/Hud/native_indicators.h" #include "Extension/Multiplayer/Hud/native_party.h" @@ -71,6 +72,7 @@ void reset_peer(Session &s, std::size_t slot) { s.peers[slot] = {}; } void stop(Session &s, std::string reason) { + object_prompt::forget_viewer(); s.local_typing_until = 0; // A join that ends before the host's map was ready never got in. if (s.mode == Mode::join && s.awaiting_map && !s.travelling && s.join_started) @@ -450,6 +452,8 @@ std::uint64_t far_sample_interval(const Peer &p, float distance) { } void render(Session &s, std::uintptr_t client, const NativeFrame &local, std::uint64_t now) { if (!world_playing(s, local)) return; + // For naming who placed the object in front of the skater (object_prompt.h). + object_prompt::set_viewer(local.pose.root.position); const auto view = latest_game_view(); // ReSkate's nametags and chat bubbles: every shown player, placed above their head // each frame. diff --git a/Extension/Objects/network_object_runtime.cpp b/Extension/Objects/network_object_runtime.cpp index 6c204076..d3e486b3 100644 --- a/Extension/Objects/network_object_runtime.cpp +++ b/Extension/Objects/network_object_runtime.cpp @@ -64,6 +64,27 @@ void refresh_budget() { extra_budget.store(static_cast(count), std::memory_order_release); } } // namespace network_objects_detail +std::uint64_t nearest_network_object_owner(const std::array &position, float reach) noexcept { + try { + std::unique_lock lock(local_runtime().native_mutex, std::try_to_lock); + if (!lock.owns_lock()) return 0; + float best = reach * reach; + std::uint64_t owner{}; + // Local objects are in `live` too, under owner 0, so the nearest being your own answers 0. + for (const auto &[key, live] : network_objects_detail::state().live) { + float squared{}; + for (std::size_t i = 0; i < 3; ++i) + squared += (live.applied.position[i] - position[i]) * (live.applied.position[i] - position[i]); + if (squared < best) { + best = squared; + owner = std::get<0>(key); + } + } + return owner; + } catch (...) { + return 0; + } +} std::uint32_t network_object_extra_budget() noexcept { return network_objects_detail::extra_budget.load(std::memory_order_acquire); } diff --git a/Extension/Objects/network_object_runtime.h b/Extension/Objects/network_object_runtime.h index 26dca94b..00389a87 100644 --- a/Extension/Objects/network_object_runtime.h +++ b/Extension/Objects/network_object_runtime.h @@ -42,4 +42,9 @@ bool network_object_observe(std::uint64_t entity, const profile::PlacedObject &o bool network_object_native_ready(); std::uint32_t network_object_extra_budget() noexcept; void move_network_object(std::uint64_t entity, const profile::PlacedObject &object); +// The player (SteamID64) whose object is nearest `position`, among every object in the +// world within `reach` metres; 0 when that is one of the local player's own, or none is +// near. Asked from inside a UI script, so it never waits: 0 when another thread is +// changing the objects. +std::uint64_t nearest_network_object_owner(const std::array &position, float reach) noexcept; } // namespace dingosdk::profile_runtime diff --git a/Extension/Objects/object_prompt.h b/Extension/Objects/object_prompt.h new file mode 100644 index 00000000..742a676e --- /dev/null +++ b/Extension/Objects/object_prompt.h @@ -0,0 +1,45 @@ +#pragma once +#include +#include +#include +#include + +// The game's prompt on a placed object names who placed it by asking for the local +// player's info, which is right in the game's own sessions (it only shows on your +// objects there) and wrong in ReSkate's, where other players' objects are spawned +// locally: every object read as your own. While that prompt's graph runs, the +// local-player lookup answers with the owner of the object nearest the skater instead +// (native_party_hooks.cpp), found among the objects ReSkate spawned for other players. +namespace dingosdk::object_prompt { +// The prompt's graph, as the expression VM names it (the hash at resource + 0x10). +inline constexpr std::uint32_t graph = 0x25bf9270; +// How far from the skater the object may be. The prompt is about the nearest one wherever +// it is, so this only rules out a position that is nowhere near the park. +inline constexpr float reach = 1337.f; + +// Where the local skater is, as the multiplayer session saw them this frame. Written +// every frame and read from a UI script, so it takes no lock; a torn read only moves +// the point by a frame. +struct Viewer { + std::atomic x{}, y{}, z{}; + std::atomic known{}; +}; +inline Viewer &viewer() noexcept { + static Viewer value; + return value; +} +inline void set_viewer(const std::array &position) noexcept { + auto &v = viewer(); + v.x.store(position[0], std::memory_order_relaxed); + v.y.store(position[1], std::memory_order_relaxed); + v.z.store(position[2], std::memory_order_relaxed); + v.known.store(true, std::memory_order_relaxed); +} +inline void forget_viewer() noexcept { viewer().known.store(false, std::memory_order_relaxed); } +inline std::optional> viewer_position() noexcept { + const auto &v = viewer(); + if (!v.known.load(std::memory_order_relaxed)) return std::nullopt; + return std::array{v.x.load(std::memory_order_relaxed), v.y.load(std::memory_order_relaxed), + v.z.load(std::memory_order_relaxed)}; +} +} // namespace dingosdk::object_prompt