fix(portal): synchronize destination presentation state
This commit is contained in:
parent
4b1bceefbb
commit
e95f55f25b
42 changed files with 2815 additions and 288 deletions
|
|
@ -2714,8 +2714,8 @@ public sealed class GameWindow : IDisposable
|
|||
_streamer.Start();
|
||||
|
||||
_streamingController = new AcDream.App.Streaming.StreamingController(
|
||||
enqueueLoad: (id, kind) => _streamer.EnqueueLoad(id, kind),
|
||||
enqueueUnload: _streamer.EnqueueUnload,
|
||||
enqueueLoad: (id, kind, generation) => _streamer.EnqueueLoad(id, kind, generation),
|
||||
enqueueUnload: (id, generation) => _streamer.EnqueueUnload(id, generation),
|
||||
drainCompletions: _streamer.DrainCompletions,
|
||||
applyTerrain: ApplyLoadedTerrain,
|
||||
state: _worldState,
|
||||
|
|
@ -2743,10 +2743,12 @@ public sealed class GameWindow : IDisposable
|
|||
_buildingRegistries.Remove(id & 0xFFFF0000u); // Phase A8 (key normalization fix 2026-05-28)
|
||||
_envCellRenderer?.RemoveLandblock(id); // Phase A8
|
||||
},
|
||||
demoteNearLayer: DemoteLoadedLandblockToTerrain,
|
||||
// #138: restore retained server objects when a landblock reloads
|
||||
// (dungeon-exit expand or Far→Near promote). ACE won't re-send the
|
||||
// objects it thinks we still know, so we re-project them ourselves.
|
||||
onLandblockLoaded: RehydrateServerEntitiesForLandblock);
|
||||
onLandblockLoaded: RehydrateServerEntitiesForLandblock,
|
||||
ensureEnvCellMeshes: EnsureEnvCellMeshesAfterPin);
|
||||
// A.5 T22.5: apply max-completions from resolved quality.
|
||||
_streamingController.MaxCompletionsPerFrame = _resolvedQuality.MaxCompletionsPerFrame;
|
||||
|
||||
|
|
@ -7058,14 +7060,12 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// worldReady for the TAS transit: is the player's teleport destination resident so we
|
||||
/// can materialize? Indoor (sealed dungeon / building interior) gates on the EnvCell
|
||||
/// struct hydrating (#135); outdoor gates on the destination's NEAR RING (the 3×3 around
|
||||
/// the player, <see cref="TeleportNearRingRadius"/>) being registered — not just the
|
||||
/// single destination landblock — so portal space exits onto a loaded, collidable world
|
||||
/// (the player's cell-walk can root into neighbour cells; no walk-through-walls and no
|
||||
/// "only one landblock loaded"). The streaming controller priority-applies that same ring,
|
||||
/// so it flips fast. An impossible claim (indoor cell id outside the dat's NumCells)
|
||||
/// worldReady for the TAS transit: is the player's teleport destination BOTH rendered
|
||||
/// and collidable so we can materialize? Retail crosses this edge after its blocking cell
|
||||
/// load. acdream's async equivalent must join the render publication barrier
|
||||
/// (<see cref="StreamingController.IsRenderNeighborhoodResident"/>) with physics
|
||||
/// residency. Indoor destinations require the center render landblock + EnvCell; outdoor
|
||||
/// destinations require both domains for the priority near ring. An impossible claim
|
||||
/// returns true so the TAS stops holding and the forced placement surfaces the failure
|
||||
/// loudly rather than holding forever.
|
||||
/// </summary>
|
||||
|
|
@ -7073,9 +7073,14 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
if (IsSpawnClaimUnhydratable(destCell)) return true;
|
||||
bool indoor = (destCell & 0xFFFFu) >= 0x0100u;
|
||||
int renderRadius = indoor ? 0 : TeleportNearRingRadius;
|
||||
bool renderReady = _streamingController?.IsRenderNeighborhoodResident(
|
||||
destCell,
|
||||
renderRadius) == true;
|
||||
return indoor
|
||||
? _physicsEngine.IsSpawnCellReady(destCell)
|
||||
: _physicsEngine.IsNeighborhoodTerrainResident(destCell, TeleportNearRingRadius);
|
||||
? renderReady && _physicsEngine.IsSpawnCellReady(destCell)
|
||||
: renderReady
|
||||
&& _physicsEngine.IsNeighborhoodTerrainResident(destCell, TeleportNearRingRadius);
|
||||
}
|
||||
|
||||
// The deferred snap (the original OnLivePositionUpdated steps 2-5), now run only
|
||||
|
|
@ -7560,6 +7565,13 @@ public sealed class GameWindow : IDisposable
|
|||
completedEnvCells);
|
||||
}
|
||||
|
||||
private void EnsureEnvCellMeshesAfterPin(
|
||||
AcDream.App.Rendering.Wb.EnvCellLandblockBuild build)
|
||||
{
|
||||
if (_wbMeshAdapter?.MeshManager is { } meshManager)
|
||||
AcDream.App.Rendering.Wb.EnvCellMeshPreparationScheduler.Schedule(build, meshManager);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pre-reads (under the worker's <c>_datLock</c>) every dat object
|
||||
/// <see cref="ApplyLoadedTerrainLocked"/> consumes, so the apply makes zero
|
||||
|
|
@ -7682,15 +7694,13 @@ public sealed class GameWindow : IDisposable
|
|||
(lbY - _liveCenterY) * 192f,
|
||||
0f);
|
||||
|
||||
// Per-landblock id namespace. Landblock IDs are formatted 0xXXYYFFFF
|
||||
// where XX = landblock X coord (bits 24-31), YY = Y coord (bits 16-23).
|
||||
// Both must go into our ID so landblocks don't collide.
|
||||
// Format: 0x80 | XX | YY | local_index(8 bits) = 0x80XXYY_II.
|
||||
// 256 slots per landblock is enough (SceneryGenerator caps ~200).
|
||||
// Per-landblock id namespace. The top nibble identifies procedural
|
||||
// scenery and the full X/Y bytes plus a 12-bit counter prevent dense
|
||||
// DAT-generated landblocks from overflowing into a neighbour's range.
|
||||
// See ProceduralSceneryIdAllocator for the audited 0x8XXYYIII layout.
|
||||
uint lbXByte = (lb.LandblockId >> 24) & 0xFFu;
|
||||
uint lbYByte = (lb.LandblockId >> 16) & 0xFFu;
|
||||
uint sceneryIdBase = 0x80000000u | (lbXByte << 16) | (lbYByte << 8);
|
||||
uint localIndex = 0;
|
||||
uint sceneryCounter = 0;
|
||||
|
||||
foreach (var spawn in spawns)
|
||||
{
|
||||
|
|
@ -7836,13 +7846,12 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
if (localIndex > 0xFFu)
|
||||
throw new InvalidDataException(
|
||||
$"Landblock 0x{lb.LandblockId:X8} exceeds the 256-entry procedural scenery id namespace.");
|
||||
|
||||
var hydrated = new AcDream.Core.World.WorldEntity
|
||||
{
|
||||
Id = sceneryIdBase + localIndex++,
|
||||
Id = AcDream.Core.World.ProceduralSceneryIdAllocator.Allocate(
|
||||
lbXByte,
|
||||
lbYByte,
|
||||
ref sceneryCounter),
|
||||
SourceGfxObjOrSetupId = spawn.ObjectId,
|
||||
Position = new System.Numerics.Vector3(localX, localY, finalZ) + lbOffset,
|
||||
Rotation = spawn.Rotation,
|
||||
|
|
@ -8135,6 +8144,31 @@ public sealed class GameWindow : IDisposable
|
|||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retire the Near-only presentation and collision layer while preserving
|
||||
/// the terrain slot/surface used by Far streaming. StreamingController calls
|
||||
/// this before GpuWorldState drops static entities so owner-based resources
|
||||
/// can be released exactly once.
|
||||
/// </summary>
|
||||
private void DemoteLoadedLandblockToTerrain(uint landblockId)
|
||||
{
|
||||
if (_worldState.TryGetLandblock(landblockId, out var landblock))
|
||||
{
|
||||
foreach (var entity in landblock!.Entities)
|
||||
{
|
||||
if (entity.ServerGuid != 0)
|
||||
continue;
|
||||
_lightingSink?.UnregisterOwner(entity.Id);
|
||||
_translucencyFades.ClearEntity(entity.Id);
|
||||
}
|
||||
}
|
||||
|
||||
_physicsEngine.DemoteLandblockToTerrain(landblockId);
|
||||
_cellVisibility.RemoveLandblock((landblockId >> 16) & 0xFFFFu);
|
||||
_buildingRegistries.Remove(landblockId & 0xFFFF0000u);
|
||||
_envCellRenderer?.RemoveLandblock(landblockId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase A.1 / A.5 T12: render-thread callback from StreamingController.Tick
|
||||
/// whenever a new landblock's terrain + entities are ready for GPU upload.
|
||||
|
|
@ -8568,7 +8602,7 @@ public sealed class GameWindow : IDisposable
|
|||
// 0xC0XXYY00+n layout per LandblockLoader.cs:55. Their BSP
|
||||
// collision covers the whole structure; the mesh-AABB-fallback
|
||||
// path below is for canopy-only-BSP procedural scenery
|
||||
// (0x80XXYY00+n) and produces a redundant 1.5m-clamped
|
||||
// (0x8XXYYIII) and produces a redundant 1.5m-clamped
|
||||
// invisible disc at the stab's mesh origin — the user-reported
|
||||
// "thin air" collision inside cottages. Gate the fallback to
|
||||
// exclude stabs. Spec:
|
||||
|
|
@ -10746,20 +10780,15 @@ public sealed class GameWindow : IDisposable
|
|||
&& _liveEntities?.ShouldAdvanceRootRuntime(serverGuid) == false)
|
||||
continue;
|
||||
|
||||
// Retail UpdatePositionInternal keeps a narrow Hidden path alive:
|
||||
// no PartArray animation and no physics integration, but
|
||||
// PositionManager::adjust_offset plus the manager tail still run.
|
||||
// Scripts and particles tick in their shared passes below.
|
||||
if (serverGuid != 0 && _liveEntities?.IsHidden(serverGuid) == true)
|
||||
{
|
||||
// Hidden suppresses the PartArray/mesh, not the effect owner.
|
||||
// Remote PositionManager composition already ran in the
|
||||
// unconditional live-entity pass; publish again for the local
|
||||
// player and for retained objects without remote motion.
|
||||
// Frozen part-local poses remain unchanged until UnHide.
|
||||
_effectPoses.UpdateRoot(ae.Entity);
|
||||
continue;
|
||||
}
|
||||
// Retail Hidden suppresses PartArray TIME ADVANCE, not part-pose
|
||||
// composition. set_hidden -> HandleEnterWorld can move CSequence's
|
||||
// cursor from a finished link/action to the first cyclic node; the
|
||||
// next hidden CPhysicsObj::UpdateObjectInternal (0x005156B0)
|
||||
// performs UpdatePositionInternal and then reaches set_frame ->
|
||||
// CPartArray::UpdateParts, publishing that newly-current pose before
|
||||
// the zero-time Hidden PES creates its silhouette particles.
|
||||
bool hidden = serverGuid != 0
|
||||
&& _liveEntities?.IsHidden(serverGuid) == true;
|
||||
|
||||
// ── Dead-reckoning: smooth position between UpdatePosition bursts.
|
||||
// The server broadcasts UpdatePosition at ~5-10Hz for distant
|
||||
|
|
@ -10774,7 +10803,8 @@ public sealed class GameWindow : IDisposable
|
|||
// runaway when the sequencer's velocity and the server's reality
|
||||
// disagree (e.g. server is rubber-banding the entity). Retail
|
||||
// uses a similar clamp at PhysicsObj::IsInterpolationComplete.
|
||||
if (ae.Sequencer is not null
|
||||
if (!hidden
|
||||
&& ae.Sequencer is not null
|
||||
&& serverGuid != 0
|
||||
&& serverGuid != _playerServerGuid
|
||||
&& _remoteDeadReckon.TryGetValue(serverGuid, out var rm)
|
||||
|
|
@ -10839,7 +10869,9 @@ public sealed class GameWindow : IDisposable
|
|||
rmDiag.LastSeqStateLogTime = nowSec;
|
||||
}
|
||||
}
|
||||
seqFrames = ae.Sequencer.Advance(dt);
|
||||
seqFrames = hidden
|
||||
? ae.Sequencer.SampleCurrentPose()
|
||||
: ae.Sequencer.Advance(dt);
|
||||
|
||||
// Capture hooks now, but deliver them only after every final
|
||||
// part transform and equipped-child root has been published.
|
||||
|
|
@ -10847,21 +10879,25 @@ public sealed class GameWindow : IDisposable
|
|||
// CPhysicsObj::UpdateChild (0x00512D50): a hand/weapon effect
|
||||
// reads this frame's pose, never the previous frame's pose.
|
||||
// AnimationDone/UseTime remain paired with the deferred drain.
|
||||
_animationHookFrames?.Capture(ae.Entity.Id, ae.Sequencer);
|
||||
if (!hidden)
|
||||
_animationHookFrames?.Capture(ae.Entity.Id, ae.Sequencer);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Legacy path (entities without a MotionTable / sequencer).
|
||||
int span = ae.HighFrame - ae.LowFrame;
|
||||
if (span <= 0) continue;
|
||||
ae.CurrFrame += dt * ae.Framerate;
|
||||
if (ae.CurrFrame > ae.HighFrame)
|
||||
if (span <= 0 && !hidden) continue;
|
||||
if (!hidden)
|
||||
{
|
||||
float over = ae.CurrFrame - ae.LowFrame;
|
||||
ae.CurrFrame = ae.LowFrame + (over % (span + 1));
|
||||
ae.CurrFrame += dt * ae.Framerate;
|
||||
if (ae.CurrFrame > ae.HighFrame)
|
||||
{
|
||||
float over = ae.CurrFrame - ae.LowFrame;
|
||||
ae.CurrFrame = ae.LowFrame + (over % (span + 1));
|
||||
}
|
||||
else if (ae.CurrFrame < ae.LowFrame)
|
||||
ae.CurrFrame = ae.LowFrame;
|
||||
}
|
||||
else if (ae.CurrFrame < ae.LowFrame)
|
||||
ae.CurrFrame = ae.LowFrame;
|
||||
}
|
||||
|
||||
int partCount = ae.PartTemplate.Count;
|
||||
|
|
@ -12357,8 +12393,8 @@ public sealed class GameWindow : IDisposable
|
|||
_streamer.Start();
|
||||
|
||||
_streamingController = new AcDream.App.Streaming.StreamingController(
|
||||
enqueueLoad: (id, kind) => _streamer.EnqueueLoad(id, kind),
|
||||
enqueueUnload: _streamer.EnqueueUnload,
|
||||
enqueueLoad: (id, kind, generation) => _streamer.EnqueueLoad(id, kind, generation),
|
||||
enqueueUnload: (id, generation) => _streamer.EnqueueUnload(id, generation),
|
||||
drainCompletions: _streamer.DrainCompletions,
|
||||
applyTerrain: ApplyLoadedTerrain,
|
||||
state: _worldState,
|
||||
|
|
@ -12381,7 +12417,9 @@ public sealed class GameWindow : IDisposable
|
|||
_cellVisibility.RemoveLandblock((id >> 16) & 0xFFFFu);
|
||||
_buildingRegistries.Remove(id & 0xFFFF0000u); // Phase A8 (key normalization fix 2026-05-28)
|
||||
_envCellRenderer?.RemoveLandblock(id); // Phase A8
|
||||
});
|
||||
},
|
||||
demoteNearLayer: DemoteLoadedLandblockToTerrain,
|
||||
ensureEnvCellMeshes: EnsureEnvCellMeshesAfterPin);
|
||||
_streamingController.MaxCompletionsPerFrame = newResolved.MaxCompletionsPerFrame;
|
||||
|
||||
Console.WriteLine(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue