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:
Erik 2026-07-14 14:59:48 +02:00
parent a51ebc66e9
commit 1e98d81448
46 changed files with 4883 additions and 127 deletions

View file

@ -149,6 +149,17 @@ public sealed class PlayerMovementController
/// </summary>
public uint LocalEntityId { get; set; }
/// <summary>
/// Applies the canonical server PhysicsState to the local body. Retail's
/// <c>CPhysicsObj::SetState</c> replaces these persistent bits before its
/// next acceleration/integration decision.
/// </summary>
public void ApplyPhysicsState(PhysicsStateFlags state)
{
_body.State = state;
_body.calc_acceleration();
}
public bool IsAirborne => !_body.OnWalkable;
/// <summary>
@ -571,6 +582,86 @@ public sealed class PlayerMovementController
return Vector3.Lerp(_prevPhysicsPos, _currPhysicsPos, alpha);
}
/// <summary>
/// Retail Hidden slice of <c>CPhysicsObj::UpdatePositionInternal</c>
/// (0x00512C30). Input, PartArray root motion, and physics integration are
/// skipped, while PositionManager composition and the manager tail remain
/// live. A composed offset still commits through CTransition so cell
/// membership and contact state cannot become stale behind the hidden mesh.
/// </summary>
public MovementResult TickHidden(float dt)
{
_simTimeSeconds += dt;
_physicsAccum = 0f;
Vector3 previousPosition = _body.Position;
bool previousContact = _body.InContact;
bool previousOnWalkable = _body.OnWalkable;
if (PositionManager is { } manager)
{
var delta = new AcDream.Core.Physics.Motion.MotionDeltaFrame();
manager.AdjustOffset(delta, dt);
if (delta.Origin != Vector3.Zero)
_body.Position += Vector3.Transform(delta.Origin, _body.Orientation);
if (!delta.Orientation.IsIdentity)
{
Yaw = AcDream.Core.Physics.Motion.MoveToMath.YawFromHeading(
AcDream.Core.Physics.Motion.MoveToMath.HeadingFromYaw(Yaw)
+ delta.GetHeading());
_body.Orientation = Quaternion.CreateFromAxisAngle(
Vector3.UnitZ,
Yaw - MathF.PI / 2f);
}
}
if (_body.Position != previousPosition && CellId != 0 && _physics.LandblockCount > 0)
{
ResolveResult resolved = _physics.ResolveWithTransition(
previousPosition,
_body.Position,
CellId,
sphereRadius: 0.48f,
sphereHeight: 1.835f,
stepUpHeight: StepUpHeight,
stepDownHeight: StepDownHeight,
isOnGround: previousOnWalkable,
body: _body,
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
movingEntityId: LocalEntityId);
_body.Position = resolved.Position;
PhysicsObjUpdate.CommitSetPositionTransition(
_body,
resolved.InContact,
resolved.OnWalkable,
resolved.CollisionNormalValid,
resolved.CollisionNormal,
previousContact,
previousOnWalkable,
Movement.HitGround,
_motion.LeaveGround);
UpdateCellId(resolved.CellId, "hidden-position-manager");
}
Movement.UseTime();
PositionManager?.UseTime();
_prevPhysicsPos = _body.Position;
_currPhysicsPos = _body.Position;
_wasAirborneLastFrame = !_body.OnWalkable;
return new MovementResult(
Position: _body.Position,
RenderPosition: _body.Position,
CellId: CellId,
IsOnGround: _body.OnWalkable,
MotionStateChanged: false,
ForwardCommand: null,
SidestepCommand: null,
TurnCommand: null,
ForwardSpeed: null,
SidestepSpeed: null,
TurnSpeed: null);
}
public MovementResult Update(float dt, MovementInput input)
{
_simTimeSeconds += dt;