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:
parent
57e79dc679
commit
afd5f2a012
4 changed files with 111 additions and 1 deletions
|
|
@ -2394,6 +2394,7 @@ public sealed class GameWindow : IDisposable
|
||||||
_vitalsVm?.SetLocalPlayerGuid(chosen.Id);
|
_vitalsVm?.SetLocalPlayerGuid(chosen.Id);
|
||||||
Chat.SetLocalPlayerGuid(chosen.Id);
|
Chat.SetLocalPlayerGuid(chosen.Id);
|
||||||
_worldState.MarkPersistent(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}");
|
Console.WriteLine($"live: entering world as 0x{chosen.Id:X8} {chosen.Name}");
|
||||||
_liveSession.EnterWorld(user, characterIndex: 0);
|
_liveSession.EnterWorld(user, characterIndex: 0);
|
||||||
|
|
||||||
|
|
@ -8014,7 +8015,21 @@ public sealed class GameWindow : IDisposable
|
||||||
int ply = _liveCenterY + (int)System.Math.Floor(pp.Y / 192f);
|
int ply = _liveCenterY + (int)System.Math.Floor(pp.Y / 192f);
|
||||||
currentLb = (uint)((plx << 24) | (ply << 16) | 0xFFFF);
|
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
|
// Update chase camera(s). The CameraController exposes whichever
|
||||||
|
|
|
||||||
|
|
@ -726,6 +726,10 @@ public sealed class RetailPViewRenderer
|
||||||
{
|
{
|
||||||
EntitySphere(e, out var c, out float r);
|
EntitySphere(e, out var c, out float r);
|
||||||
bool indoor = InteriorEntityPartition.IsIndoorCellId(e.ParentCellId);
|
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
|
// #118: under an interior root, outdoor-classified dynamics drew in
|
||||||
// the outside stage (pre-clear, seal-protected) — retail draws them
|
// the outside stage (pre-clear, seal-protected) — retail draws them
|
||||||
// via LScape::draw's per-landcell DrawSortCell, never in the
|
// 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
|
// seal. Indoor dynamics (incl. exit-portal straddlers, which drew
|
||||||
// in BOTH stages) stay — this pass is retail's loop C.
|
// in BOTH stages) stay — this pass is retail's loop C.
|
||||||
if (!rootIsOutdoor && !indoor)
|
if (!rootIsOutdoor && !indoor)
|
||||||
|
{
|
||||||
|
if (isProbePlayer)
|
||||||
|
AcDream.App.Streaming.EntityVanishProbe.LogPlayerDynOnChange(
|
||||||
|
$"cell=0x{(e.ParentCellId ?? 0):X8} indoor=False rootOutdoor={rootIsOutdoor} -> CULLED(outside-stage)");
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
bool visible = indoor
|
bool visible = indoor
|
||||||
? viewcone.SphereVisibleInCell(e.ParentCellId!.Value, c, r)
|
? viewcone.SphereVisibleInCell(e.ParentCellId!.Value, c, r)
|
||||||
: viewcone.SphereVisibleOutside(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)
|
if (visible)
|
||||||
_dynamicsScratch.Add(e);
|
_dynamicsScratch.Add(e);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
49
src/AcDream.App/Streaming/EntityVanishProbe.cs
Normal file
49
src/AcDream.App/Streaming/EntityVanishProbe.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -298,6 +298,7 @@ public sealed class GpuWorldState
|
||||||
if (entity.ServerGuid != 0 && _persistentGuids.Contains(entity.ServerGuid))
|
if (entity.ServerGuid != 0 && _persistentGuids.Contains(entity.ServerGuid))
|
||||||
{
|
{
|
||||||
_persistentRescued.Add(entity);
|
_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)
|
foreach (var entity in pendingForLb)
|
||||||
{
|
{
|
||||||
if (entity.ServerGuid != 0 && _persistentGuids.Contains(entity.ServerGuid))
|
if (entity.ServerGuid != 0 && _persistentGuids.Contains(entity.ServerGuid))
|
||||||
|
{
|
||||||
_persistentRescued.Add(entity);
|
_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);
|
_entityScriptActivator?.OnCreate(entity);
|
||||||
|
|
||||||
uint canonicalLandblockId = (landblockId & 0xFFFF0000u) | 0xFFFFu;
|
uint canonicalLandblockId = (landblockId & 0xFFFF0000u) | 0xFFFFu;
|
||||||
|
bool probePersistent = EntityVanishProbe.Enabled
|
||||||
|
&& entity.ServerGuid != 0 && _persistentGuids.Contains(entity.ServerGuid);
|
||||||
|
|
||||||
if (_loaded.TryGetValue(canonicalLandblockId, out var lb))
|
if (_loaded.TryGetValue(canonicalLandblockId, out var lb))
|
||||||
{
|
{
|
||||||
|
|
@ -451,6 +457,8 @@ public sealed class GpuWorldState
|
||||||
lb.LandblockId,
|
lb.LandblockId,
|
||||||
lb.Heightmap,
|
lb.Heightmap,
|
||||||
newEntities);
|
newEntities);
|
||||||
|
if (probePersistent)
|
||||||
|
EntityVanishProbe.Log($"[ent] APPEND guid=0x{entity.ServerGuid:X8} lb=0x{canonicalLandblockId:X8} -> LOADED(drawn)");
|
||||||
RebuildFlatView();
|
RebuildFlatView();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -464,6 +472,8 @@ public sealed class GpuWorldState
|
||||||
_pendingByLandblock[canonicalLandblockId] = bucket;
|
_pendingByLandblock[canonicalLandblockId] = bucket;
|
||||||
}
|
}
|
||||||
bucket.Add(entity);
|
bucket.Add(entity);
|
||||||
|
if (probePersistent)
|
||||||
|
EntityVanishProbe.Log($"[ent] APPEND guid=0x{entity.ServerGuid:X8} lb=0x{canonicalLandblockId:X8} -> PENDING(hidden)");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -570,5 +580,29 @@ public sealed class GpuWorldState
|
||||||
private void RebuildFlatView()
|
private void RebuildFlatView()
|
||||||
{
|
{
|
||||||
_flatEntities = _loaded.Values.SelectMany(lb => lb.Entities).ToArray();
|
_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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue