From df86553b77aeeec36205f6a5519b043c0e0b705a Mon Sep 17 00:00:00 2001 From: MiTHRAL Date: Fri, 28 Aug 2026 01:20:52 -0400 Subject: [PATCH] ghost: capture local player transform from the body item, not the UBT hook The self-check cube (skate3_ghost_draw_local) appeared on a nearby NPC because OnLocalSkaterTick read the world from the UpdateBoneTransforms hook's pack-owner entity, and UpdateBoneTransforms fires for NPC skaters too - the last writer won, often an NPC. Local-player capture now happens in OnFrameSceneBuilt (the same per-frame body-item heuristic the ghost models use): resolve the body item's OWNING entity via the ctx->entity store and read its world matrix (entity+416), falling back to the body root-bone translation. The UBT-based capture and hook call are removed. --- src/native/skate3_native_ghost.cpp | 47 ++++++++++++++++++------------ src/native/skate3_native_ghost.h | 7 ++--- src/skate3_native_render.cpp | 3 -- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/native/skate3_native_ghost.cpp b/src/native/skate3_native_ghost.cpp index 461a853..5eb4a6e 100644 --- a/src/native/skate3_native_ghost.cpp +++ b/src/native/skate3_native_ghost.cpp @@ -17,6 +17,7 @@ #include "skate3_native_ghost_net.h" #include "skate3_native_ghost_relay.h" +#include "skate3_native_entity.h" #include "skate3_native_guest_read.h" #include "skate3_native_palette.h" @@ -901,24 +902,7 @@ void GhostNetworkLoop() { } // namespace -// ---- capture (sim thread; hook in skate3_native_render.cpp) ------------------ -void OnLocalSkaterTick(uint8_t* base) { - if (!Enabled()) { - return; - } - const uint32_t entity = skate3::native_palette::CurrentPackOwner(); - if (entity == 0) { - return; - } - float world[16]; - if (!ReadEntityWorld(base, entity, world)) { - return; - } - std::lock_guard lock(g_mu); - std::memcpy(g_local_world, world, sizeof(g_local_world)); - g_local_valid = true; - g_local_time = NowSec(); -} +// ---- capture (per built frame; see OnFrameSceneBuilt) ---------------------- namespace { // Identity rows for a bone (column-vector affine [R|t]): the safe padding @@ -988,6 +972,33 @@ void OnFrameSceneBuilt(uint8_t* base, g_local_pose_count = rows; g_local_pose_valid = true; g_local_pose_time = NowSec(); + + // Local player's world transform for the self-check cube / the wire + // pos/rot. Resolve the body item's OWNING ENTITY (ctx -> entity store) + // and read its world matrix - the reliable player signal. The previous + // UBT-hook capture could pick an NPC skater (UpdateBoneTransforms fires + // for them too), which put the cube on a nearby NPC. + float world[16]; + bool have_world = false; + if (body->ctx != 0) { + skate3::native_entity::CtxInfo info; + if (skate3::native_entity::LookupCtx(body->ctx, &info) && info.entity != 0 && + ReadEntityWorld(base, info.entity, world)) { + have_world = true; + } + } + if (!have_world) { + // Fallback: the body item's root bone translation (the player's pelvis + // world position); identity rotation is fine for a debug cube. + std::memset(world, 0, sizeof(world)); + world[0] = world[5] = world[10] = world[15] = 1.0f; + world[12] = body->bones[3]; + world[13] = body->bones[7]; + world[14] = body->bones[11]; + } + std::memcpy(g_local_world, world, sizeof(g_local_world)); + g_local_valid = true; + g_local_time = NowSec(); } void EnsureNetworkThread() { diff --git a/src/native/skate3_native_ghost.h b/src/native/skate3_native_ghost.h index 1f34ca1..e6238e4 100644 --- a/src/native/skate3_native_ghost.h +++ b/src/native/skate3_native_ghost.h @@ -31,13 +31,12 @@ struct FrameScene; namespace skate3::native_ghost { -// ---- host-side capture (called from skate3_native_render.cpp) ------------- -void OnLocalSkaterTick(uint8_t* base); - // ---- host-side pose capture (called from BuildFrameScene pre-publish) ----- // Snapshot the local player's skater skeleton (the highest-bone-count // skinned non-ropa character item near the camera) as the pose this client -// broadcasts. Called every built frame; gated internally on the enable cvar. +// broadcasts, plus the local player's world transform (resolved from that +// item's owning entity). Called every built frame; gated internally on the +// enable cvar. void OnFrameSceneBuilt(uint8_t* base, const skate3::native_scene::FrameScene& scene); // ---- network thread lifecycle ------------------------------------------------- diff --git a/src/skate3_native_render.cpp b/src/skate3_native_render.cpp index 33eacfd..3af9101 100644 --- a/src/skate3_native_render.cpp +++ b/src/skate3_native_render.cpp @@ -473,9 +473,6 @@ extern "C" REX_FUNC(sub_827A52C8) { __imp__sub_827A52C8(ctx, base); if (skate3::native_render::Enabled()) { skate3::native_palette::OnBoneTransforms(base, parts, count); - // Ghost multiplayer: capture the local player's world transform (the - // pack-owner entity at this point is the player's SkaterPresEntity). - skate3::native_ghost::OnLocalSkaterTick(base); } }