fix(world): complete recall before teleport hide
Match retail's update ordering so object animation, particles, and scripts advance before inbound teleport state is applied. Separate input-originated movement from post-network autonomous position output, and reconcile presentation without a second physics tick so recall cannot resume after arrival. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
dded9e6b17
commit
75acae02d6
13 changed files with 1068 additions and 332 deletions
|
|
@ -163,6 +163,10 @@ public sealed class GameWindow : IDisposable
|
|||
// once the injected shared helpers exist as method groups (GetSetupCylinder,
|
||||
// ApplyServerControlledVelocityCycle). See RemotePhysicsUpdater.
|
||||
private readonly AcDream.App.Physics.RemotePhysicsUpdater _remotePhysicsUpdater;
|
||||
private readonly AcDream.App.Input.LocalPlayerProjectionController _localPlayerProjection;
|
||||
private readonly AcDream.App.Input.LocalPlayerOutboundController _localPlayerOutbound;
|
||||
private readonly AcDream.App.Input.RetailLocalPlayerFrameController _localPlayerFrame;
|
||||
private readonly AcDream.App.World.RetailLiveFrameCoordinator _liveFrameCoordinator;
|
||||
private AcDream.App.Physics.RemoteTeleportController? _remoteTeleportController;
|
||||
// Step 7 projectile presentation. The controller owns no identity map;
|
||||
// each runtime component is stored on the canonical LiveEntityRecord.
|
||||
|
|
@ -1182,6 +1186,51 @@ public sealed class GameWindow : IDisposable
|
|||
// handler) stay on GameWindow and are injected as delegates.
|
||||
_remotePhysicsUpdater = new AcDream.App.Physics.RemotePhysicsUpdater(
|
||||
_physicsEngine, GetSetupCylinder, ApplyServerControlledVelocityCycle);
|
||||
_localPlayerProjection = new AcDream.App.Input.LocalPlayerProjectionController(
|
||||
resolveEntity: () =>
|
||||
_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var entity)
|
||||
? entity
|
||||
: null,
|
||||
liveCenterX: () => _liveCenterX,
|
||||
liveCenterY: () => _liveCenterY,
|
||||
syncShadow: (entity, cellId) => SyncLocalPlayerShadow(entity, cellId),
|
||||
rebucket: (serverGuid, landblockId) =>
|
||||
_liveEntities?.RebucketLiveEntity(serverGuid, landblockId));
|
||||
_localPlayerOutbound = new AcDream.App.Input.LocalPlayerOutboundController(
|
||||
YawToAcQuaternion,
|
||||
DumpMovementTruthOutbound);
|
||||
_localPlayerFrame = new AcDream.App.Input.RetailLocalPlayerFrameController(
|
||||
canPresentPlayer: CanAdvanceLocalPlayer,
|
||||
getController: () => _playerController,
|
||||
captureInput: () =>
|
||||
{
|
||||
_playerMouseDeltaX = 0f;
|
||||
return CaptureMovementInput();
|
||||
},
|
||||
resolveLocalEntityId: () =>
|
||||
_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var entity)
|
||||
? entity.Id
|
||||
: 0u,
|
||||
handleTargeting: () => _playerHost?.HandleTargetting(),
|
||||
isHidden: () => _liveEntities?.IsHidden(_playerServerGuid) == true,
|
||||
project: (controller, movement, hidden) =>
|
||||
_localPlayerProjection.Project(controller, movement, hidden),
|
||||
sendPreNetwork: (controller, movement, hidden) =>
|
||||
_localPlayerOutbound.SendPreNetworkActions(
|
||||
_liveSession,
|
||||
controller,
|
||||
movement,
|
||||
hidden),
|
||||
sendPostNetwork: (controller, hidden) =>
|
||||
_localPlayerOutbound.SendPostNetworkPosition(
|
||||
_liveSession,
|
||||
controller,
|
||||
hidden));
|
||||
_liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator(
|
||||
AdvanceLiveObjectRuntime,
|
||||
() => _liveSessionController?.Tick(),
|
||||
_localPlayerFrame.RunPostNetworkCommandPhase,
|
||||
ReconcileLiveObjectSpatialPresentation);
|
||||
}
|
||||
|
||||
public void Run()
|
||||
|
|
@ -7066,6 +7115,7 @@ public sealed class GameWindow : IDisposable
|
|||
// camera path re-extends the boom. Updating directly from the stale
|
||||
// source-world viewer made the destination bend across the reveal.
|
||||
_retailChaseCamera?.ResetViewerToPlayer(snappedPos, _playerController.Yaw);
|
||||
ReconcileLiveObjectSpatialPresentation();
|
||||
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.LogTeleport(
|
||||
"PLACED", resolved.CellId, $"forced={forced}");
|
||||
|
|
@ -8764,11 +8814,15 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
using var _updStage = _frameProfiler.BeginStage(AcDream.App.Diagnostics.FrameStage.Update);
|
||||
|
||||
double frameSeconds =
|
||||
AcDream.App.World.RetailLiveFrameCoordinator.NormalizeDeltaSeconds(dt);
|
||||
float frameDelta = (float)frameSeconds;
|
||||
|
||||
// Retail ScriptManager::AddScriptInternal (0x0051B310) stamps
|
||||
// Timer::cur_time at the packet/default-script call site. Publish this
|
||||
// update frame's clock before streaming and network dispatch, then
|
||||
// reuse the same timestamp for animation hooks and the later drain.
|
||||
_physicsScriptGameTime += Math.Max(0.0, dt);
|
||||
_physicsScriptGameTime += frameSeconds;
|
||||
_scriptRunner?.PublishTime(_physicsScriptGameTime);
|
||||
|
||||
// [FRAME-DIAG]: applies run later this frame inside _streamingController.Tick;
|
||||
|
|
@ -8955,12 +9009,64 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// Input callbacks feed the current object tick. Packets already read
|
||||
// by Core.Net remain queued until the retail object/physics phase has
|
||||
// consumed that input and published its final pose.
|
||||
_inputDispatcher?.Tick();
|
||||
|
||||
// Phase K.2 — mouse-look is an input source for this object's
|
||||
// movement tick, so sample it before the retail CPhysics/network
|
||||
// barrier just like held keyboard actions.
|
||||
if (_mouseLook is not null)
|
||||
{
|
||||
bool wantCaptureMouse = IsUiCapturingMouse();
|
||||
if (wantCaptureMouse != _lastWantCaptureMouse)
|
||||
{
|
||||
if (wantCaptureMouse && _mouseLook.Active)
|
||||
EndMouseLookAndRestoreCursor();
|
||||
_mouseLook.OnWantCaptureMouseChanged(wantCaptureMouse);
|
||||
_lastWantCaptureMouse = wantCaptureMouse;
|
||||
}
|
||||
|
||||
float nowSeconds = (float)(Environment.TickCount64 / 1000.0);
|
||||
if (_mouseLook.TryTakeRawSample(nowSeconds, out float rawX, out float rawY))
|
||||
{
|
||||
// GetInput synthesizes (0,0) only after >0.2 s idle.
|
||||
// MouseLookHandler stops drift before filtering that sample;
|
||||
// the filter tail may then start a smaller Rotate again.
|
||||
if (rawX == 0f && rawY == 0f)
|
||||
_playerController?.StopMouseDrift(CaptureMovementInput());
|
||||
|
||||
(float filteredX, float filteredY) =
|
||||
AcDream.Core.Rendering.CameraDiagnostics.UseRetailChaseCamera
|
||||
&& _retailChaseCamera is not null
|
||||
? _retailChaseCamera.FilterMouseDelta(
|
||||
rawX,
|
||||
rawY,
|
||||
weight: 0.5f,
|
||||
nowSec: nowSeconds)
|
||||
: (rawX, rawY);
|
||||
_mouseLook.ApplyDelta(filteredX, _sensChase);
|
||||
if (_retailChaseCamera is not null)
|
||||
_retailChaseCamera.AdjustPitch(filteredY * 0.003f * _sensChase);
|
||||
else
|
||||
_chaseCamera?.AdjustPitch(filteredY * 0.003f * _sensChase);
|
||||
}
|
||||
}
|
||||
|
||||
_combatAttackController?.Tick();
|
||||
|
||||
// Drain pending live-session traffic AFTER streaming so any incoming
|
||||
// CreateObject events find their landblock already loaded in
|
||||
// GpuWorldState. Non-blocking — returns immediately if no datagrams
|
||||
// are in the kernel buffer. Fires EntitySpawned events synchronously.
|
||||
// Step 2: routed through the controller; functionally identical.
|
||||
_liveSessionController?.Tick();
|
||||
// Retail SmartBox::UseTime (0x00455410) advances CObjectMaint and
|
||||
// CPhysics before it drains the inbound event queue. Keep the complete
|
||||
// live-object phase on that side of the barrier too. In particular,
|
||||
// the recall action retires before ACE's Hidden SetState freezes its
|
||||
// PartArray at the teleport boundary.
|
||||
_liveFrameCoordinator.Tick(frameDelta);
|
||||
|
||||
// Usually F751 activates immediately. This no-op convergence check
|
||||
// covers the session edge where the canonical player exists before
|
||||
|
|
@ -8980,7 +9086,7 @@ public sealed class GameWindow : IDisposable
|
|||
bool ready = haveDest && TeleportWorldReady(_pendingTeleportCell);
|
||||
if (haveDest && !ready)
|
||||
{
|
||||
_teleportHoldSeconds += (float)dt;
|
||||
_teleportHoldSeconds += frameDelta;
|
||||
if (_teleportHoldSeconds >= TeleportMaxHoldSeconds)
|
||||
{
|
||||
ready = true;
|
||||
|
|
@ -8989,7 +9095,7 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
|
||||
int tunnelFrame = _portalTunnel?.CurrentAnimationFrame ?? 0;
|
||||
var (snap, evts) = _teleportAnim.Tick((float)dt, ready, tunnelFrame);
|
||||
var (snap, evts) = _teleportAnim.Tick(frameDelta, ready, tunnelFrame);
|
||||
_teleportViewPlane.Update(snap);
|
||||
|
||||
foreach (var e in evts)
|
||||
|
|
@ -9027,62 +9133,16 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
_portalTunnel?.Tick((float)dt);
|
||||
_portalTunnel?.Tick(frameDelta);
|
||||
}
|
||||
|
||||
// Phase K.1a — tick the input dispatcher so Hold-type bindings
|
||||
// re-fire while their chord is held. K.1b adds the subscribers
|
||||
// that actually consume the events.
|
||||
_inputDispatcher?.Tick();
|
||||
_combatAttackController?.Tick();
|
||||
|
||||
// Phase D.5.3a — advance the selected-object overlay flash (0.25s green pulse
|
||||
// on selection, then revert). No-op when nothing is flashing.
|
||||
// Retained panel-local timers advance through RetailUiRuntime.Tick on the draw seam.
|
||||
|
||||
// Phase K.2 — re-evaluate WantCaptureMouse for the MMB
|
||||
// mouse-look state machine. Detect rising/falling edges so the
|
||||
// state suspends correctly when ImGui claims the cursor while
|
||||
// MMB is held (e.g. a tooltip pop-up over the cursor). When the
|
||||
// suspend deactivates an active session, restore the cursor so
|
||||
// it doesn't get stuck hidden under a panel.
|
||||
if (_mouseLook is not null)
|
||||
{
|
||||
bool wcm = IsUiCapturingMouse();
|
||||
if (wcm != _lastWantCaptureMouse)
|
||||
{
|
||||
if (wcm && _mouseLook.Active)
|
||||
EndMouseLookAndRestoreCursor();
|
||||
_mouseLook.OnWantCaptureMouseChanged(wcm);
|
||||
_lastWantCaptureMouse = wcm;
|
||||
}
|
||||
|
||||
float nowSeconds = (float)(Environment.TickCount64 / 1000.0);
|
||||
if (_mouseLook.TryTakeRawSample(nowSeconds, out float rawX, out float rawY))
|
||||
{
|
||||
// GetInput synthesizes (0,0) only after >0.2 s idle.
|
||||
// MouseLookHandler stops drift before filtering that sample;
|
||||
// the filter tail may then start a smaller Rotate again.
|
||||
if (rawX == 0f && rawY == 0f)
|
||||
_playerController?.StopMouseDrift(CaptureMovementInput());
|
||||
|
||||
(float filteredX, float filteredY) =
|
||||
AcDream.Core.Rendering.CameraDiagnostics.UseRetailChaseCamera
|
||||
&& _retailChaseCamera is not null
|
||||
? _retailChaseCamera.FilterMouseDelta(
|
||||
rawX,
|
||||
rawY,
|
||||
weight: 0.5f,
|
||||
nowSec: nowSeconds)
|
||||
: (rawX, rawY);
|
||||
_mouseLook.ApplyDelta(filteredX, _sensChase);
|
||||
if (_retailChaseCamera is not null)
|
||||
_retailChaseCamera.AdjustPitch(filteredY * 0.003f * _sensChase);
|
||||
else
|
||||
_chaseCamera?.AdjustPitch(filteredY * 0.003f * _sensChase);
|
||||
}
|
||||
}
|
||||
|
||||
// Phase K.2 — auto-enter player mode at login. The guard
|
||||
// returns true on the firing tick (one-shot); subsequent ticks
|
||||
// are no-ops. Skipped offline (no _liveSession → IsLiveInWorld
|
||||
|
|
@ -9116,7 +9176,7 @@ public sealed class GameWindow : IDisposable
|
|||
// delta drives fly heading instead).
|
||||
if (_inputDispatcher is null) return;
|
||||
_cameraController.Fly.Update(
|
||||
dt,
|
||||
frameSeconds,
|
||||
w: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementForward),
|
||||
a: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementTurnLeft),
|
||||
s: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementBackup),
|
||||
|
|
@ -9142,7 +9202,7 @@ public sealed class GameWindow : IDisposable
|
|||
// retail chase is selected; legacy camera ignores these.
|
||||
if (AcDream.Core.Rendering.CameraDiagnostics.UseRetailChaseCamera && _retailChaseCamera is not null)
|
||||
{
|
||||
float adj = AcDream.Core.Rendering.CameraDiagnostics.CameraAdjustmentSpeed * (float)dt;
|
||||
float adj = AcDream.Core.Rendering.CameraDiagnostics.CameraAdjustmentSpeed * frameDelta;
|
||||
if (_inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.CameraZoomIn))
|
||||
_retailChaseCamera.AdjustDistance(-adj);
|
||||
if (_inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.CameraZoomOut))
|
||||
|
|
@ -9161,82 +9221,17 @@ public sealed class GameWindow : IDisposable
|
|||
// until Q is pressed again. Handled in OnInputAction; here
|
||||
// we just OR _autoRunActive into the Forward flag.
|
||||
// * Mouse never drives character yaw (K.1b regression-prevention).
|
||||
var input = CaptureMovementInput();
|
||||
if (!_localPlayerFrame.TryGetPresentationAfterNetwork(out var playerFrame))
|
||||
return;
|
||||
|
||||
// Fix #42 (2026-05-05): keep PlayerMovementController's
|
||||
// LocalEntityId in sync with the live local player entity so
|
||||
// FindObjCollisions skips its own ShadowEntry. Re-fetched per
|
||||
// tick so re-spawns / character switches don't leave a stale
|
||||
// id on the controller. Pre-spawn or between-character it
|
||||
// stays 0 (no filter), which is harmless because there's no
|
||||
// ShadowEntry registered yet.
|
||||
_playerController.LocalEntityId =
|
||||
_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var localEnt)
|
||||
? localEnt.Id
|
||||
: 0u;
|
||||
|
||||
// R5-V2: tick the player's TargetManager BEFORE Update ticks
|
||||
// MoveToManager.UseTime (retail UpdateObjectInternal order). This is
|
||||
// the load-bearing call for creature-chase: the player, as a watched
|
||||
// target, pushes its position to its voyeurs (the NPCs moving-to it)
|
||||
// here — that push lands in each NPC's HandleUpdateTarget the same
|
||||
// frame, ahead of the NPCs' own UseTime in the per-remote loop
|
||||
// (which runs after this block). Replaces the AP-79 player poll.
|
||||
_playerHost?.HandleTargetting();
|
||||
|
||||
bool localPlayerHidden = _liveEntities?.IsHidden(_playerServerGuid) == true;
|
||||
var result = localPlayerHidden
|
||||
? _playerController.TickHidden((float)dt)
|
||||
: _playerController.Update((float)dt, input);
|
||||
|
||||
// Update the player entity's position + rotation so it renders at
|
||||
// the physics-resolved location each frame.
|
||||
if (_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var pe))
|
||||
AcDream.App.Input.MovementResult result = playerFrame.Movement;
|
||||
bool localPlayerHidden = playerFrame.Hidden;
|
||||
if (!playerFrame.AdvancedBeforeNetwork)
|
||||
{
|
||||
pe.SetPosition(result.RenderPosition); // A.5 T18: SetPosition propagates AabbDirty
|
||||
pe.ParentCellId = result.CellId;
|
||||
pe.Rotation = System.Numerics.Quaternion.CreateFromAxisAngle(
|
||||
System.Numerics.Vector3.UnitZ, _playerController.Yaw - MathF.PI / 2f);
|
||||
if (!localPlayerHidden)
|
||||
SyncLocalPlayerShadow(pe, result.CellId);
|
||||
|
||||
// Move the player entity to its current landblock in GpuWorldState
|
||||
// so it doesn't get frustum-culled when the player walks away from
|
||||
// the spawn landblock. Without this, the entity stays in the spawn
|
||||
// landblock's entity list and disappears when that landblock is culled.
|
||||
uint currentLb;
|
||||
if (result.CellId != 0 && (result.CellId & 0xFFFFu) >= 0x0100u)
|
||||
{
|
||||
// Indoor cell (dungeon/building EnvCell): the entity's landblock is
|
||||
// the CELL's landblock. Dungeon EnvCells sit at arbitrary "ocean"
|
||||
// world coords with negative local-Y, so floor(pp.Y/192) lands one
|
||||
// landblock off (the Bug-A class) — relocating the player into the
|
||||
// landblock the dungeon collapse unloaded, making the avatar
|
||||
// invisible. The cell id is authoritative.
|
||||
currentLb = (result.CellId & 0xFFFF0000u) | 0xFFFFu;
|
||||
}
|
||||
else
|
||||
{
|
||||
var pp = _playerController.Position;
|
||||
int plx = _liveCenterX + (int)System.Math.Floor(pp.X / 192f);
|
||||
int ply = _liveCenterY + (int)System.Math.Floor(pp.Y / 192f);
|
||||
currentLb = (uint)((plx << 24) | (ply << 16) | 0xFFFF);
|
||||
}
|
||||
// #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
|
||||
// (the old relocate path only scanned _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)
|
||||
_liveEntities?.RebucketLiveEntity(pe.ServerGuid, currentLb);
|
||||
// The player was materialized by this frame's inbound pass.
|
||||
// Its non-advancing initial projection must publish child and
|
||||
// effect anchors before the first draw too.
|
||||
ReconcileLiveObjectSpatialPresentation();
|
||||
}
|
||||
|
||||
// Update chase camera(s). The CameraController exposes whichever
|
||||
|
|
@ -9249,7 +9244,7 @@ public sealed class GameWindow : IDisposable
|
|||
// low-stiffness damping).
|
||||
_chaseCamera.Update(result.RenderPosition, _playerController.Yaw,
|
||||
isOnGround: result.IsOnGround,
|
||||
dt: (float)dt);
|
||||
dt: frameDelta);
|
||||
// RetailChaseCamera: heading is the player's facing direction
|
||||
// projected onto the contact plane when grounded, or the
|
||||
// world XY plane when airborne. The contact plane normal
|
||||
|
|
@ -9263,98 +9258,26 @@ public sealed class GameWindow : IDisposable
|
|||
playerVelocity: _playerController.BodyVelocity,
|
||||
isOnGround: result.IsOnGround,
|
||||
contactPlaneNormal: _playerController.ContactPlane.Normal,
|
||||
dt: (float)dt,
|
||||
dt: frameDelta,
|
||||
cellId: _playerController.CellId,
|
||||
selfEntityId: _playerController.LocalEntityId,
|
||||
trackedTargetPoint: trackedCombatTarget);
|
||||
|
||||
// Send outbound movement messages to the live server.
|
||||
if (_liveSession is not null
|
||||
&& !localPlayerHidden
|
||||
&& _playerController.TryGetOutboundPosition(
|
||||
YawToAcQuaternion(_playerController.Yaw),
|
||||
out uint wireCellId,
|
||||
out System.Numerics.Vector3 wirePos))
|
||||
{
|
||||
// Retail serializes CPhysicsObj::m_position directly. The
|
||||
// controller owns this canonical (cell, landblock-local origin)
|
||||
// pair; the render/streaming origin never enters wire coordinates.
|
||||
var wireRot = YawToAcQuaternion(_playerController.Yaw);
|
||||
byte contactByte = result.IsOnGround ? (byte)1 : (byte)0;
|
||||
var wirePosition = new AcDream.Core.Physics.Position(
|
||||
wireCellId,
|
||||
wirePos,
|
||||
wireRot);
|
||||
|
||||
// Snapshot the AP predicate before this path mutates its send
|
||||
// tracker. Retail's UseTime calls ShouldSendPositionEvent then
|
||||
// SendPositionEvent; MTS originates in separate input handlers,
|
||||
// so their relative same-tick callback order remains TS-33.
|
||||
bool positionEventDue = _playerController.ShouldSendPositionEvent(
|
||||
wirePosition,
|
||||
_playerController.ContactPlane,
|
||||
_playerController.SimTimeSeconds);
|
||||
|
||||
// 2026-05-16 (issue #75) / R4-V5: user-MoveToState packets
|
||||
// are ONLY for user-initiated motion intent — retail's
|
||||
// architectural split between user-input motion and
|
||||
// server-driven motion. Post-V5 the split holds BY
|
||||
// By construction, ShouldSendMovementEvent derives from user
|
||||
// input edges plus CameraSet's mouse-look start/stop/0.5-second
|
||||
// cadence. MoveToManager dispatches never touch either source,
|
||||
// so server-driven movement cannot echo a MoveToState. A user
|
||||
// edge cancels the moveto and legitimately takes control.
|
||||
if (result.ShouldSendMovementEvent)
|
||||
TrySendPlayerMovementEvent(result);
|
||||
|
||||
// SendPositionEvent itself admits only Contact+OnWalkable.
|
||||
if (positionEventDue && _playerController.CanSendPositionEvent)
|
||||
{
|
||||
var seq = _liveSession.NextGameActionSequence();
|
||||
var body = AcDream.Core.Net.Messages.AutonomousPosition.Build(
|
||||
gameActionSequence: seq,
|
||||
cellId: wireCellId,
|
||||
position: wirePos,
|
||||
rotation: wireRot,
|
||||
instanceSequence: _liveSession.InstanceSequence,
|
||||
serverControlSequence: _liveSession.ServerControlSequence,
|
||||
teleportSequence: _liveSession.TeleportSequence,
|
||||
forcePositionSequence: _liveSession.ForcePositionSequence,
|
||||
lastContact: contactByte);
|
||||
DumpMovementTruthOutbound(
|
||||
"AP", seq, result, wirePos, wireCellId, contactByte);
|
||||
_liveSession.SendGameAction(body);
|
||||
_playerController.NotePositionSent(
|
||||
position: wirePosition,
|
||||
contactPlane: _playerController.ContactPlane,
|
||||
nowSeconds: _playerController.SimTimeSeconds);
|
||||
}
|
||||
|
||||
if (result.JumpExtent.HasValue && result.JumpVelocity.HasValue)
|
||||
{
|
||||
// D4/L.2b (2026-06-30): JumpPack::Pack (0x00516d10) packs the
|
||||
// full Position, not an objectGuid/spellId — pass the same
|
||||
// wireCellId/wirePos/wireRot the MoveToState send above uses.
|
||||
var seq = _liveSession.NextGameActionSequence();
|
||||
var jumpBody = AcDream.Core.Net.Messages.JumpAction.Build(
|
||||
gameActionSequence: seq,
|
||||
extent: result.JumpExtent.Value,
|
||||
velocity: result.JumpVelocity.Value,
|
||||
cellId: wireCellId,
|
||||
position: wirePos,
|
||||
rotation: wireRot,
|
||||
instanceSequence: _liveSession.InstanceSequence,
|
||||
serverControlSequence: _liveSession.ServerControlSequence,
|
||||
teleportSequence: _liveSession.TeleportSequence,
|
||||
forcePositionSequence: _liveSession.ForcePositionSequence);
|
||||
_liveSession.SendGameAction(jumpBody);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the player entity's animation cycle to match current motion.
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanAdvanceLocalPlayer() =>
|
||||
_cameraController is not null
|
||||
&& _input is not null
|
||||
&& !_cameraController.IsFlyMode
|
||||
&& _playerMode
|
||||
&& _playerController is not null
|
||||
&& _chaseCamera is not null
|
||||
&& _inputDispatcher is not null
|
||||
&& !(DevToolsEnabled && ImGuiNET.ImGui.GetIO().WantCaptureKeyboard);
|
||||
|
||||
private void DumpMovementTruthOutbound(
|
||||
string kind,
|
||||
uint sequence,
|
||||
|
|
@ -9589,40 +9512,6 @@ public sealed class GameWindow : IDisposable
|
|||
if (DevToolsEnabled && _imguiBootstrap is not null)
|
||||
_imguiBootstrap.BeginFrame((float)deltaSeconds);
|
||||
|
||||
// Phase 6.4: advance per-entity animation playback before drawing
|
||||
// so the renderer always sees the up-to-date per-part transforms.
|
||||
// PhysicsScript timed hooks and animation hooks read the same current
|
||||
// retail update-frame clock published before network dispatch. Drain
|
||||
// the script pass later after final root/part poses are composed.
|
||||
// Re-publish without advancing so an extra render between updates still
|
||||
// retains retail's animation-hook versus object-hook phase barrier.
|
||||
_scriptRunner?.PublishTime(_physicsScriptGameTime);
|
||||
if (_liveEntities is { } liveEntities)
|
||||
{
|
||||
_remotePhysicsUpdater.TickHiddenEntities(
|
||||
liveEntities,
|
||||
_playerServerGuid,
|
||||
(float)deltaSeconds,
|
||||
entity => _effectPoses.UpdateRoot(entity));
|
||||
}
|
||||
_projectileController?.Tick(
|
||||
_physicsScriptGameTime,
|
||||
_liveCenterX,
|
||||
_liveCenterY,
|
||||
_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var projectileViewer)
|
||||
? projectileViewer.Position
|
||||
: null);
|
||||
if (_animatedEntities.Count > 0)
|
||||
TickAnimations((float)deltaSeconds);
|
||||
_equippedChildRenderer?.Tick();
|
||||
_animationHookFrames?.Drain();
|
||||
|
||||
// #188 — advance translucency fades UNCONDITIONALLY (not gated on
|
||||
// _animatedEntities.Count): a one-shot open-cycle animation can
|
||||
// finish and drop its entity from _animatedEntities before the
|
||||
// fade's own Time has elapsed, but the ramp must keep running.
|
||||
_translucencyFades.AdvanceAll((float)deltaSeconds);
|
||||
|
||||
// Phase G.1: weather state machine — deterministic per-day roll
|
||||
// + transitions + lightning flash.
|
||||
var cal = WorldTime.CurrentCalendar;
|
||||
|
|
@ -9784,12 +9673,6 @@ public sealed class GameWindow : IDisposable
|
|||
// debug-only and disabled for normal retail rendering.
|
||||
if (_options.EnableSkyPesDebug)
|
||||
UpdateSkyPes((float)WorldTime.DayFraction, _activeDayGroup, camPos, cameraInsideCell);
|
||||
_entityEffects?.RefreshLiveOwnerPoses();
|
||||
_scriptRunner?.Tick(_physicsScriptGameTime);
|
||||
_particleSink?.RefreshAttachedEmitters();
|
||||
_liveEntityLights?.Refresh();
|
||||
_particleSystem?.Tick((float)deltaSeconds);
|
||||
|
||||
// Phase G.1/G.2: feed the sun, tick LightManager, build + upload
|
||||
// the scene-lighting UBO once per frame. Every shader that
|
||||
// consumes binding=1 reads the same data for the rest of the
|
||||
|
|
@ -10737,6 +10620,71 @@ public sealed class GameWindow : IDisposable
|
|||
ReportCollisionEnd: () => _physicsEngine.ShadowObjects.Suspend(localEntityId)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advances the live object's retail UseTime phase before inbound packet
|
||||
/// dispatch. Final root/part poses and their animation hooks are published
|
||||
/// together so rendering consumes one coherent update snapshot.
|
||||
/// </summary>
|
||||
private void AdvanceLiveObjectRuntime(float dt)
|
||||
{
|
||||
// The local body participates in the same retail CPhysics phase as
|
||||
// remote objects. Its outbound movement snapshot is completed here,
|
||||
// before SmartBox dispatch can apply F751/ForcePosition.
|
||||
_localPlayerFrame.AdvanceBeforeNetwork(dt);
|
||||
_scriptRunner?.PublishTime(_physicsScriptGameTime);
|
||||
if (_liveEntities is { } liveEntities)
|
||||
{
|
||||
_remotePhysicsUpdater.TickHiddenEntities(
|
||||
liveEntities,
|
||||
_playerServerGuid,
|
||||
dt,
|
||||
entity => _effectPoses.UpdateRoot(entity));
|
||||
}
|
||||
|
||||
_projectileController?.Tick(
|
||||
_physicsScriptGameTime,
|
||||
_liveCenterX,
|
||||
_liveCenterY,
|
||||
_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var projectileViewer)
|
||||
? projectileViewer.Position
|
||||
: null);
|
||||
if (_animatedEntities.Count > 0)
|
||||
TickAnimations(dt);
|
||||
_equippedChildRenderer?.Tick();
|
||||
|
||||
// Advance already-active FP hooks before routing hooks emitted by
|
||||
// this PartArray update. A newly created translucency hook begins at
|
||||
// its authored Start value this frame; it does not consume dt twice.
|
||||
_translucencyFades.AdvanceAll(dt);
|
||||
_animationHookFrames?.Drain();
|
||||
_entityEffects?.RefreshLiveOwnerPoses();
|
||||
_particleSink?.RefreshAttachedEmitters();
|
||||
_liveEntityLights?.Refresh();
|
||||
|
||||
// Retail CPhysicsObj::UpdateObjectInternal (0x005156B0) advances
|
||||
// ParticleManager before ScriptManager. A particle created by a PES
|
||||
// hook therefore begins simulation on the following object frame.
|
||||
_particleSystem?.Tick(dt);
|
||||
_scriptRunner?.Tick(_physicsScriptGameTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-composes spatial derivatives after authoritative packet handlers
|
||||
/// move roots. This pass advances no time, animation, scripts, particles,
|
||||
/// or fades; it only makes children and live anchors agree with the root
|
||||
/// that the same frame will draw.
|
||||
/// </summary>
|
||||
private void ReconcileLiveObjectSpatialPresentation()
|
||||
{
|
||||
// Authoritative handlers mutate WorldEntity roots directly. Publish
|
||||
// those roots before children read their parent's pose; child Tick
|
||||
// then publishes each newly composed child pose for attached effects.
|
||||
_entityEffects?.RefreshLiveOwnerPoses();
|
||||
_equippedChildRenderer?.Tick();
|
||||
_particleSink?.RefreshAttachedEmitters();
|
||||
_liveEntityLights?.Refresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase 6.4: advance every animated entity's frame counter by
|
||||
/// <paramref name="dt"/> * Framerate, wrapping around the cycle's
|
||||
|
|
@ -12921,79 +12869,14 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
|
||||
internal static AcDream.Core.Physics.RawMotionState BuildOutboundRawMotionState(
|
||||
AcDream.App.Input.MovementResult result)
|
||||
{
|
||||
var axisHoldKey = result.IsRunning
|
||||
? AcDream.Core.Physics.HoldKey.Run
|
||||
: AcDream.Core.Physics.HoldKey.None;
|
||||
return new AcDream.Core.Physics.RawMotionState
|
||||
{
|
||||
CurrentHoldKey = axisHoldKey,
|
||||
ForwardCommand = result.ForwardCommand
|
||||
?? AcDream.Core.Physics.RawMotionState.Default.ForwardCommand,
|
||||
ForwardHoldKey = result.ForwardCommand.HasValue
|
||||
? axisHoldKey : AcDream.Core.Physics.HoldKey.Invalid,
|
||||
ForwardSpeed = result.ForwardSpeed
|
||||
?? AcDream.Core.Physics.RawMotionState.Default.ForwardSpeed,
|
||||
SidestepCommand = result.SidestepCommand
|
||||
?? AcDream.Core.Physics.RawMotionState.Default.SidestepCommand,
|
||||
SidestepHoldKey = result.SidestepCommand.HasValue
|
||||
? result.SidestepUsesRunHold
|
||||
? AcDream.Core.Physics.HoldKey.Run
|
||||
: axisHoldKey
|
||||
: AcDream.Core.Physics.HoldKey.Invalid,
|
||||
SidestepSpeed = result.SidestepSpeed
|
||||
?? AcDream.Core.Physics.RawMotionState.Default.SidestepSpeed,
|
||||
TurnCommand = result.TurnCommand
|
||||
?? AcDream.Core.Physics.RawMotionState.Default.TurnCommand,
|
||||
TurnHoldKey = result.TurnCommand.HasValue
|
||||
? result.TurnUsesRunHold
|
||||
? AcDream.Core.Physics.HoldKey.Run
|
||||
: axisHoldKey
|
||||
: AcDream.Core.Physics.HoldKey.Invalid,
|
||||
TurnSpeed = result.TurnSpeed
|
||||
?? AcDream.Core.Physics.RawMotionState.Default.TurnSpeed,
|
||||
};
|
||||
}
|
||||
AcDream.App.Input.MovementResult result) =>
|
||||
AcDream.App.Input.LocalPlayerOutboundController.BuildRawMotionState(result);
|
||||
|
||||
private bool TrySendPlayerMovementEvent(AcDream.App.Input.MovementResult result)
|
||||
{
|
||||
if (_liveSession is not { } session
|
||||
|| _playerController is not { } controller)
|
||||
return false;
|
||||
|
||||
var wireRot = YawToAcQuaternion(controller.Yaw);
|
||||
if (!controller.TryGetOutboundPosition(
|
||||
wireRot,
|
||||
out uint wireCellId,
|
||||
out System.Numerics.Vector3 wirePos))
|
||||
return false;
|
||||
|
||||
byte contactByte = result.IsOnGround ? (byte)1 : (byte)0;
|
||||
AcDream.Core.Physics.RawMotionState rawMotionState =
|
||||
BuildOutboundRawMotionState(result);
|
||||
|
||||
uint seq = session.NextGameActionSequence();
|
||||
byte[] body = AcDream.Core.Net.Messages.MoveToState.Build(
|
||||
gameActionSequence: seq,
|
||||
rawMotionState: rawMotionState,
|
||||
cellId: wireCellId,
|
||||
position: wirePos,
|
||||
rotation: wireRot,
|
||||
instanceSequence: session.InstanceSequence,
|
||||
serverControlSequence: session.ServerControlSequence,
|
||||
teleportSequence: session.TeleportSequence,
|
||||
forcePositionSequence: session.ForcePositionSequence,
|
||||
contact: contactByte != 0,
|
||||
standingLongjump: false);
|
||||
DumpMovementTruthOutbound(
|
||||
"MTS", seq, result, wirePos, wireCellId, contactByte);
|
||||
session.SendGameAction(body);
|
||||
controller.NoteMovementSent(
|
||||
controller.SimTimeSeconds,
|
||||
result.IsMouseLookMovementEvent);
|
||||
return true;
|
||||
}
|
||||
=> _localPlayerOutbound.TrySendMovement(
|
||||
_liveSession,
|
||||
_playerController,
|
||||
result);
|
||||
|
||||
private uint? GetSelectedOrClosestCombatTarget()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue