feat(headless): complete deterministic bot command parity
This commit is contained in:
parent
7e8acb74dd
commit
38e83640d9
37 changed files with 2805 additions and 295 deletions
|
|
@ -1,12 +1,17 @@
|
|||
using System.Buffers.Binary;
|
||||
using System.Net;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Headless.Configuration;
|
||||
using AcDream.Headless.Credentials;
|
||||
using AcDream.Headless.Diagnostics;
|
||||
using AcDream.Headless.Hosting;
|
||||
using AcDream.Headless.Platform;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Runtime.Session;
|
||||
|
||||
namespace AcDream.Headless.Tests;
|
||||
|
|
@ -72,7 +77,8 @@ public sealed class HeadlessSessionHostTests
|
|||
using var host = new HeadlessProcessHost(
|
||||
configuration,
|
||||
paths,
|
||||
new StringReader("process-password" + Environment.NewLine),
|
||||
new System.IO.StringReader(
|
||||
"process-password" + Environment.NewLine),
|
||||
diagnostics,
|
||||
operations);
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
|
|
@ -88,6 +94,71 @@ public sealed class HeadlessSessionHostTests
|
|||
diagnostics.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DirectFrameUsesSharedRetailOrderAndMovementCadence()
|
||||
{
|
||||
var operations = new FixtureSessionOperations();
|
||||
using var credential = new HeadlessCredentialSecret(
|
||||
"fixture",
|
||||
"password");
|
||||
using var host = new HeadlessSessionHost(
|
||||
Descriptor(),
|
||||
credential,
|
||||
new HeadlessDiagnosticWriter(TextWriter.Null),
|
||||
operations);
|
||||
Assert.Equal(
|
||||
RuntimeSessionStartStatus.Connected,
|
||||
host.Start().Status);
|
||||
HydrateGroundedPlayer(host.Runtime);
|
||||
var sent = new List<(byte[] Body, double Time)>();
|
||||
var trace = new RuntimeTraceRecorder();
|
||||
using IDisposable subscription =
|
||||
host.Runtime.Subscribe(trace);
|
||||
operations.Sessions[^1].GameActionCapture =
|
||||
body => sent.Add((
|
||||
body,
|
||||
host.Runtime.Clock.SimulationTimeSeconds));
|
||||
|
||||
RuntimeCommandResult intent = host.Commands.Movement.SetIntent(
|
||||
host.Runtime.Generation,
|
||||
new MovementInput(Forward: true, Run: true));
|
||||
host.Tick(0.015d);
|
||||
|
||||
Assert.True(intent.Accepted);
|
||||
Assert.Contains(
|
||||
trace.Entries,
|
||||
static entry =>
|
||||
entry.Kind == RuntimeTraceKind.Movement);
|
||||
Assert.Equal(
|
||||
[
|
||||
MoveToState.MoveToStateAction,
|
||||
AutonomousPosition.AutonomousPositionAction,
|
||||
],
|
||||
sent.Select(static entry =>
|
||||
ActionOpcode(entry.Body)).ToArray());
|
||||
|
||||
sent.Clear();
|
||||
for (int index = 0; index < 70; index++)
|
||||
host.Tick(0.015d);
|
||||
|
||||
(byte[] Body, double Time)[] positions = sent
|
||||
.Where(static entry =>
|
||||
ActionOpcode(entry.Body)
|
||||
== AutonomousPosition.AutonomousPositionAction)
|
||||
.ToArray();
|
||||
Assert.InRange(positions.Length, 1, 2);
|
||||
Assert.True(positions[^1].Time >= 1d);
|
||||
if (positions.Length == 2)
|
||||
{
|
||||
Assert.True(
|
||||
positions[1].Time - positions[0].Time >= 0.99d);
|
||||
}
|
||||
Assert.DoesNotContain(
|
||||
sent,
|
||||
entry => ActionOpcode(entry.Body)
|
||||
== MoveToState.MoveToStateAction);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TeardownRetriesOnlyTheUnfinishedSuffix()
|
||||
{
|
||||
|
|
@ -142,8 +213,109 @@ public sealed class HeadlessSessionHostTests
|
|||
},
|
||||
};
|
||||
|
||||
private static void HydrateGroundedPlayer(GameRuntime runtime)
|
||||
{
|
||||
const uint player = 0x50000002u;
|
||||
PhysicsEngine engine = runtime.EntityObjects.Physics.Engine;
|
||||
var heights = new byte[81];
|
||||
Array.Fill(heights, (byte)50);
|
||||
var heightTable = new float[256];
|
||||
for (int index = 0; index < heightTable.Length; index++)
|
||||
heightTable[index] = index;
|
||||
engine.AddLandblock(
|
||||
0xA9B4FFFFu,
|
||||
new TerrainSurface(heights, heightTable),
|
||||
[],
|
||||
[],
|
||||
worldOffsetX: 0f,
|
||||
worldOffsetY: 0f);
|
||||
|
||||
RuntimeEntityRecord record = runtime.EntityObjects
|
||||
.RegisterEntity(Spawn(player))
|
||||
.Canonical!;
|
||||
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
|
||||
record,
|
||||
record.CreateIntegrationVersion,
|
||||
record.Snapshot,
|
||||
replaceGeneration: false));
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(
|
||||
new Vector3(96f, 97f, 50f),
|
||||
0xA9B40001u,
|
||||
new Vector3(96f, 97f, 50f));
|
||||
runtime.MovementOwner.Controller = controller;
|
||||
}
|
||||
|
||||
private static WorldSession.EntitySpawn Spawn(uint guid)
|
||||
{
|
||||
var position = new CreateObject.ServerPosition(
|
||||
0xA9B40001u,
|
||||
96f,
|
||||
97f,
|
||||
50f,
|
||||
1f,
|
||||
0f,
|
||||
0f,
|
||||
0f);
|
||||
var timestamps = new PhysicsTimestamps(
|
||||
Position: 1,
|
||||
Movement: 1,
|
||||
State: 1,
|
||||
Vector: 1,
|
||||
Teleport: 0,
|
||||
ServerControlledMove: 1,
|
||||
ForcePosition: 0,
|
||||
ObjDesc: 1,
|
||||
Instance: 1);
|
||||
var physics = new PhysicsSpawnData(
|
||||
RawState: (uint)PhysicsStateFlags.ReportCollisions,
|
||||
Position: position,
|
||||
Movement: null,
|
||||
AnimationFrame: null,
|
||||
SetupTableId: 0x02000001u,
|
||||
MotionTableId: null,
|
||||
SoundTableId: null,
|
||||
PhysicsScriptTableId: null,
|
||||
Parent: null,
|
||||
Children: null,
|
||||
Scale: null,
|
||||
Friction: null,
|
||||
Elasticity: null,
|
||||
Translucency: null,
|
||||
Velocity: null,
|
||||
Acceleration: null,
|
||||
AngularVelocity: null,
|
||||
DefaultScriptType: null,
|
||||
DefaultScriptIntensity: null,
|
||||
Timestamps: timestamps);
|
||||
return new WorldSession.EntitySpawn(
|
||||
guid,
|
||||
position,
|
||||
0x02000001u,
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
"Headless",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
PhysicsState: physics.RawState,
|
||||
InstanceSequence: 1,
|
||||
MovementSequence: 1,
|
||||
ServerControlSequence: 1,
|
||||
PositionSequence: 1,
|
||||
Physics: physics);
|
||||
}
|
||||
|
||||
private static uint ActionOpcode(byte[] body) =>
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(
|
||||
body.AsSpan(8, sizeof(uint)));
|
||||
|
||||
private sealed class FixtureSessionOperations : ILiveSessionOperations
|
||||
{
|
||||
public List<WorldSession> Sessions { get; } = [];
|
||||
public int CreatedSessionCount { get; private set; }
|
||||
public int DisposedSessionCount { get; private set; }
|
||||
|
||||
|
|
@ -153,7 +325,9 @@ public sealed class HeadlessSessionHostTests
|
|||
public WorldSession CreateSession(IPEndPoint endpoint)
|
||||
{
|
||||
CreatedSessionCount++;
|
||||
return new WorldSession(endpoint);
|
||||
var session = new WorldSession(endpoint);
|
||||
Sessions.Add(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
public void Connect(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue