test(physics): pin issue 270 animation fixes
This commit is contained in:
parent
bb1640f777
commit
0cb60d98a0
7 changed files with 337 additions and 43 deletions
|
|
@ -98,6 +98,7 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
private readonly LiveSessionWorldRuntime _world;
|
||||
private readonly LiveSessionCommandSurface _commands;
|
||||
private readonly Action<string> _log;
|
||||
private readonly StaminaExhaustionEdgeTracker _staminaExhaustion = new();
|
||||
|
||||
public LiveSessionRuntimeFactory(
|
||||
LiveSessionPlayerRuntime player,
|
||||
|
|
@ -189,6 +190,7 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
|
||||
private void ResetPlayerPresentation()
|
||||
{
|
||||
_staminaExhaustion.Reset();
|
||||
_interaction.PlayerMode.ResetSession();
|
||||
_world.SpawnClaims.Reset();
|
||||
}
|
||||
|
|
@ -289,13 +291,6 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
OnMovementStatsUpdated: () => ApplyMovementStats("stats"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the previous "stamina exhausted" state so
|
||||
/// <see cref="ApplyMovementStats"/> can fire retail's exhaustion
|
||||
/// notification on the EDGE only. Null = no stamina reading applied yet.
|
||||
/// </summary>
|
||||
private bool? _lastStaminaExhausted;
|
||||
|
||||
/// <summary>
|
||||
/// Re-applies the current <see cref="RuntimeMovementSkillState"/>
|
||||
/// snapshot (skills/burden/stamina) to the live player controller.
|
||||
|
|
@ -328,14 +323,8 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
}
|
||||
|
||||
RuntimeMovementSkillSnapshot snapshot = _domain.Character.MovementSkills.Snapshot;
|
||||
bool exhausted = snapshot.CurrentStamina == 0;
|
||||
if (_lastStaminaExhausted != exhausted)
|
||||
{
|
||||
bool isEdge = _lastStaminaExhausted is not null;
|
||||
_lastStaminaExhausted = exhausted;
|
||||
if (isEdge)
|
||||
controller!.Motion.ReportExhaustion();
|
||||
}
|
||||
if (_staminaExhaustion.Observe(snapshot.CurrentStamina))
|
||||
controller!.Motion.ReportExhaustion();
|
||||
|
||||
_log(
|
||||
$"player: applied server movement {reason} "
|
||||
|
|
|
|||
32
src/AcDream.App/Net/StaminaExhaustionEdgeTracker.cs
Normal file
32
src/AcDream.App/Net/StaminaExhaustionEdgeTracker.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
namespace AcDream.App.Net;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks retail's stamina-exhaustion notification edge for one live-session
|
||||
/// generation.
|
||||
/// </summary>
|
||||
internal sealed class StaminaExhaustionEdgeTracker
|
||||
{
|
||||
private bool? _wasExhausted;
|
||||
|
||||
/// <summary>
|
||||
/// Observes the current stamina value and returns <see langword="true"/>
|
||||
/// only when a previously initialized state changes between exhausted and
|
||||
/// non-exhausted.
|
||||
/// </summary>
|
||||
public bool Observe(int currentStamina)
|
||||
{
|
||||
bool exhausted = currentStamina == 0;
|
||||
if (_wasExhausted == exhausted)
|
||||
return false;
|
||||
|
||||
bool isEdge = _wasExhausted.HasValue;
|
||||
_wasExhausted = exhausted;
|
||||
return isEdge;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forgets the retiring character/session sample. The first sample in the
|
||||
/// next generation establishes a baseline and must not synthesize an edge.
|
||||
/// </summary>
|
||||
public void Reset() => _wasExhausted = null;
|
||||
}
|
||||
|
|
@ -168,9 +168,6 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
System.Numerics.Vector3 worldPos,
|
||||
uint cellId)
|
||||
{
|
||||
if (cellId == 0)
|
||||
return;
|
||||
|
||||
var (radius, height) = _motionRuntime.GetSetupCylinder(serverGuid, entity);
|
||||
if (radius < 0.05f)
|
||||
{
|
||||
|
|
@ -193,33 +190,20 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
// retail's first frame would (position snapped onto the floor,
|
||||
// contact plane + CONTACT/ON_WALKABLE committed below). A sweep that
|
||||
// finds no floor (true airborne spawn) leaves the body airborne.
|
||||
AcDream.Core.Physics.ResolveResult settle = _physicsEngine.ResolveWithTransition(
|
||||
worldPos,
|
||||
worldPos + new System.Numerics.Vector3(0f, 0f, -0.5f),
|
||||
cellId,
|
||||
sphereRadius: radius,
|
||||
sphereHeight: height,
|
||||
stepUpHeight: 0.4f,
|
||||
stepDownHeight: 0.4f,
|
||||
isOnGround: false,
|
||||
body: remote.Body,
|
||||
moverFlags: moverFlags,
|
||||
movingEntityId: entity.Id);
|
||||
if (!settle.Ok || !settle.InContact)
|
||||
return; // no floor within reach — stays airborne like retail's fall
|
||||
|
||||
remote.Body.Position = settle.Position;
|
||||
|
||||
AcDream.Core.Physics.PhysicsObjUpdate.CommitSetPositionTransition(
|
||||
if (!RemoteSpawnPlacementSettler.TrySettle(
|
||||
_physicsEngine,
|
||||
remote.Body,
|
||||
settle.InContact,
|
||||
settle.OnWalkable,
|
||||
settle.CollisionNormalValid,
|
||||
settle.CollisionNormal,
|
||||
previousContact: false,
|
||||
previousOnWalkable: false,
|
||||
worldPos,
|
||||
cellId,
|
||||
radius,
|
||||
height,
|
||||
moverFlags,
|
||||
entity.Id,
|
||||
remote.Movement.HitGround,
|
||||
remote.Motion.LeaveGround);
|
||||
remote.Motion.LeaveGround))
|
||||
{
|
||||
return; // no floor within reach — stays airborne like retail's fall
|
||||
}
|
||||
remote.Airborne = !remote.Body.OnWalkable;
|
||||
}
|
||||
|
||||
|
|
|
|||
62
src/AcDream.App/Physics/RemoteSpawnPlacementSettler.cs
Normal file
62
src/AcDream.App/Physics/RemoteSpawnPlacementSettler.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.App.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Performs the compressed first-gravity-frame settle used to establish
|
||||
/// retail Contact/OnWalkable state for a newly materialized remote body.
|
||||
/// </summary>
|
||||
internal static class RemoteSpawnPlacementSettler
|
||||
{
|
||||
internal const float SettleDistance = 0.5f;
|
||||
|
||||
public static bool TrySettle(
|
||||
PhysicsEngine physicsEngine,
|
||||
PhysicsBody body,
|
||||
Vector3 worldPosition,
|
||||
uint cellId,
|
||||
float sphereRadius,
|
||||
float sphereHeight,
|
||||
ObjectInfoState moverFlags,
|
||||
uint movingEntityId,
|
||||
Action hitGround,
|
||||
Action leaveGround)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(physicsEngine);
|
||||
ArgumentNullException.ThrowIfNull(body);
|
||||
ArgumentNullException.ThrowIfNull(hitGround);
|
||||
ArgumentNullException.ThrowIfNull(leaveGround);
|
||||
|
||||
if (cellId == 0)
|
||||
return false;
|
||||
|
||||
ResolveResult settle = physicsEngine.ResolveWithTransition(
|
||||
worldPosition,
|
||||
worldPosition - new Vector3(0f, 0f, SettleDistance),
|
||||
cellId,
|
||||
sphereRadius,
|
||||
sphereHeight,
|
||||
stepUpHeight: 0.4f,
|
||||
stepDownHeight: 0.4f,
|
||||
isOnGround: false,
|
||||
body,
|
||||
moverFlags,
|
||||
movingEntityId);
|
||||
if (!settle.Ok || !settle.InContact)
|
||||
return false;
|
||||
|
||||
body.Position = settle.Position;
|
||||
PhysicsObjUpdate.CommitSetPositionTransition(
|
||||
body,
|
||||
settle.InContact,
|
||||
settle.OnWalkable,
|
||||
settle.CollisionNormalValid,
|
||||
settle.CollisionNormal,
|
||||
previousContact: false,
|
||||
previousOnWalkable: false,
|
||||
hitGround,
|
||||
leaveGround);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue