From 845e4cb6baa838a82d76aae3c342b3a5920b62ef Mon Sep 17 00:00:00 2001 From: shumsic Date: Mon, 26 Oct 2026 15:54:44 -0400 Subject: [PATCH] Multiplayer: check a player's distance when their first pose places them, before spawning a skater MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Problem 1. **Unplaced first-pose phantom spawns (`render`):** In the 500 ms distance loop (`s.next_shown_rank`), `p.shown_wanted` is set to `everyone || !p.placed || nearest_distance(...) <= ...` so that before a peer's first pose arrives (`!p.placed`, `sampled == false`), their status reads `"Waiting for player poses."` rather than `"Further away than players are shown."`. However, when their first pose packet arrives in `each_active_peer` (`const bool sampled = ...`), `if (sampled) p.placed = true;` flips `p.placed` to `true` while `p.shown_wanted` is still `true` from when `!p.placed` was true. Because `nearest_distance` has not yet been checked against the newly populated `p.render_pose`, `show_remote()` is immediately called on that frame for every newly-placed player on the server — spawning a full 395-bone 3D skater, 16-bone skateboard, and cosmetics for players 300–500 m across the map, only for the next 500 ms `s.next_shown_rank` pass to evaluate `nearest_distance > s.player_distance` and immediately destroy them (`remove_remote()`). 2. **Follow-camera orbit shifting the distance boundary (`nearest_distance`):** `nearest_distance(p, local, view)` takes the nearer of `local.pose.root.position` and `view` (the camera). While `view` is needed when the camera is detached from the local skater (spectating another player across the map, replay camera, or flying a drone), during ordinary skating the follow camera orbits ~3–5 m around the local skater. Rotating the camera around the skater shifts the effective `player_distance` boundary by ~8–10 m (consuming the 15% hysteresis band at 50 m) and causes clusters of players near the boundary (such as a Spot Battle crowd) to spawn and despawn as the camera turns. ### Solution 1. When a peer's first pose transitions `p.placed` from `false` to `true` (`if (sampled && !std::exchange(p.placed, true))`), immediately update `p.shown_wanted` from `nearest_distance(p, local, view) <= s.player_distance` before evaluating `spawning` and `show_remote()`. Peers before their first pose still keep `p.shown_wanted = true` (`"Waiting for player poses."`), nearby peers still spawn on their first pose frame without waiting for the 500 ms tick, and far-away peers never phantom-spawn on arrival. 2. In `nearest_distance()`, consider `view` when `!local.ready` or when the camera is more than 12 m from `local.pose.root.position` (spectating, replay, or drone), keeping the distance radius anchored to the local skater during normal third-person follow-camera rotation. ### Verification - Tested in-game in 44–78 player online lobbies with active Spot Battle throwdowns (`ReSkate.dll` runtime verification). - Verified in `ReSkate.log` that far-away peers no longer trigger `creating a remote skater` / `Skater component factory` spawns on their first pose packet and that rotating the camera toward a Spot Battle huddle near the 50 m / 70 m boundary no longer triggers a spawn storm. --- Extension/Multiplayer/Session/session.cpp | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/Extension/Multiplayer/Session/session.cpp b/Extension/Multiplayer/Session/session.cpp index 744c2be1..5423d7b0 100644 --- a/Extension/Multiplayer/Session/session.cpp +++ b/Extension/Multiplayer/Session/session.cpp @@ -462,8 +462,9 @@ Pose out_of_sight(Pose pose) { // as it stood each frame and their camera looks wherever it likes afterwards: a player who had // been behind the camera moved at 20 Hz in the replay. constexpr int far_full_rate_return = 190, far_half_rate_start = 200, far_low_rate_start = 350; -// Metres from a player to the nearer of the local skater and the camera (infinite if neither -// is known). +// Metres from a player to the nearer of the local skater and a detached camera (spectating, +// replay or a drone; the follow camera orbits a few metres around the skater, which would +// otherwise shift the distance edge as the camera turns). Infinite if neither is known. float nearest_distance(const Peer &p, const NativeFrame &local, const std::optional &view) { const auto &at = p.render_pose.root.position; float nearest = std::numeric_limits::infinity(); @@ -472,7 +473,17 @@ float nearest_distance(const Peer &p, const NativeFrame &local, const std::optio nearest = std::min(nearest, dx * dx + dy * dy + dz * dz); }; if (local.ready) consider(local.pose.root.position[0], local.pose.root.position[1], local.pose.root.position[2]); - if (view) consider(view->world[12], view->world[13], view->world[14]); + if (view) { + const float cx = view->world[12], cy = view->world[13], cz = view->world[14]; + if (!local.ready) { + consider(cx, cy, cz); + } else { + const float lx = cx - local.pose.root.position[0]; + const float ly = cy - local.pose.root.position[1]; + const float lz = cz - local.pose.root.position[2]; + if (lx * lx + ly * ly + lz * lz > 12.0f * 12.0f) consider(cx, cy, cz); + } + } return std::sqrt(nearest); } std::uint64_t far_sample_interval(const Peer &p, float distance) { @@ -652,7 +663,11 @@ void render(Session &s, std::uintptr_t client, const NativeFrame &local, std::ui // the next frames slower still. const bool held = !sampled && was_visible && p.poses.heard_within(now, 8000000); // Showing a player without an actor spawns one (native_skater_spawn.cpp). - if (sampled) p.placed = true; + // Before a pose placed them, shown_wanted stayed true so their status read "Waiting for + // player poses."; check their distance now that render_pose is known. + if (sampled && !std::exchange(p.placed, true)) + p.shown_wanted = s.player_distance >= player_distance_unlimited || + nearest_distance(p, local, view) <= s.player_distance; const bool spawning = sampled && p.shown_wanted && p.appearance.value() && !was_visible && !remote_skater_entity(); if (spawning && now < p.next_spawn) { p.native_status = "Waiting to show the player again.";