fix(headless): complete connected movement gate
This commit is contained in:
parent
fd9559a063
commit
3f3401257c
11 changed files with 756 additions and 48 deletions
|
|
@ -1,3 +1,5 @@
|
|||
using AcDream.Headless.Configuration;
|
||||
|
||||
namespace AcDream.Headless.Tests;
|
||||
|
||||
public sealed class HeadlessEntryPointTests
|
||||
|
|
@ -17,10 +19,48 @@ public sealed class HeadlessEntryPointTests
|
|||
|
||||
Assert.Equal(0, exitCode);
|
||||
Assert.Contains("acdream-headless", output.ToString());
|
||||
Assert.Contains("Secrets are never accepted", output.ToString());
|
||||
Assert.Contains("--password", output.ToString());
|
||||
Assert.Equal(string.Empty, error.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RunCommandAcceptsDirectCredentialAliases()
|
||||
{
|
||||
HeadlessCommandLine command = HeadlessCommandLine.Parse(
|
||||
[
|
||||
"run",
|
||||
"--config",
|
||||
"bot.json",
|
||||
"-user",
|
||||
"testaccount",
|
||||
"--password",
|
||||
"fixture-password",
|
||||
]);
|
||||
|
||||
Assert.Equal("testaccount", command.DirectCredentials?.User);
|
||||
Assert.Equal(
|
||||
"fixture-password",
|
||||
command.DirectCredentials?.Password);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("-user", "testaccount")]
|
||||
[InlineData("--password", "fixture-password")]
|
||||
public void DirectCredentialPairMustBeComplete(
|
||||
string option,
|
||||
string value)
|
||||
{
|
||||
Assert.Throws<HeadlessCommandLineException>(
|
||||
() => HeadlessCommandLine.Parse(
|
||||
[
|
||||
"run",
|
||||
"--config",
|
||||
"bot.json",
|
||||
option,
|
||||
value,
|
||||
]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAcceptsStrictEmptyNoConnectConfiguration()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -95,6 +95,41 @@ public sealed class HeadlessSessionHostTests
|
|||
diagnostics.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DirectCredentialsOverrideSingleConfiguredSession()
|
||||
{
|
||||
var configuration = new HeadlessConfiguration
|
||||
{
|
||||
Version = 1,
|
||||
Sessions = [Descriptor()],
|
||||
};
|
||||
HeadlessPathSet paths = HeadlessPathSet.Resolve(
|
||||
new HeadlessPathOverrides());
|
||||
using var diagnostics = new StringWriter();
|
||||
var operations = new FixtureSessionOperations();
|
||||
using var host = new HeadlessProcessHost(
|
||||
configuration,
|
||||
paths,
|
||||
TextReader.Null,
|
||||
diagnostics,
|
||||
operations,
|
||||
directCredentials: new HeadlessDirectCredentials(
|
||||
"direct-account",
|
||||
"direct-secret"));
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
cancellation.Cancel();
|
||||
|
||||
HeadlessExitCode result =
|
||||
await host.RunAsync(cancellation.Token);
|
||||
|
||||
Assert.Equal(HeadlessExitCode.Success, result);
|
||||
Assert.Equal("direct-account", operations.LastUser);
|
||||
Assert.Equal("direct-secret", operations.LastPassword);
|
||||
Assert.DoesNotContain(
|
||||
"direct-secret",
|
||||
diagnostics.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DirectFrameUsesSharedRetailOrderAndMovementCadence()
|
||||
{
|
||||
|
|
@ -220,10 +255,17 @@ public sealed class HeadlessSessionHostTests
|
|||
PlayerMovementController controller =
|
||||
Assert.IsType<PlayerMovementController>(
|
||||
runtime.MovementOwner.Controller);
|
||||
projection.ProjectPosition(record, isLocalPlayer: true);
|
||||
controller.SetPosition(
|
||||
new Vector3(48f, 49f, 50f),
|
||||
0xA9B40001u);
|
||||
projection.ProjectPosition(
|
||||
record,
|
||||
isLocalPlayer: true,
|
||||
PositionTimestampDisposition.Apply);
|
||||
|
||||
Assert.Same(controller, runtime.MovementOwner.Controller);
|
||||
Assert.Equal(record.LocalEntityId, controller.LocalEntityId);
|
||||
Assert.Equal(new Vector3(48f, 49f, 50f), controller.Position);
|
||||
Assert.Equal(
|
||||
0xA9B40000u,
|
||||
controller.CellId & 0xFFFF0000u);
|
||||
|
|
@ -248,10 +290,89 @@ public sealed class HeadlessSessionHostTests
|
|||
Assert.True(readiness.IsCollisionReady);
|
||||
Assert.False(readiness.IsUnhydratable);
|
||||
Assert.Equal(PlayerState.InWorld, controller.State);
|
||||
Assert.Equal(4, collision.CenterCount);
|
||||
Assert.Equal(3, collision.CenterCount);
|
||||
Assert.Equal(0xA9B40001u, collision.LastCell);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorldProjectionIgnoresNormalEchoButBlipsForcePosition()
|
||||
{
|
||||
var operations = new FixtureSessionOperations();
|
||||
using var credential = new HeadlessCredentialSecret(
|
||||
"fixture",
|
||||
"password");
|
||||
using var host = new HeadlessSessionHost(
|
||||
Descriptor(),
|
||||
credential,
|
||||
new HeadlessDiagnosticWriter(TextWriter.Null),
|
||||
operations);
|
||||
GameRuntime runtime = host.Runtime;
|
||||
const uint player = 0x50000003u;
|
||||
runtime.PlayerIdentity.ServerGuid = player;
|
||||
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
|
||||
RuntimeEntityRecord record = runtime.EntityObjects
|
||||
.RegisterEntity(Spawn(player))
|
||||
.Canonical!;
|
||||
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
|
||||
record,
|
||||
record.CreateIntegrationVersion,
|
||||
record.Snapshot,
|
||||
replaceGeneration: false));
|
||||
var collision = new FixtureCollisionNeighborhood();
|
||||
var projection = new HeadlessSessionWorldProjection(
|
||||
runtime,
|
||||
collision);
|
||||
projection.ProjectSpawn(record, isLocalPlayer: true);
|
||||
PlayerMovementController controller =
|
||||
Assert.IsType<PlayerMovementController>(
|
||||
runtime.MovementOwner.Controller);
|
||||
|
||||
controller.SetPosition(
|
||||
new Vector3(48f, 49f, 50f),
|
||||
0xA9B40001u);
|
||||
projection.ProjectPosition(
|
||||
record,
|
||||
isLocalPlayer: true,
|
||||
PositionTimestampDisposition.Apply);
|
||||
|
||||
Assert.Equal(new Vector3(48f, 49f, 50f), controller.Position);
|
||||
|
||||
var force = new WorldSession.EntityPositionUpdate(
|
||||
player,
|
||||
record.Snapshot.Position!.Value with
|
||||
{
|
||||
PositionX = 72f,
|
||||
PositionY = 73f,
|
||||
},
|
||||
Velocity: null,
|
||||
PlacementId: null,
|
||||
IsGrounded: true,
|
||||
InstanceSequence: 1,
|
||||
PositionSequence: 2,
|
||||
TeleportSequence: 0,
|
||||
ForcePositionSequence: 1);
|
||||
Assert.True(runtime.EntityObjects.TryApplyPosition(
|
||||
force,
|
||||
isLocalPlayer: true,
|
||||
forcePositionRotation: Quaternion.Identity,
|
||||
currentLocalVelocity: controller.BodyVelocity,
|
||||
projectionRequiresTeleportHook: false,
|
||||
acknowledgeProjection: null,
|
||||
out PositionTimestampDisposition disposition,
|
||||
out _,
|
||||
out _));
|
||||
Assert.Equal(
|
||||
PositionTimestampDisposition.ForcePosition,
|
||||
disposition);
|
||||
projection.ProjectPosition(
|
||||
record,
|
||||
isLocalPlayer: true,
|
||||
disposition);
|
||||
|
||||
Assert.Equal(new Vector3(72f, 73f, 50f), controller.Position);
|
||||
Assert.Equal(2, collision.CenterCount);
|
||||
}
|
||||
|
||||
private static HeadlessSessionDescriptor Descriptor(
|
||||
HeadlessCredentialProviderKind provider =
|
||||
HeadlessCredentialProviderKind.Environment,
|
||||
|
|
@ -389,6 +510,8 @@ public sealed class HeadlessSessionHostTests
|
|||
public List<WorldSession> Sessions { get; } = [];
|
||||
public int CreatedSessionCount { get; private set; }
|
||||
public int DisposedSessionCount { get; private set; }
|
||||
public string? LastUser { get; private set; }
|
||||
public string? LastPassword { get; private set; }
|
||||
|
||||
public IPEndPoint ResolveEndpoint(string host, int port) =>
|
||||
new(IPAddress.Loopback, port);
|
||||
|
|
@ -406,6 +529,8 @@ public sealed class HeadlessSessionHostTests
|
|||
string user,
|
||||
string password)
|
||||
{
|
||||
LastUser = user;
|
||||
LastPassword = password;
|
||||
}
|
||||
|
||||
public CharacterList.Parsed GetCharacters(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue