feat(teleport C): TAS-driven fade transit + retire TeleportArrivalController
Wires the dormant TeleportAnimSequencer as the transit driver: on PlayerTeleport the player holds in PortalSpace behind a full-screen fade (FadeOverlay) until the destination terrain is resident (TeleportWorldReady, gated on the priority-applied landblock), then materializes (Place), and after the world fades back in regains control + acks the server (FireLoginComplete). No movement resolves against the empty world, so the outbound cell frame can't corrupt. Outdoor changes from place-immediately back to hold-until-resident (now fast, not a band-aid). - FadeOverlay: fullscreen NDC black quad, alpha = ShowTunnel ? 1 : FadeAlpha. - Retires TeleportArrivalController + its 2 tests (TAS subsumes the driver role). - Divergence register: AD-2 updated to the new mechanism; AD-31 (fade vs swirl). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9ac719424c
commit
3ce1fae332
7 changed files with 237 additions and 407 deletions
|
|
@ -171,6 +171,7 @@ public sealed class GameWindow : IDisposable
|
|||
// each frame on an indoor root (null on the outdoor root).
|
||||
private AcDream.App.Rendering.RetailPViewRenderer? _retailPViewRenderer;
|
||||
private AcDream.App.Rendering.PortalDepthMaskRenderer? _portalDepthMask;
|
||||
private AcDream.App.Rendering.FadeOverlay? _fadeOverlay; // teleport fade cover (spec C)
|
||||
private AcDream.App.Rendering.InteriorEntityPartition.Result? _interiorPartition;
|
||||
|
||||
// Phase U.3: the shared per-frame clip data (binding=2 mesh SSBO + terrain
|
||||
|
|
@ -2211,6 +2212,7 @@ public sealed class GameWindow : IDisposable
|
|||
// T1: invisible portal depth writes (seal/punch) — retail
|
||||
// DrawPortalPolyInternal (Ghidra 0x0059bc90).
|
||||
_portalDepthMask = new AcDream.App.Rendering.PortalDepthMaskRenderer(_gl);
|
||||
_fadeOverlay = new AcDream.App.Rendering.FadeOverlay(_gl);
|
||||
}
|
||||
|
||||
// Phase G.1 sky renderer — its own shader (sky.vert / sky.frag)
|
||||
|
|
@ -5462,29 +5464,44 @@ public sealed class GameWindow : IDisposable
|
|||
newWorldPos = worldPos;
|
||||
}
|
||||
|
||||
// G.3a: do NOT snap here. The destination dungeon landblock has not
|
||||
// streamed in yet; an immediate Resolve falls back to the resident
|
||||
// (old) landblocks and lands the player in ocean (#133). HOLD the snap
|
||||
// in portal space — TeleportArrivalController.Tick (per frame) places
|
||||
// the player via PlaceTeleportArrival once the destination cell
|
||||
// hydrates (TeleportArrivalReadiness == Ready), or force-places on an
|
||||
// impossible claim / timeout. PortalSpace keeps input frozen meanwhile.
|
||||
EnsureTeleportArrivalController();
|
||||
_pendingTeleportRot = rot;
|
||||
_teleportArrival!.BeginArrival(newWorldPos, p.LandblockId);
|
||||
// Retail "pink-bubble" transit: do NOT snap here. Record the destination and
|
||||
// PRIORITIZE its landblock in streaming so it applies ahead of the per-frame
|
||||
// budget (residency in ~hundreds of ms, not 10-14s). The TAS — ticked per frame
|
||||
// in OnUpdate — holds the player in PortalSpace behind the fade until the
|
||||
// destination terrain is resident (TeleportWorldReady), then fires Place. While
|
||||
// held, no movement resolve runs, so the outbound cell frame can't corrupt.
|
||||
_pendingTeleportRot = rot;
|
||||
_pendingTeleportPos = newWorldPos;
|
||||
_pendingTeleportCell = p.LandblockId;
|
||||
_teleportHoldFrames = 0;
|
||||
_teleportForced = false;
|
||||
if (_streamingController is not null)
|
||||
_streamingController.PriorityLandblockId =
|
||||
AcDream.App.Streaming.StreamingRegion.EncodeLandblockId(lbX, lbY);
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.LogTeleport(
|
||||
"AIM", p.LandblockId,
|
||||
$"lb={lbX},{lbY} indoor={((p.LandblockId & 0xFFFFu) >= 0x0100u)} diffLb={differentLandblock}");
|
||||
}
|
||||
}
|
||||
|
||||
// G.3a (#133): holds a teleport arrival in portal space until the destination
|
||||
// dungeon landblock/cell has hydrated, then places the player via the unchanged
|
||||
// validated-claim Resolve path. Lazily constructed on the first teleport (all
|
||||
// runtime deps are wired by then).
|
||||
private AcDream.App.World.TeleportArrivalController? _teleportArrival;
|
||||
// Retail teleport transit: the dormant 7-state TAS drives the fade cover, holds the
|
||||
// player in PortalSpace until the destination is resident (TeleportWorldReady), then
|
||||
// fires Place (materialize) and FireLoginComplete (regain control + ack the server).
|
||||
// Replaces the old TeleportArrivalController hold/place machine.
|
||||
private readonly AcDream.Core.World.TeleportAnimSequencer _teleportAnim = new();
|
||||
private bool _teleportInProgress;
|
||||
private System.Numerics.Vector3 _pendingTeleportPos;
|
||||
private uint _pendingTeleportCell;
|
||||
private int _teleportHoldFrames; // frames waiting for residency (safety-net timeout)
|
||||
private bool _teleportForced; // true when the safety-net timeout force-places
|
||||
private float _teleportFadeAlpha; // 0 = clear, 1 = full black; consumed by the fade overlay
|
||||
private System.Numerics.Quaternion _pendingTeleportRot = System.Numerics.Quaternion.Identity;
|
||||
|
||||
// ~10s at 60fps. Loud safety net for a destination that never streams (worker crash /
|
||||
// corrupt dat / OOB coords) — mirrors the retired controller's 600-frame ceiling. Now
|
||||
// rarely fires because priority-apply makes residency fast.
|
||||
private const int TeleportMaxHoldFrames = 600;
|
||||
|
||||
// #145: the LANDBLOCK-relative (cell-local) position used to SEED the player
|
||||
// body's cell-relative CellPosition. This is the ONE place the streaming center
|
||||
// (_liveCenter) is allowed to touch the physics frame — at the placement seam,
|
||||
|
|
@ -5501,35 +5518,21 @@ public sealed class GameWindow : IDisposable
|
|||
return worldPos - origin;
|
||||
}
|
||||
|
||||
private void EnsureTeleportArrivalController()
|
||||
/// <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 terrain landblock being
|
||||
/// registered (priority-applied). An impossible claim (indoor cell id outside the dat's
|
||||
/// NumCells) returns true so the TAS stops holding and the forced placement surfaces the
|
||||
/// failure loudly rather than holding forever.
|
||||
/// </summary>
|
||||
private bool TeleportWorldReady(uint destCell)
|
||||
{
|
||||
if (_teleportArrival is not null) return;
|
||||
_teleportArrival = new AcDream.App.World.TeleportArrivalController(
|
||||
readiness: TeleportArrivalReadiness,
|
||||
place: PlaceTeleportArrival);
|
||||
}
|
||||
|
||||
// Reuses the #107 login readiness triplet (GameWindow.cs:1010-1024), evaluated
|
||||
// against the teleport's (destPos, destCell): an impossible indoor claim short-
|
||||
// circuits to immediate placement; otherwise hold until terrain is sampled and,
|
||||
// for an indoor cell, the cell struct has hydrated.
|
||||
private AcDream.App.World.ArrivalReadiness TeleportArrivalReadiness(
|
||||
System.Numerics.Vector3 destPos, uint destCell)
|
||||
{
|
||||
bool claimUnhydratable = IsSpawnClaimUnhydratable(destCell);
|
||||
|
||||
// #135: an INDOOR destination (sealed dungeon / building interior) gates on the
|
||||
// EnvCell FLOOR hydrating. Retail places on the cell floor. An OUTDOOR destination
|
||||
// places immediately on the server-authoritative position — see TeleportArrivalRules
|
||||
// (#145: holding is futile because streaming doesn't progress during a PortalSpace
|
||||
// hold; the stale source landblock is dropped at recenter so the resolve trusts the
|
||||
// server cell until the destination streams in).
|
||||
if (IsSpawnClaimUnhydratable(destCell)) return true;
|
||||
bool indoor = (destCell & 0xFFFFu) >= 0x0100u;
|
||||
bool indoorCellReady = indoor && !claimUnhydratable
|
||||
&& _physicsEngine.IsSpawnCellReady(destCell);
|
||||
|
||||
return AcDream.App.World.TeleportArrivalRules.Decide(
|
||||
claimUnhydratable, indoor, indoorCellReady);
|
||||
return indoor
|
||||
? _physicsEngine.IsSpawnCellReady(destCell)
|
||||
: _physicsEngine.IsLandblockTerrainResident(destCell);
|
||||
}
|
||||
|
||||
// The deferred snap (the original OnLivePositionUpdated steps 2-5), now run only
|
||||
|
|
@ -5563,29 +5566,30 @@ public sealed class GameWindow : IDisposable
|
|||
contactPlaneNormal: System.Numerics.Vector3.UnitZ,
|
||||
dt: 1f / 60f);
|
||||
|
||||
_playerController.State = AcDream.App.Input.PlayerState.InWorld;
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.LogTeleport(
|
||||
"PLACED", resolved.CellId, $"forced={forced}");
|
||||
Console.WriteLine($"live: teleport complete — snapped to {snappedPos} cell=0x{resolved.CellId:X8}");
|
||||
|
||||
// Tell the server the client finished loading the new landblock (holtburger
|
||||
// client/messages.rs:434 — re-send LoginComplete after each portal transition).
|
||||
_liveSession?.SendGameAction(
|
||||
AcDream.Core.Net.Messages.GameActionLoginComplete.Build());
|
||||
// Do NOT flip to InWorld or send LoginComplete here — the player materializes BEHIND
|
||||
// the fade and stays input-frozen (PortalSpace) until the TAS fades the world back in
|
||||
// and fires FireLoginComplete (which regains control + acks the server). This is the
|
||||
// retail "pop out the other side" sequence.
|
||||
Console.WriteLine($"live: teleport materialized — snapped to {snappedPos} cell=0x{resolved.CellId:X8}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase B.3: fires when the server sends a PlayerTeleport (0xF751).
|
||||
/// Freeze movement input by setting the player controller to PortalSpace.
|
||||
/// The controller's Update() will return a zero-movement result until the
|
||||
/// destination UpdatePosition arrives and OnLivePositionUpdated resets the
|
||||
/// state to InWorld.
|
||||
/// Phase B.3: fires when the server sends a PlayerTeleport (0xF751). Freeze movement
|
||||
/// input (PortalSpace) and begin the retail fade transit. The per-frame TAS tick holds
|
||||
/// the player behind the fade until the destination is resident, then materializes them
|
||||
/// (Place) and, after the world fades back in, regains control + acks the server
|
||||
/// (FireLoginComplete).
|
||||
/// </summary>
|
||||
private void OnTeleportStarted(uint sequence)
|
||||
{
|
||||
if (_playerController is not null)
|
||||
_playerController.State = AcDream.App.Input.PlayerState.PortalSpace;
|
||||
EnsureTeleportArrivalController();
|
||||
_teleportInProgress = true;
|
||||
_teleportHoldFrames = 0;
|
||||
_teleportForced = false;
|
||||
_teleportAnim.Begin(AcDream.Core.World.TeleportEntryKind.Portal);
|
||||
Console.WriteLine($"live: teleport started (seq={sequence})");
|
||||
}
|
||||
|
||||
|
|
@ -7573,11 +7577,52 @@ public sealed class GameWindow : IDisposable
|
|||
// Step 2: routed through the controller; functionally identical.
|
||||
_liveSessionController?.Tick();
|
||||
|
||||
// G.3a (#133): advance any held teleport arrival. Runs AFTER streaming
|
||||
// (which applies the destination landblock) and the live-session drain
|
||||
// (which may have just called BeginArrival), so a destination that
|
||||
// hydrated this frame is placed the same frame.
|
||||
_teleportArrival?.Tick();
|
||||
// Retail teleport transit. Runs AFTER streaming (which priority-applies the
|
||||
// destination landblock this frame) and the live-session drain, so a destination
|
||||
// that became resident this frame materializes the same frame. The TAS holds at
|
||||
// Tunnel until worldReady, fires Place (materialize, hidden), then after the min-
|
||||
// continue + fades fires FireLoginComplete (regain control + ack). The fade overlay
|
||||
// is opaque black during the tunnel states (we render a fade, not the 3D swirl) and
|
||||
// ramps the world back in on WorldFadeIn.
|
||||
if (_teleportInProgress)
|
||||
{
|
||||
bool haveDest = _pendingTeleportCell != 0u;
|
||||
bool ready = haveDest && TeleportWorldReady(_pendingTeleportCell);
|
||||
if (haveDest && !ready && ++_teleportHoldFrames >= TeleportMaxHoldFrames)
|
||||
{
|
||||
ready = true;
|
||||
_teleportForced = true;
|
||||
}
|
||||
|
||||
var (snap, evts) = _teleportAnim.Tick((float)dt, ready);
|
||||
_teleportFadeAlpha = snap.ShowTunnel ? 1f : snap.FadeAlpha;
|
||||
|
||||
foreach (var e in evts)
|
||||
{
|
||||
switch (e)
|
||||
{
|
||||
case AcDream.Core.World.TeleportAnimEvent.Place:
|
||||
PlaceTeleportArrival(_pendingTeleportPos, _pendingTeleportCell, _teleportForced);
|
||||
if (_streamingController is not null)
|
||||
_streamingController.PriorityLandblockId = 0u;
|
||||
break;
|
||||
case AcDream.Core.World.TeleportAnimEvent.FireLoginComplete:
|
||||
if (_playerController is not null)
|
||||
_playerController.State = AcDream.App.Input.PlayerState.InWorld;
|
||||
// holtburger client/messages.rs:434 — re-send LoginComplete after
|
||||
// each portal transition.
|
||||
_liveSession?.SendGameAction(
|
||||
AcDream.Core.Net.Messages.GameActionLoginComplete.Build());
|
||||
_teleportInProgress = false;
|
||||
_pendingTeleportCell = 0u;
|
||||
_teleportFadeAlpha = 0f;
|
||||
break;
|
||||
default:
|
||||
// PlayEnterSound / EnterTunnel / PlayExitSound — audio polish deferred.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase K.1a — tick the input dispatcher so Hold-type bindings
|
||||
// re-fire while their chord is held. K.1b adds the subscribers
|
||||
|
|
@ -9019,6 +9064,11 @@ public sealed class GameWindow : IDisposable
|
|||
_uiHost.Draw(new System.Numerics.Vector2(_window!.Size.X, _window.Size.Y));
|
||||
}
|
||||
|
||||
// Teleport fade cover (retail-teleport spec C). Drawn AFTER the world + retail UI so
|
||||
// it covers them during a transit; the ImGui devtools below composite on top so they
|
||||
// stay visible for debugging. Alpha is 0 outside a teleport → Draw is a no-op.
|
||||
_fadeOverlay?.Draw(_teleportFadeAlpha);
|
||||
|
||||
// Phase D.2a — end ImGui frame. Runs AFTER all scene + debug draws
|
||||
// so ImGui composites on top. ImGuiController save/restores the
|
||||
// GL state it touches (blend, scissor, VAO, shader, texture); any
|
||||
|
|
@ -13072,6 +13122,7 @@ public sealed class GameWindow : IDisposable
|
|||
_wbDrawDispatcher?.Dispose();
|
||||
_envCellRenderer?.Dispose(); // Phase A8
|
||||
_portalDepthMask?.Dispose(); // T1
|
||||
_fadeOverlay?.Dispose();
|
||||
_clipFrame?.Dispose(); // Phase U.3
|
||||
_skyRenderer?.Dispose(); // depends on sampler cache; dispose first
|
||||
_samplerCache?.Dispose();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue