diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index 8910e919..e4f61dc3 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -2394,6 +2394,7 @@ public sealed class GameWindow : IDisposable
_vitalsVm?.SetLocalPlayerGuid(chosen.Id);
Chat.SetLocalPlayerGuid(chosen.Id);
_worldState.MarkPersistent(chosen.Id);
+ AcDream.App.Streaming.EntityVanishProbe.PlayerGuid = chosen.Id; // TEMP #138-B probe
Console.WriteLine($"live: entering world as 0x{chosen.Id:X8} {chosen.Name}");
_liveSession.EnterWorld(user, characterIndex: 0);
@@ -8014,7 +8015,21 @@ public sealed class GameWindow : IDisposable
int ply = _liveCenterY + (int)System.Math.Floor(pp.Y / 192f);
currentLb = (uint)((plx << 24) | (ply << 16) | 0xFFFF);
}
- _worldState.RelocateEntity(pe, currentLb);
+ // #138-B (2026-06-24): do NOT relocate the avatar while a teleport
+ // is in transit (PortalSpace). The controller's cell is still the
+ // FROZEN SOURCE until PlaceTeleportArrival materializes the
+ // destination, so relocating here drags the avatar — which the
+ // teleport's rescue/re-inject (GpuWorldState.DrainRescued) already
+ // 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 vanishes
+ // after teleporting out. The teleport machinery owns the avatar's
+ // landblock during transit; per-frame relocation resumes at
+ // FireLoginComplete (InWorld). Probe-confirmed: launch4 line 561
+ // APPEND guid=player lb=0x0007FFFF(source dungeon) -> PENDING ->
+ // DRAWSET ABSENT, never recovered.
+ if (_playerController.State != AcDream.App.Input.PlayerState.PortalSpace)
+ _worldState.RelocateEntity(pe, currentLb);
}
// Update chase camera(s). The CameraController exposes whichever
diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs
index 2819c3c5..30db299f 100644
--- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs
+++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs
@@ -726,6 +726,10 @@ public sealed class RetailPViewRenderer
{
EntitySphere(e, out var c, out float r);
bool indoor = InteriorEntityPartition.IsIndoorCellId(e.ParentCellId);
+ // TEMP (#138-B): trace the avatar's survival through this cull.
+ bool isProbePlayer = AcDream.App.Streaming.EntityVanishProbe.Enabled
+ && AcDream.App.Streaming.EntityVanishProbe.PlayerGuid != 0
+ && e.ServerGuid == AcDream.App.Streaming.EntityVanishProbe.PlayerGuid;
// #118: under an interior root, outdoor-classified dynamics drew in
// the outside stage (pre-clear, seal-protected) — retail draws them
// via LScape::draw's per-landcell DrawSortCell, never in the
@@ -734,10 +738,18 @@ public sealed class RetailPViewRenderer
// seal. Indoor dynamics (incl. exit-portal straddlers, which drew
// in BOTH stages) stay — this pass is retail's loop C.
if (!rootIsOutdoor && !indoor)
+ {
+ if (isProbePlayer)
+ AcDream.App.Streaming.EntityVanishProbe.LogPlayerDynOnChange(
+ $"cell=0x{(e.ParentCellId ?? 0):X8} indoor=False rootOutdoor={rootIsOutdoor} -> CULLED(outside-stage)");
continue;
+ }
bool visible = indoor
? viewcone.SphereVisibleInCell(e.ParentCellId!.Value, c, r)
: viewcone.SphereVisibleOutside(c, r);
+ if (isProbePlayer)
+ AcDream.App.Streaming.EntityVanishProbe.LogPlayerDynOnChange(
+ $"cell=0x{(e.ParentCellId ?? 0):X8} indoor={indoor} rootOutdoor={rootIsOutdoor} viewcone={visible} -> {(visible ? "DRAWN" : "CULLED(viewcone)")}");
if (visible)
_dynamicsScratch.Add(e);
}
diff --git a/src/AcDream.App/Streaming/EntityVanishProbe.cs b/src/AcDream.App/Streaming/EntityVanishProbe.cs
new file mode 100644
index 00000000..a991e261
--- /dev/null
+++ b/src/AcDream.App/Streaming/EntityVanishProbe.cs
@@ -0,0 +1,49 @@
+using System;
+
+namespace AcDream.App.Streaming;
+
+///
+/// TEMP diagnostic (#138-B avatar-vanish). Env-gated (ACDREAM_PROBE_ENT=1)
+/// trace of the persistent player entity across teleport streaming churn:
+/// whether it is present in the render draw-DATA (
+/// flat view) and whether it survives the dynamics CULL
+/// (RetailPViewRenderer.DrawDynamicsLast).
+///
+/// The two halves answer the decisive question: is the avatar
+/// "missing for a moment" because it is absent from the draw set
+/// (re-injected into the pending bucket while its landblock re-streams), or
+/// because it is present but culled (in the dynamics list, dropped by
+/// the outside-stage / viewcone gate)?
+///
+/// STRIP once #138-B is root-caused (throwaway, like the dense-town FPS
+/// apparatus). Observation-only — emits no behavior change.
+///
+internal static class EntityVanishProbe
+{
+ public static readonly bool Enabled =
+ Environment.GetEnvironmentVariable("ACDREAM_PROBE_ENT") == "1";
+
+ /// Player server guid, set once at world entry so the draw-side
+ /// [dyn] line can single out the avatar without plumbing the guid
+ /// through the render stack.
+ public static uint PlayerGuid;
+
+ public static void Log(string msg)
+ {
+ if (Enabled) Console.WriteLine(msg);
+ }
+
+ private static string _lastPlayerDyn = "";
+
+ /// Emit a [dyn] player 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.
+ public static void LogPlayerDynOnChange(string status)
+ {
+ if (!Enabled) return;
+ if (status == _lastPlayerDyn) return;
+ _lastPlayerDyn = status;
+ Console.WriteLine("[dyn] player " + status);
+ }
+}
diff --git a/src/AcDream.App/Streaming/GpuWorldState.cs b/src/AcDream.App/Streaming/GpuWorldState.cs
index 5f67b527..6217c814 100644
--- a/src/AcDream.App/Streaming/GpuWorldState.cs
+++ b/src/AcDream.App/Streaming/GpuWorldState.cs
@@ -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)");
}
///
@@ -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 _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();
+ 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);
}
}