acdream/src/AcDream.Runtime/Gameplay/PlayerPhysicsQuantumCapture.cs

191 lines
5.8 KiB
C#

using System.Numerics;
using System.Text.Json;
using AcDream.Core.Physics;
namespace AcDream.Runtime.Gameplay;
/// <summary>
/// Opt-in, player-only trace of the complete retail object quantum.
/// <para>
/// Set <c>ACDREAM_CAPTURE_PLAYER_QUANTA=&lt;path&gt;</c> before process
/// startup to emit one JSON-Lines record per admitted physics quantum. The
/// capture is deliberately outside the physics implementation: it observes
/// the same stage boundaries as retail
/// <c>CPhysicsObj::UpdateObjectInternal</c> without changing their order or
/// introducing a second simulation path. When disabled, the hot path pays
/// one static null/empty check and performs no allocations.
/// </para>
/// </summary>
internal static class PlayerPhysicsQuantumCapture
{
internal static string? CapturePath { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_CAPTURE_PLAYER_QUANTA");
internal static bool IsEnabled => !string.IsNullOrWhiteSpace(CapturePath);
private static readonly JsonSerializerOptions s_jsonOptions = new()
{
IncludeFields = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false,
};
private static readonly object s_writerLock = new();
private static StreamWriter? s_writer;
private static long s_sequence;
private static bool s_processExitHooked;
internal static PlayerPhysicsBodyTraceSnapshot Snapshot(PhysicsBody body) =>
new(
Position: body.Position,
Orientation: body.Orientation,
Velocity: body.Velocity,
Acceleration: body.Acceleration,
GroundNormal: body.GroundNormal,
ContactPlaneValid: body.ContactPlaneValid,
ContactPlane: body.ContactPlane,
Friction: body.Friction,
Elasticity: body.Elasticity,
State: (uint)body.State,
TransientState: (uint)body.TransientState,
FramesStationaryFall: body.FramesStationaryFall);
internal static void Log(
float dt,
uint cellBefore,
uint cellAfter,
MovementInput input,
Vector3 rootAndManagerDelta,
bool candidateMoved,
PlayerPhysicsBodyTraceSnapshot quantumStart,
PlayerPhysicsBodyTraceSnapshot preIntegration,
PlayerPhysicsBodyTraceSnapshot postIntegration,
PlayerPhysicsResolveTraceSnapshot resolve,
PlayerPhysicsBodyTraceSnapshot postCommit)
{
string? path = CapturePath;
if (string.IsNullOrWhiteSpace(path))
return;
var record = new PlayerPhysicsQuantumTraceRecord(
Sequence: Interlocked.Increment(ref s_sequence) - 1,
TimestampTicks: System.Diagnostics.Stopwatch.GetTimestamp(),
Dt: dt,
CellBefore: cellBefore,
CellAfter: cellAfter,
Input: new PlayerMovementInputTraceSnapshot(
input.Forward,
input.Backward,
input.StrafeLeft,
input.StrafeRight,
input.TurnLeft,
input.TurnRight,
input.Run,
input.Jump),
RootAndManagerDelta: rootAndManagerDelta,
CandidateMoved: candidateMoved,
QuantumStart: quantumStart,
PreIntegration: preIntegration,
PostIntegration: postIntegration,
Resolve: resolve,
PostCommit: postCommit);
string json = JsonSerializer.Serialize(record, s_jsonOptions);
lock (s_writerLock)
{
EnsureWriter_NoLock(path);
s_writer!.WriteLine(json);
s_writer.Flush();
}
}
internal static void Close()
{
lock (s_writerLock)
{
s_writer?.Dispose();
s_writer = null;
}
}
internal static void ResetForTest()
{
Close();
CapturePath = null;
Interlocked.Exchange(ref s_sequence, 0);
}
private static void EnsureWriter_NoLock(string path)
{
if (s_writer is not null)
return;
string? directory = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(directory))
Directory.CreateDirectory(directory);
s_writer = new StreamWriter(new FileStream(
path,
FileMode.Append,
FileAccess.Write,
FileShare.Read))
{
AutoFlush = false,
};
if (!s_processExitHooked)
{
AppDomain.CurrentDomain.ProcessExit += static (_, _) => Close();
s_processExitHooked = true;
}
}
}
internal sealed record PlayerPhysicsQuantumTraceRecord(
long Sequence,
long TimestampTicks,
float Dt,
uint CellBefore,
uint CellAfter,
PlayerMovementInputTraceSnapshot Input,
Vector3 RootAndManagerDelta,
bool CandidateMoved,
PlayerPhysicsBodyTraceSnapshot QuantumStart,
PlayerPhysicsBodyTraceSnapshot PreIntegration,
PlayerPhysicsBodyTraceSnapshot PostIntegration,
PlayerPhysicsResolveTraceSnapshot Resolve,
PlayerPhysicsBodyTraceSnapshot PostCommit);
internal readonly record struct PlayerMovementInputTraceSnapshot(
bool Forward,
bool Backward,
bool StrafeLeft,
bool StrafeRight,
bool TurnLeft,
bool TurnRight,
bool Run,
bool Jump);
internal readonly record struct PlayerPhysicsBodyTraceSnapshot(
Vector3 Position,
Quaternion Orientation,
Vector3 Velocity,
Vector3 Acceleration,
Vector3 GroundNormal,
bool ContactPlaneValid,
Plane ContactPlane,
float Friction,
float Elasticity,
uint State,
uint TransientState,
int FramesStationaryFall);
internal readonly record struct PlayerPhysicsResolveTraceSnapshot(
Vector3 Position,
uint CellId,
bool Ok,
bool IsOnGround,
bool InContact,
bool OnWalkable,
bool CollisionNormalValid,
Vector3 CollisionNormal);