feat(vfx): port retail hidden and teleport presentation
Preserve canonical live-object ownership across Hidden transitions and remote teleport placement so effects, collision, streaming, and targeting remain synchronized.
This commit is contained in:
parent
a51ebc66e9
commit
1e98d81448
46 changed files with 4883 additions and 127 deletions
|
|
@ -159,6 +159,7 @@ 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 AcDream.App.Physics.RemoteTeleportController? _remoteTeleportController;
|
||||
// Step 7 projectile presentation. The controller owns no identity map;
|
||||
// each runtime component is stored on the canonical LiveEntityRecord.
|
||||
private AcDream.App.Physics.ProjectileController? _projectileController;
|
||||
|
|
@ -460,10 +461,9 @@ public sealed class GameWindow : IDisposable
|
|||
/// </para>
|
||||
/// </summary>
|
||||
internal sealed class RemoteMotion :
|
||||
AcDream.App.World.ILiveEntityRemoteMotionRuntime,
|
||||
AcDream.App.World.ILiveEntityCanonicalCellConsumer // internal: the R2-Q5 sink callbacks (ObservedOmega seam) capture it
|
||||
AcDream.App.World.ILiveEntityRemotePlacementRuntime // internal: the R2-Q5 sink callbacks (ObservedOmega seam) capture it
|
||||
{
|
||||
public AcDream.Core.Physics.PhysicsBody Body;
|
||||
public AcDream.Core.Physics.PhysicsBody Body { get; }
|
||||
AcDream.Core.Physics.PhysicsBody AcDream.App.World.ILiveEntityRemoteMotionRuntime.Body => Body;
|
||||
|
||||
/// <summary>R5-V5: retail <c>CPhysicsObj::movement_manager</c> — the
|
||||
|
|
@ -613,6 +613,36 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
System.Numerics.Vector3 AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastServerPosition
|
||||
{
|
||||
get => LastServerPos;
|
||||
set => LastServerPos = value;
|
||||
}
|
||||
|
||||
double AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastServerPositionTime
|
||||
{
|
||||
get => LastServerPosTime;
|
||||
set => LastServerPosTime = value;
|
||||
}
|
||||
|
||||
System.Numerics.Vector3 AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastShadowSyncPosition
|
||||
{
|
||||
get => LastShadowSyncPos;
|
||||
set => LastShadowSyncPos = value;
|
||||
}
|
||||
|
||||
bool AcDream.App.World.ILiveEntityRemotePlacementRuntime.Airborne
|
||||
{
|
||||
get => Airborne;
|
||||
set => Airborne = value;
|
||||
}
|
||||
|
||||
void AcDream.App.World.ILiveEntityRemotePlacementRuntime.HitGround() =>
|
||||
Movement.HitGround();
|
||||
|
||||
void AcDream.App.World.ILiveEntityRemotePlacementRuntime.LeaveGround() =>
|
||||
Motion.LeaveGround();
|
||||
|
||||
/// <summary>
|
||||
/// K-fix9 (2026-04-26): true while the remote is airborne (jump
|
||||
/// arc in flight). Set when a 0xF74E VectorUpdate arrives with
|
||||
|
|
@ -857,6 +887,7 @@ public sealed class GameWindow : IDisposable
|
|||
// from the animation pipeline flip the matching LightSource.IsLit.
|
||||
private AcDream.Core.Lighting.LightingHookSink? _lightingSink;
|
||||
private AcDream.App.Rendering.Vfx.LiveEntityLightController? _liveEntityLights;
|
||||
private AcDream.App.World.LiveEntityPresentationController? _liveEntityPresentation;
|
||||
|
||||
// #188 — TransparentPartHook fires from the animation pipeline drive
|
||||
// a per-(entity,part) translucency ramp; WbDrawDispatcher reads it
|
||||
|
|
@ -2173,7 +2204,8 @@ public sealed class GameWindow : IDisposable
|
|||
playerCellId: () => _playerController?.CellId ?? 0u,
|
||||
selectedGuid: () => _selection.SelectedObjectId,
|
||||
coordinatesOnRadar: () => _persistedGameplay.CoordinatesOnRadar,
|
||||
uiLocked: () => _persistedGameplay.LockUI);
|
||||
uiLocked: () => _persistedGameplay.LockUI,
|
||||
playerEntities: _entitiesByServerGuid);
|
||||
var retailChatVm = new AcDream.UI.Abstractions.Panels.Chat.ChatVM(Chat, displayLimit: 200);
|
||||
_retailChatVm = retailChatVm;
|
||||
AcDream.App.UI.RetailUiPersistenceBindings? persistence = _settingsStore is null
|
||||
|
|
@ -2444,7 +2476,6 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
DiagnosticSink = message => Console.Error.WriteLine($"projectile: {message}"),
|
||||
};
|
||||
|
||||
_liveEntityLights = new AcDream.App.Rendering.Vfx.LiveEntityLightController(
|
||||
_liveEntities,
|
||||
_effectPoses,
|
||||
|
|
@ -2479,9 +2510,34 @@ public sealed class GameWindow : IDisposable
|
|||
_entityEffects = entityEffects;
|
||||
entityEffects.DiagnosticSink = message =>
|
||||
Console.Error.WriteLine($"vfx: {message}");
|
||||
_liveEntityPresentation = new AcDream.App.World.LiveEntityPresentationController(
|
||||
_liveEntities,
|
||||
_physicsEngine.ShadowObjects,
|
||||
entityEffects.PlayTyped,
|
||||
_equippedChildRenderer.SetDirectChildrenNoDraw,
|
||||
ClearTargetForHiddenEntity,
|
||||
() => (_liveCenterX, _liveCenterY));
|
||||
_remoteTeleportController = new AcDream.App.Physics.RemoteTeleportController(
|
||||
_physicsEngine,
|
||||
_liveEntities,
|
||||
GetSetupCylinder,
|
||||
CellLocalForSeed,
|
||||
(entity, remote, cellId) => _remotePhysicsUpdater.SyncRemoteShadowToBody(
|
||||
entity.Id,
|
||||
remote,
|
||||
_liveCenterX,
|
||||
_liveCenterY,
|
||||
cellId),
|
||||
(guid, generation, deferShadowRestore) =>
|
||||
_liveEntityPresentation.CompleteAuthoritativePlacement(
|
||||
guid,
|
||||
generation,
|
||||
deferShadowRestore),
|
||||
(guid, generation) =>
|
||||
_liveEntityPresentation.BeginAuthoritativePlacement(guid, generation));
|
||||
_equippedChildRenderer.EntityReady += guid =>
|
||||
{
|
||||
entityEffects.OnLiveEntityReady(guid);
|
||||
CompleteLiveEntityReady(guid);
|
||||
};
|
||||
_equippedChildRenderer.ProjectionPoseReady += guid =>
|
||||
_liveEntityLights.OnAttachedPoseReady(guid);
|
||||
|
|
@ -2704,16 +2760,33 @@ public sealed class GameWindow : IDisposable
|
|||
_equippedChildRenderer?.Clear();
|
||||
Objects.Clear();
|
||||
_selection.Reset();
|
||||
_pendingPostArrivalAction = null;
|
||||
try
|
||||
{
|
||||
_liveEntities?.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
// F754/F755 can precede CreateObject, so the mixed pending FIFO
|
||||
// may contain owners with no LiveEntityRecord to tear down.
|
||||
_entityEffects?.ClearNetworkState();
|
||||
_animationHookFrames?.Clear();
|
||||
try
|
||||
{
|
||||
// A pending teleport is operational state outside the live
|
||||
// record. Clear it independently even if a resource callback
|
||||
// failed while the canonical runtime was draining.
|
||||
_remoteTeleportController?.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
// F754/F755 can precede CreateObject, so the mixed pending FIFO
|
||||
// may contain owners with no LiveEntityRecord to tear down.
|
||||
_entityEffects?.ClearNetworkState();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_animationHookFrames?.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4096,7 +4169,7 @@ public sealed class GameWindow : IDisposable
|
|||
// Renderer, script owner, optional animation owner, collision body,
|
||||
// and effect profile are now all installed. Only at this boundary may
|
||||
// the mixed pre-Create F754/F755 FIFO replay against the local ID.
|
||||
_entityEffects?.OnLiveEntityReady(spawn.Guid);
|
||||
CompleteLiveEntityReady(spawn.Guid);
|
||||
|
||||
// Dump a summary periodically so we can see drop breakdowns without
|
||||
// waiting for a graceful shutdown.
|
||||
|
|
@ -4468,7 +4541,9 @@ public sealed class GameWindow : IDisposable
|
|||
if (spawn.ItemType == (uint)AcDream.Core.Items.ItemType.Creature)
|
||||
flags |= AcDream.Core.Physics.EntityCollisionFlags.IsCreature;
|
||||
|
||||
uint state = spawn.PhysicsState ?? 0u;
|
||||
uint state = _liveEntities?.TryGetRecord(spawn.Guid, out LiveEntityRecord liveRecord) == true
|
||||
? (uint)liveRecord.FinalPhysicsState
|
||||
: spawn.PhysicsState ?? 0u;
|
||||
|
||||
_physicsEngine.ShadowObjects.RegisterMultiPart(
|
||||
entityId: entity.Id,
|
||||
|
|
@ -4596,6 +4671,13 @@ public sealed class GameWindow : IDisposable
|
|||
uint cellId,
|
||||
bool force = false)
|
||||
{
|
||||
if (_liveEntities?.IsHidden(_playerServerGuid) == true)
|
||||
{
|
||||
_physicsEngine.ShadowObjects.Suspend(playerEntity.Id);
|
||||
_lastLocalPlayerShadow = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!force
|
||||
&& _lastLocalPlayerShadow is { } last
|
||||
&& last.CellId == cellId
|
||||
|
|
@ -4836,17 +4918,20 @@ public sealed class GameWindow : IDisposable
|
|||
/// </summary>
|
||||
private AcDream.Core.Physics.Motion.IPhysicsObjHost? ResolvePhysicsHost(uint id)
|
||||
{
|
||||
// CObjCell::hide_object removes Hidden objects from the lookup surface
|
||||
// used to establish new target/voyeur relationships. Existing
|
||||
// subscriptions are cleared by LiveEntityPresentationController.
|
||||
if (!_visibleEntitiesByServerGuid.ContainsKey(id))
|
||||
return null;
|
||||
if (_physicsHosts.TryGetValue(id, out var existing))
|
||||
return existing;
|
||||
if (!_entitiesByServerGuid.ContainsKey(id))
|
||||
return null;
|
||||
|
||||
double NowSeconds() => (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
|
||||
var minimal = new EntityPhysicsHost(
|
||||
id,
|
||||
getPosition: () => new AcDream.Core.Physics.Position(
|
||||
0u,
|
||||
_entitiesByServerGuid.TryGetValue(id, out var e)
|
||||
_visibleEntitiesByServerGuid.TryGetValue(id, out var e)
|
||||
? e.Position : System.Numerics.Vector3.Zero,
|
||||
System.Numerics.Quaternion.Identity),
|
||||
getVelocity: () => System.Numerics.Vector3.Zero, // static target
|
||||
|
|
@ -4914,20 +4999,63 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
if (host is null)
|
||||
return;
|
||||
if (!_entitiesByServerGuid.TryGetValue(targetGuid, out var tgtEnt))
|
||||
if (_liveEntities is not { } liveEntities
|
||||
|| !liveEntities.TryGetInteractionEligibleEntity(targetGuid, out var tgtEnt))
|
||||
return;
|
||||
var (radius, height) = GetSetupCylinder(targetGuid, tgtEnt);
|
||||
host.PositionManager.StickTo(targetGuid, radius, height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes retail object construction in order: register the effect
|
||||
/// owner, apply constructor/PhysicsDesc state transitions, then replay
|
||||
/// SmartBox's queued F754/F755 blobs.
|
||||
/// </summary>
|
||||
private void CompleteLiveEntityReady(uint serverGuid)
|
||||
{
|
||||
if (_entityEffects?.PrepareLiveEntityOwner(serverGuid) != true)
|
||||
return;
|
||||
_liveEntityPresentation?.OnLiveEntityReady(serverGuid);
|
||||
_entityEffects.ReplayPendingForLiveEntity(serverGuid);
|
||||
}
|
||||
|
||||
private void ClearTargetForHiddenEntity(uint serverGuid)
|
||||
{
|
||||
if (_pendingPostArrivalAction is { Guid: var pendingGuid }
|
||||
&& pendingGuid == serverGuid)
|
||||
_pendingPostArrivalAction = null;
|
||||
|
||||
if (_selection.SelectedObjectId == serverGuid)
|
||||
{
|
||||
_selection.Clear(
|
||||
AcDream.Core.Selection.SelectionChangeSource.System,
|
||||
AcDream.Core.Selection.SelectionChangeReason.Cleared);
|
||||
}
|
||||
|
||||
if (_physicsHosts.TryGetValue(serverGuid, out var hiddenHost)
|
||||
&& hiddenHost is EntityPhysicsHost hiddenEntityHost)
|
||||
hiddenEntityHost.NotifyHidden();
|
||||
}
|
||||
|
||||
private void TearDownLiveEntityRuntimeComponents(LiveEntityRecord record)
|
||||
{
|
||||
_entityEffects?.OnLiveEntityUnregistered(record);
|
||||
uint serverGuid = record.ServerGuid;
|
||||
var cleanups = new List<Action>
|
||||
{
|
||||
() => _liveEntityPresentation?.Forget(record),
|
||||
() => _entityEffects?.OnLiveEntityUnregistered(record),
|
||||
() => _remoteTeleportController?.Forget(serverGuid),
|
||||
};
|
||||
if (_pendingPostArrivalAction is { Guid: var pendingGuid }
|
||||
&& pendingGuid == serverGuid)
|
||||
_pendingPostArrivalAction = null;
|
||||
|
||||
if (record.WorldEntity is not { } existingEntity)
|
||||
{
|
||||
AcDream.App.World.LiveEntityTeardown.Run(cleanups);
|
||||
return;
|
||||
}
|
||||
|
||||
uint serverGuid = record.ServerGuid;
|
||||
// R2-Q5 + R3-W2: retail runs BOTH layers' exit-world drains
|
||||
// independently (r3-port-plan §4): the manager's (each pending
|
||||
// animation fires MotionDone(success:false) → the bound interp pops
|
||||
|
|
@ -4935,9 +5063,9 @@ public sealed class GameWindow : IDisposable
|
|||
// MovementManager::HandleExitWorld 0x00524350, the R5-V5 facade
|
||||
// relay: interp ONLY → CMotionInterp::HandleExitWorld 0x00527f30).
|
||||
if (_animatedEntities.TryGetValue(existingEntity.Id, out var aeGone))
|
||||
aeGone.Sequencer?.Manager.HandleExitWorld();
|
||||
cleanups.Add(() => aeGone.Sequencer?.Manager.HandleExitWorld());
|
||||
if (_remoteDeadReckon.TryGetValue(serverGuid, out var rmGone))
|
||||
rmGone.Movement.HandleExitWorld();
|
||||
cleanups.Add(rmGone.Movement.HandleExitWorld);
|
||||
// R5-V2: retail CPhysicsObj::exit_world tells every voyeur of this
|
||||
// entity that it left the world (TargetManager::NotifyVoyeurOfEvent
|
||||
// ExitWorld) — a watcher moving-to/stuck-to this entity drops the
|
||||
|
|
@ -4956,31 +5084,35 @@ public sealed class GameWindow : IDisposable
|
|||
// (ExitWorld) tells the entities watching IT. Without the first
|
||||
// two, a despawning attacker leaves a dead voyeur entry on its
|
||||
// target until send-failure pruning.
|
||||
ephGone.PositionManager.UnStick();
|
||||
ephGone.ClearTarget();
|
||||
ephGone.NotifyExitWorld();
|
||||
cleanups.Add(ephGone.PositionManager.UnStick);
|
||||
cleanups.Add(ephGone.ClearTarget);
|
||||
cleanups.Add(ephGone.NotifyExitWorld);
|
||||
}
|
||||
_physicsHosts.Remove(serverGuid);
|
||||
_animatedEntities.Remove(existingEntity.Id);
|
||||
_classificationCache.InvalidateEntity(existingEntity.Id);
|
||||
cleanups.Add(() => _physicsHosts.Remove(serverGuid));
|
||||
cleanups.Add(() => _animatedEntities.Remove(existingEntity.Id));
|
||||
cleanups.Add(() => _classificationCache.InvalidateEntity(existingEntity.Id));
|
||||
|
||||
// Dead-reckon state is keyed by SERVER guid (not local id) so we
|
||||
// clear using the same guid the next spawn/update would use.
|
||||
_remoteDeadReckon.Remove(serverGuid);
|
||||
_remoteLastMove.Remove(serverGuid);
|
||||
if (_selection.SelectedObjectId == serverGuid)
|
||||
cleanups.Add(() => _remoteDeadReckon.Remove(serverGuid));
|
||||
cleanups.Add(() => _remoteLastMove.Remove(serverGuid));
|
||||
cleanups.Add(() =>
|
||||
{
|
||||
_selection.Clear(
|
||||
AcDream.Core.Selection.SelectionChangeSource.System,
|
||||
AcDream.Core.Selection.SelectionChangeReason.SelectedObjectRemoved);
|
||||
}
|
||||
if (_selection.SelectedObjectId == serverGuid)
|
||||
{
|
||||
_selection.Clear(
|
||||
AcDream.Core.Selection.SelectionChangeSource.System,
|
||||
AcDream.Core.Selection.SelectionChangeReason.SelectedObjectRemoved);
|
||||
}
|
||||
});
|
||||
|
||||
_translucencyFades.ClearEntity(existingEntity.Id); // #188
|
||||
LeaveWorldLiveEntityRuntimeComponents(record);
|
||||
cleanups.Add(() => _translucencyFades.ClearEntity(existingEntity.Id)); // #188
|
||||
cleanups.Add(() => LeaveWorldLiveEntityRuntimeComponents(record));
|
||||
// Logical teardown ends the retained shadow payload too. The weaker
|
||||
// pickup/parent path stops after Suspend so re-entry can restore it.
|
||||
_physicsEngine.ShadowObjects.Deregister(existingEntity.Id);
|
||||
_liveEntityLights?.Forget(existingEntity.Id);
|
||||
cleanups.Add(() => _physicsEngine.ShadowObjects.Deregister(existingEntity.Id));
|
||||
cleanups.Add(() => _liveEntityLights?.Forget(existingEntity.Id));
|
||||
AcDream.App.World.LiveEntityTeardown.Run(cleanups);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -5075,7 +5207,8 @@ public sealed class GameWindow : IDisposable
|
|||
// MoveToPosition at the wire origin (§2f).
|
||||
if (update.MotionState.MovementType == 6
|
||||
&& path.TargetGuid is { } tgtGuid
|
||||
&& _entitiesByServerGuid.TryGetValue(tgtGuid, out var tgtEnt))
|
||||
&& _liveEntities is { } liveMoveEntities
|
||||
&& liveMoveEntities.TryGetInteractionEligibleEntity(tgtGuid, out var tgtEnt))
|
||||
{
|
||||
ms.Type = AcDream.Core.Physics.MovementType.MoveToObject;
|
||||
ms.ObjectId = tgtGuid;
|
||||
|
|
@ -5115,7 +5248,8 @@ public sealed class GameWindow : IDisposable
|
|||
var ms = new AcDream.Core.Physics.MovementStruct { Params = mp };
|
||||
if (update.MotionState.MovementType == 8
|
||||
&& turnPath.TargetGuid is { } turnTgt
|
||||
&& _entitiesByServerGuid.TryGetValue(turnTgt, out var turnEnt))
|
||||
&& _liveEntities is { } liveTurnEntities
|
||||
&& liveTurnEntities.TryGetInteractionEligibleEntity(turnTgt, out var turnEnt))
|
||||
{
|
||||
ms.Type = AcDream.Core.Physics.MovementType.TurnToObject;
|
||||
ms.ObjectId = turnTgt;
|
||||
|
|
@ -5761,15 +5895,25 @@ public sealed class GameWindow : IDisposable
|
|||
/// </summary>
|
||||
private void OnLiveStateUpdated(AcDream.Core.Net.Messages.SetState.Parsed parsed)
|
||||
{
|
||||
if (!_liveEntities!.TryApplyState(parsed, out _)) return;
|
||||
if (!_liveEntities!.TryApplyState(parsed, out _, out _)) return;
|
||||
|
||||
if (!_liveEntities.TryGetRecord(parsed.Guid, out LiveEntityRecord record))
|
||||
return;
|
||||
|
||||
// Retail set_state order: Lighting, NoDraw, then Hidden. The live
|
||||
// runtime already committed the raw/final bits and draw visibility;
|
||||
// apply the ordered owners before updating motion/collision consumers.
|
||||
_liveEntityLights?.OnStateChanged(parsed.Guid);
|
||||
_liveEntityPresentation?.OnStateAccepted(parsed.Guid);
|
||||
|
||||
_projectileController?.ApplyAuthoritativeState(
|
||||
parsed.Guid,
|
||||
(AcDream.Core.Physics.PhysicsStateFlags)parsed.PhysicsState,
|
||||
record.FinalPhysicsState,
|
||||
_physicsScriptGameTime,
|
||||
_liveCenterX,
|
||||
_liveCenterY);
|
||||
_liveEntityLights?.OnStateChanged(parsed.Guid);
|
||||
if (parsed.Guid == _playerServerGuid)
|
||||
_playerController?.ApplyPhysicsState(record.FinalPhysicsState);
|
||||
|
||||
if (!_entitiesByServerGuid.TryGetValue(parsed.Guid, out var entity)) return;
|
||||
|
||||
|
|
@ -5782,11 +5926,9 @@ public sealed class GameWindow : IDisposable
|
|||
// ACE flipped the ETHEREAL bit.
|
||||
uint registryKey = entity.Id;
|
||||
|
||||
_physicsEngine.ShadowObjects.UpdatePhysicsState(registryKey, parsed.PhysicsState);
|
||||
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeBuildingEnabled)
|
||||
Console.WriteLine(System.FormattableString.Invariant(
|
||||
$"[setstate] guid=0x{parsed.Guid:X8} entityId=0x{registryKey:X8} state=0x{parsed.PhysicsState:X8} instSeq={parsed.InstanceSequence} stateSeq={parsed.StateSequence}"));
|
||||
$"[setstate] guid=0x{parsed.Guid:X8} entityId=0x{registryKey:X8} raw=0x{parsed.PhysicsState:X8} final=0x{(uint)record.FinalPhysicsState:X8} instSeq={parsed.InstanceSequence} stateSeq={parsed.StateSequence}"));
|
||||
}
|
||||
|
||||
private void ApplyServerControlledVelocityCycle(
|
||||
|
|
@ -5995,6 +6137,14 @@ public sealed class GameWindow : IDisposable
|
|||
: new System.Numerics.Quaternion(p.RotationX, p.RotationY, p.RotationZ, p.RotationW);
|
||||
DumpMovementTruthServerEcho(update, worldPos);
|
||||
|
||||
bool remoteHardTeleport = update.Guid != _playerServerGuid
|
||||
&& timestamps.TeleportHookRequired;
|
||||
bool remotePlacementRequired = update.Guid != _playerServerGuid
|
||||
&& (remoteHardTeleport
|
||||
|| _remoteTeleportController?.HasPending(update.Guid) == true);
|
||||
if (remoteHardTeleport)
|
||||
RunRemoteTeleportHook(update.Guid, entity.Id);
|
||||
|
||||
// Missiles reconcile the same predicted PhysicsBody in place. The
|
||||
// timestamp gate above already rejected stale corrections; returning
|
||||
// here prevents the generic remote locomotion path from allocating a
|
||||
|
|
@ -6013,6 +6163,13 @@ public sealed class GameWindow : IDisposable
|
|||
_liveCenterY) == true)
|
||||
return;
|
||||
|
||||
if (remotePlacementRequired)
|
||||
{
|
||||
_remoteTeleportController!.BeginPlacement(
|
||||
update.Guid,
|
||||
acceptedSpawn.InstanceSequence);
|
||||
}
|
||||
|
||||
// Capture the pre-update render position for the soft-snap residual
|
||||
// calculation below. Assign entity.Position to the server truth up
|
||||
// front; if we then compute a snap residual, we restore the rendered
|
||||
|
|
@ -6096,6 +6253,50 @@ public sealed class GameWindow : IDisposable
|
|||
rmState.Body.Position = worldPos;
|
||||
}
|
||||
|
||||
// Retail CPhysicsObj::MoveOrTeleport Branch A (0x00516330): a
|
||||
// fresh TELEPORT_TS, or the first placement of a cell-less body,
|
||||
// runs teleport_hook and SetPosition(0x1012) BEFORE the contact
|
||||
// test. In particular, an airborne UP cannot veto or undo this
|
||||
// authoritative destination. Do not pre-clear velocity or invent
|
||||
// grounded flags here: retail SetPosition derives contact from its
|
||||
// transition, while MoveOrTeleport does not consume arg5/arg6.
|
||||
if (remotePlacementRequired)
|
||||
{
|
||||
double teleportTime =
|
||||
(System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
|
||||
bool projectionVisible = _liveEntities.TryGetRecord(
|
||||
update.Guid,
|
||||
out LiveEntityRecord teleportRecord)
|
||||
&& teleportRecord.IsSpatiallyVisible;
|
||||
var placement = _remoteTeleportController!.TryApply(
|
||||
rmState,
|
||||
entity,
|
||||
worldPos,
|
||||
p.LandblockId,
|
||||
new System.Numerics.Vector3(
|
||||
p.PositionX,
|
||||
p.PositionY,
|
||||
p.PositionZ),
|
||||
rot,
|
||||
teleportTime,
|
||||
projectionVisible,
|
||||
acceptedSpawn.InstanceSequence,
|
||||
acceptedSpawn.PositionSequence);
|
||||
if (!placement.Applied)
|
||||
{
|
||||
entity.SetPosition(rmState.Body.Position);
|
||||
entity.ParentCellId = rmState.CellId;
|
||||
entity.Rotation = rmState.Body.Orientation;
|
||||
if (rmState.CellId != 0)
|
||||
_liveEntities.RebucketLiveEntity(update.Guid, rmState.CellId);
|
||||
return;
|
||||
}
|
||||
|
||||
entity.SetPosition(rmState.Body.Position);
|
||||
entity.Rotation = rmState.Body.Orientation;
|
||||
return;
|
||||
}
|
||||
|
||||
// L.3 M2 (2026-05-05): retail-faithful MoveOrTeleport routing for
|
||||
// player remotes. Mirrors CPhysicsObj::MoveOrTeleport
|
||||
// (acclient @ 0x00516330) — airborne no-op, far-snap, near
|
||||
|
|
@ -6284,7 +6485,7 @@ public sealed class GameWindow : IDisposable
|
|||
// DR tick) and for no-Sequencer players. rmState.CellId is the server
|
||||
// cell adopted above (:5735). The per-tick loop keeps it current
|
||||
// between UPs (movement-gated).
|
||||
if (rmState.CellId != 0)
|
||||
if (rmState.CellId != 0 && !_liveEntities.IsHidden(update.Guid))
|
||||
_remotePhysicsUpdater.SyncRemoteShadowToBody(
|
||||
entity.Id, rmState, _liveCenterX, _liveCenterY);
|
||||
entity.SetPosition(rmState.Body.Position);
|
||||
|
|
@ -6514,7 +6715,7 @@ public sealed class GameWindow : IDisposable
|
|||
// Covers the first UP (before any DR tick) and no-Sequencer NPCs (which
|
||||
// the per-tick loop skips). The per-tick loop keeps it current between
|
||||
// UPs, movement-gated. rmState.CellId is the server cell adopted above.
|
||||
if (rmState.CellId != 0)
|
||||
if (rmState.CellId != 0 && !_liveEntities.IsHidden(update.Guid))
|
||||
_remotePhysicsUpdater.SyncRemoteShadowToBody(
|
||||
entity.Id, rmState, _liveCenterX, _liveCenterY);
|
||||
|
||||
|
|
@ -8777,7 +8978,10 @@ public sealed class GameWindow : IDisposable
|
|||
// (which runs after this block). Replaces the AP-79 player poll.
|
||||
_playerHost?.HandleTargetting();
|
||||
|
||||
var result = _playerController.Update((float)dt, input);
|
||||
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.
|
||||
|
|
@ -8787,7 +8991,8 @@ public sealed class GameWindow : IDisposable
|
|||
pe.ParentCellId = result.CellId;
|
||||
pe.Rotation = System.Numerics.Quaternion.CreateFromAxisAngle(
|
||||
System.Numerics.Vector3.UnitZ, _playerController.Yaw - MathF.PI / 2f);
|
||||
SyncLocalPlayerShadow(pe, result.CellId);
|
||||
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
|
||||
|
|
@ -8858,7 +9063,7 @@ public sealed class GameWindow : IDisposable
|
|||
trackedTargetPoint: trackedCombatTarget);
|
||||
|
||||
// Send outbound movement messages to the live server.
|
||||
if (_liveSession is not null)
|
||||
if (_liveSession is not null && !localPlayerHidden)
|
||||
{
|
||||
// Convert world position back to AC wire coordinates.
|
||||
// World origin is _liveCenterX/_liveCenterY; each landblock is 192 units.
|
||||
|
|
@ -9274,6 +9479,14 @@ public sealed class GameWindow : IDisposable
|
|||
// 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,
|
||||
|
|
@ -10386,6 +10599,22 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private void RunRemoteTeleportHook(uint serverGuid, uint localEntityId)
|
||||
{
|
||||
_remoteDeadReckon.TryGetValue(serverGuid, out RemoteMotion? remote);
|
||||
EntityPhysicsHost? host = _physicsHosts.TryGetValue(serverGuid, out var registered)
|
||||
? registered as EntityPhysicsHost
|
||||
: remote?.Host;
|
||||
AcDream.App.Physics.RemoteTeleportHook.Execute(
|
||||
new AcDream.App.Physics.RemoteTeleportHookActions(
|
||||
CancelMoveTo: error => remote?.Movement.CancelMoveTo(error),
|
||||
UnStick: () => host?.PositionManager.UnStick(),
|
||||
StopInterpolating: () => remote?.Interp.Clear(),
|
||||
UnConstrain: () => host?.PositionManager.UnConstrain(),
|
||||
NotifyTeleported: () => host?.NotifyTeleported(),
|
||||
ReportCollisionEnd: () => _physicsEngine.ShadowObjects.Suspend(localEntityId)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase 6.4: advance every animated entity's frame counter by
|
||||
/// <paramref name="dt"/> * Framerate, wrapping around the cycle's
|
||||
|
|
@ -10433,6 +10662,21 @@ 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;
|
||||
}
|
||||
|
||||
// ── Dead-reckoning: smooth position between UpdatePosition bursts.
|
||||
// The server broadcasts UpdatePosition at ~5-10Hz for distant
|
||||
// entities; without integration, remote chars jitter-hop between
|
||||
|
|
@ -13570,6 +13814,8 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
|
||||
_playerController = new AcDream.App.Input.PlayerMovementController(_physicsEngine);
|
||||
if (_liveEntities?.TryGetRecord(_playerServerGuid, out LiveEntityRecord playerRecord) == true)
|
||||
_playerController.ApplyPhysicsState(playerRecord.FinalPhysicsState);
|
||||
|
||||
// R4-V5: the local player's verbatim MoveToManager — same seam
|
||||
// wiring shape as EnsureRemoteMotionBindings, with three
|
||||
|
|
@ -14192,6 +14438,9 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
_liveEntityLights?.Dispose();
|
||||
_liveEntityLights = null;
|
||||
_liveEntityPresentation?.Dispose();
|
||||
_remoteTeleportController?.Dispose();
|
||||
_remoteTeleportController = null;
|
||||
_entityEffects?.ClearNetworkState();
|
||||
_animationHookFrames?.Clear();
|
||||
_effectPoses.Clear();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue