fix(streaming): #138-B avatar vanishes after teleporting out — don't relocate during PortalSpace

After a teleport-OUT the per-frame avatar-sync (GameWindow ~:8018) called
GpuWorldState.RelocateEntity with the player controller's cell — which stays the
FROZEN SOURCE cell until PlaceTeleportArrival materializes the destination. So
mid-transit it dragged the avatar (which the teleport's rescue/re-inject had
correctly placed at the destination center) back into the now-UNLOADED source
landblock's pending bucket, where nothing recovers it (RelocateEntity only scans
_loaded). Net: the avatar vanished after teleporting out and stayed gone.

Fix: skip the per-frame relocate while the player is in PortalSpace. The teleport
machinery (DrainRescued + PlaceTeleportArrival) owns the avatar's landblock during
transit; per-frame relocation resumes at FireLoginComplete (InWorld).

Live-verified with a new env-gated avatar-lifecycle probe (ACDREAM_PROBE_ENT /
EntityVanishProbe; [ent] draw-set transitions + [dyn] cull check): pre-fix the
trace showed `[ent] APPEND lb=0x0007FFFF(source) -> PENDING -> DRAWSET ABSENT`
never recovering; post-fix every teleport goes RESCUE -> ABSENT ->
APPEND(destination) -> PRESENT and stays drawn (4 teleports incl. to Holtburg,
session ended PRESENT).

Suites green: Core 1568(+2 skip), App 468(+2 skip), UI 425, Net 317.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-24 12:30:20 +02:00
parent 57e79dc679
commit afd5f2a012
4 changed files with 111 additions and 1 deletions

View file

@ -0,0 +1,49 @@
using System;
namespace AcDream.App.Streaming;
/// <summary>
/// TEMP diagnostic (#138-B avatar-vanish). Env-gated (<c>ACDREAM_PROBE_ENT=1</c>)
/// trace of the persistent player entity across teleport streaming churn:
/// whether it is present in the render draw-DATA (<see cref="GpuWorldState"/>
/// flat view) and whether it survives the dynamics CULL
/// (<c>RetailPViewRenderer.DrawDynamicsLast</c>).
///
/// <para>The two halves answer the decisive question: is the avatar
/// "missing for a moment" because it is <b>absent from the draw set</b>
/// (re-injected into the pending bucket while its landblock re-streams), or
/// because it is <b>present but culled</b> (in the dynamics list, dropped by
/// the outside-stage / viewcone gate)?</para>
///
/// <para>STRIP once #138-B is root-caused (throwaway, like the dense-town FPS
/// apparatus). Observation-only — emits no behavior change.</para>
/// </summary>
internal static class EntityVanishProbe
{
public static readonly bool Enabled =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_ENT") == "1";
/// <summary>Player server guid, set once at world entry so the draw-side
/// <c>[dyn]</c> line can single out the avatar without plumbing the guid
/// through the render stack.</summary>
public static uint PlayerGuid;
public static void Log(string msg)
{
if (Enabled) Console.WriteLine(msg);
}
private static string _lastPlayerDyn = "";
/// <summary>Emit a <c>[dyn] player</c> line only when the outcome string
/// changes — a stationary, drawn avatar must not spam one line per frame
/// (that console+Tee I/O would itself depress FPS and bury the signal).
/// Fires on cell change or DRAWN↔CULLED transition.</summary>
public static void LogPlayerDynOnChange(string status)
{
if (!Enabled) return;
if (status == _lastPlayerDyn) return;
_lastPlayerDyn = status;
Console.WriteLine("[dyn] player " + status);
}
}

View file

@ -298,6 +298,7 @@ public sealed class GpuWorldState
if (entity.ServerGuid != 0 && _persistentGuids.Contains(entity.ServerGuid))
{
_persistentRescued.Add(entity);
EntityVanishProbe.Log($"[ent] RESCUE guid=0x{entity.ServerGuid:X8} from=loaded lb=0x{landblockId:X8}");
}
}
@ -331,7 +332,10 @@ public sealed class GpuWorldState
foreach (var entity in pendingForLb)
{
if (entity.ServerGuid != 0 && _persistentGuids.Contains(entity.ServerGuid))
{
_persistentRescued.Add(entity);
EntityVanishProbe.Log($"[ent] RESCUE guid=0x{entity.ServerGuid:X8} from=pending lb=0x{landblockId:X8}");
}
}
}
@ -439,6 +443,8 @@ public sealed class GpuWorldState
_entityScriptActivator?.OnCreate(entity);
uint canonicalLandblockId = (landblockId & 0xFFFF0000u) | 0xFFFFu;
bool probePersistent = EntityVanishProbe.Enabled
&& entity.ServerGuid != 0 && _persistentGuids.Contains(entity.ServerGuid);
if (_loaded.TryGetValue(canonicalLandblockId, out var lb))
{
@ -451,6 +457,8 @@ public sealed class GpuWorldState
lb.LandblockId,
lb.Heightmap,
newEntities);
if (probePersistent)
EntityVanishProbe.Log($"[ent] APPEND guid=0x{entity.ServerGuid:X8} lb=0x{canonicalLandblockId:X8} -> LOADED(drawn)");
RebuildFlatView();
return;
}
@ -464,6 +472,8 @@ public sealed class GpuWorldState
_pendingByLandblock[canonicalLandblockId] = bucket;
}
bucket.Add(entity);
if (probePersistent)
EntityVanishProbe.Log($"[ent] APPEND guid=0x{entity.ServerGuid:X8} lb=0x{canonicalLandblockId:X8} -> PENDING(hidden)");
}
/// <summary>
@ -570,5 +580,29 @@ public sealed class GpuWorldState
private void RebuildFlatView()
{
_flatEntities = _loaded.Values.SelectMany(lb => lb.Entities).ToArray();
if (EntityVanishProbe.Enabled) ProbeFlatViewTransitions();
}
// TEMP (#138-B): persistent guids currently present in the drawn flat
// view, for EntityVanishProbe transition logging. Strip with the probe.
private readonly HashSet<uint> _persistentInFlatProbe = new();
// TEMP (#138-B): log when a persistent (player) entity enters/leaves the
// drawn flat view. Transition-gated → low volume (fires at teleport
// boundaries, not every rebuild). Strip with EntityVanishProbe.
private void ProbeFlatViewTransitions()
{
var now = new HashSet<uint>();
foreach (var e in _flatEntities)
if (e.ServerGuid != 0 && _persistentGuids.Contains(e.ServerGuid))
now.Add(e.ServerGuid);
foreach (var g in now)
if (!_persistentInFlatProbe.Contains(g))
EntityVanishProbe.Log($"[ent] DRAWSET guid=0x{g:X8} -> PRESENT (flatCount={_flatEntities.Count})");
foreach (var g in _persistentInFlatProbe)
if (!now.Contains(g))
EntityVanishProbe.Log($"[ent] DRAWSET guid=0x{g:X8} -> ABSENT (flatCount={_flatEntities.Count})");
_persistentInFlatProbe.Clear();
foreach (var g in now) _persistentInFlatProbe.Add(g);
}
}