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.
This commit is contained in:
MiTHRAL 2026-08-28 01:20:52 -04:00
parent d809bbc25a
commit df86553b77
3 changed files with 32 additions and 25 deletions

View file

@ -17,6 +17,7 @@
#include "skate3_native_ghost_net.h" #include "skate3_native_ghost_net.h"
#include "skate3_native_ghost_relay.h" #include "skate3_native_ghost_relay.h"
#include "skate3_native_entity.h"
#include "skate3_native_guest_read.h" #include "skate3_native_guest_read.h"
#include "skate3_native_palette.h" #include "skate3_native_palette.h"
@ -901,24 +902,7 @@ void GhostNetworkLoop() {
} // namespace } // namespace
// ---- capture (sim thread; hook in skate3_native_render.cpp) ------------------ // ---- capture (per built frame; see OnFrameSceneBuilt) ----------------------
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<std::mutex> lock(g_mu);
std::memcpy(g_local_world, world, sizeof(g_local_world));
g_local_valid = true;
g_local_time = NowSec();
}
namespace { namespace {
// Identity rows for a bone (column-vector affine [R|t]): the safe padding // 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_count = rows;
g_local_pose_valid = true; g_local_pose_valid = true;
g_local_pose_time = NowSec(); 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() { void EnsureNetworkThread() {

View file

@ -31,13 +31,12 @@ struct FrameScene;
namespace skate3::native_ghost { 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) ----- // ---- host-side pose capture (called from BuildFrameScene pre-publish) -----
// Snapshot the local player's skater skeleton (the highest-bone-count // Snapshot the local player's skater skeleton (the highest-bone-count
// skinned non-ropa character item near the camera) as the pose this client // 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); void OnFrameSceneBuilt(uint8_t* base, const skate3::native_scene::FrameScene& scene);
// ---- network thread lifecycle ------------------------------------------------- // ---- network thread lifecycle -------------------------------------------------

View file

@ -473,9 +473,6 @@ extern "C" REX_FUNC(sub_827A52C8) {
__imp__sub_827A52C8(ctx, base); __imp__sub_827A52C8(ctx, base);
if (skate3::native_render::Enabled()) { if (skate3::native_render::Enabled()) {
skate3::native_palette::OnBoneTransforms(base, parts, count); 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);
} }
} }