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
|
|
@ -0,0 +1,325 @@
|
|||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Spells;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.Runtime.Tests.Gameplay;
|
||||
|
||||
public sealed class RuntimeHostileTargetQueryTests
|
||||
{
|
||||
private const uint Player = 0x50000001u;
|
||||
|
||||
[Fact]
|
||||
public void FindClosest_UsesAbsoluteDerethCoordinatesAcrossLandblocks()
|
||||
{
|
||||
using GameRuntime runtime = Create();
|
||||
runtime.PlayerIdentity.ServerGuid = Player;
|
||||
Add(
|
||||
runtime,
|
||||
Player,
|
||||
landblock: 0x01010001u,
|
||||
x: 191f,
|
||||
y: 100f,
|
||||
PlayerObject(Player));
|
||||
Add(
|
||||
runtime,
|
||||
0x50000010u,
|
||||
landblock: 0x02010001u,
|
||||
x: 1f,
|
||||
y: 100f,
|
||||
Hostile(0x50000010u));
|
||||
Add(
|
||||
runtime,
|
||||
0x50000011u,
|
||||
landblock: 0x01010001u,
|
||||
x: 180f,
|
||||
y: 100f,
|
||||
Hostile(0x50000011u));
|
||||
|
||||
Assert.Equal(
|
||||
0x50000010u,
|
||||
RuntimeHostileTargetQuery.FindClosest(runtime));
|
||||
Assert.True(
|
||||
RuntimeHostileTargetQuery.IsHostile(
|
||||
runtime,
|
||||
0x50000010u));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Query_RejectsHiddenNoDrawDeadPlayersPetsNpcsAndNonCreatures()
|
||||
{
|
||||
using GameRuntime runtime = Create();
|
||||
runtime.PlayerIdentity.ServerGuid = Player;
|
||||
Add(runtime, Player, 0x01010001u, 10f, 10f, PlayerObject(Player));
|
||||
Add(
|
||||
runtime,
|
||||
0x50000010u,
|
||||
0x01010001u,
|
||||
11f,
|
||||
10f,
|
||||
Hostile(0x50000010u),
|
||||
PhysicsStateFlags.Hidden);
|
||||
Add(
|
||||
runtime,
|
||||
0x50000011u,
|
||||
0x01010001u,
|
||||
12f,
|
||||
10f,
|
||||
Hostile(0x50000011u),
|
||||
PhysicsStateFlags.NoDraw);
|
||||
Add(
|
||||
runtime,
|
||||
0x50000012u,
|
||||
0x01010001u,
|
||||
13f,
|
||||
10f,
|
||||
Hostile(0x50000012u));
|
||||
runtime.ActionOwner.Combat.OnUpdateHealth(0x50000012u, 0f);
|
||||
Add(
|
||||
runtime,
|
||||
0x50000013u,
|
||||
0x01010001u,
|
||||
14f,
|
||||
10f,
|
||||
PlayerObject(0x50000013u));
|
||||
Add(
|
||||
runtime,
|
||||
0x50000014u,
|
||||
0x01010001u,
|
||||
15f,
|
||||
10f,
|
||||
Hostile(0x50000014u, petOwner: Player));
|
||||
Add(
|
||||
runtime,
|
||||
0x50000015u,
|
||||
0x01010001u,
|
||||
16f,
|
||||
10f,
|
||||
new ClientObject
|
||||
{
|
||||
ObjectId = 0x50000015u,
|
||||
Type = ItemType.Misc,
|
||||
PublicWeenieBitfield =
|
||||
SelectedObjectHealthPolicy.BfAttackable,
|
||||
});
|
||||
Add(
|
||||
runtime,
|
||||
0x50000016u,
|
||||
0x01010001u,
|
||||
17f,
|
||||
10f,
|
||||
new ClientObject
|
||||
{
|
||||
ObjectId = 0x50000016u,
|
||||
Type = ItemType.Creature,
|
||||
});
|
||||
Add(
|
||||
runtime,
|
||||
0x50000017u,
|
||||
0x01010001u,
|
||||
18f,
|
||||
10f,
|
||||
Hostile(0x50000017u));
|
||||
|
||||
Assert.Equal(
|
||||
0x50000017u,
|
||||
RuntimeHostileTargetQuery.FindClosest(runtime));
|
||||
Assert.False(RuntimeHostileTargetQuery.IsHostile(runtime, 0u));
|
||||
Assert.False(
|
||||
RuntimeHostileTargetQuery.IsHostile(
|
||||
runtime,
|
||||
0x50000010u));
|
||||
Assert.False(
|
||||
RuntimeHostileTargetQuery.IsHostile(
|
||||
runtime,
|
||||
0x50000012u));
|
||||
Assert.False(
|
||||
RuntimeHostileTargetQuery.IsHostile(
|
||||
runtime,
|
||||
0x50000014u));
|
||||
Assert.True(
|
||||
RuntimeHostileTargetQuery.IsHostile(
|
||||
runtime,
|
||||
0x50000017u));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindClosest_ReturnsNullWithoutPlayerPositionOrHostile()
|
||||
{
|
||||
using GameRuntime runtime = Create();
|
||||
runtime.PlayerIdentity.ServerGuid = Player;
|
||||
runtime.InventoryOwner.Objects.AddOrUpdate(PlayerObject(Player));
|
||||
|
||||
Assert.Null(RuntimeHostileTargetQuery.FindClosest(runtime));
|
||||
|
||||
Add(runtime, Player, 0x01010001u, 10f, 10f, PlayerObject(Player));
|
||||
Add(
|
||||
runtime,
|
||||
0x50000010u,
|
||||
0x01010001u,
|
||||
11f,
|
||||
10f,
|
||||
new ClientObject
|
||||
{
|
||||
ObjectId = 0x50000010u,
|
||||
Type = ItemType.Creature,
|
||||
});
|
||||
|
||||
Assert.Null(RuntimeHostileTargetQuery.FindClosest(runtime));
|
||||
}
|
||||
|
||||
private static GameRuntime Create()
|
||||
{
|
||||
var operations = new Operations();
|
||||
return new GameRuntime(new GameRuntimeDependencies(
|
||||
operations,
|
||||
operations,
|
||||
operations,
|
||||
operations));
|
||||
}
|
||||
|
||||
private static ClientObject PlayerObject(uint id) => new()
|
||||
{
|
||||
ObjectId = id,
|
||||
Type = ItemType.Creature,
|
||||
PublicWeenieBitfield =
|
||||
SelectedObjectHealthPolicy.BfPlayer,
|
||||
};
|
||||
|
||||
private static ClientObject Hostile(
|
||||
uint id,
|
||||
uint petOwner = 0u) => new()
|
||||
{
|
||||
ObjectId = id,
|
||||
Type = ItemType.Creature,
|
||||
PublicWeenieBitfield =
|
||||
SelectedObjectHealthPolicy.BfAttackable,
|
||||
PetOwnerId = petOwner,
|
||||
};
|
||||
|
||||
private static void Add(
|
||||
GameRuntime runtime,
|
||||
uint guid,
|
||||
uint landblock,
|
||||
float x,
|
||||
float y,
|
||||
ClientObject item,
|
||||
PhysicsStateFlags state = 0)
|
||||
{
|
||||
RuntimeEntityRecord record = runtime.EntityObjects
|
||||
.RegisterEntity(
|
||||
Spawn(guid, landblock, x, y, state))
|
||||
.Canonical!;
|
||||
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
|
||||
record,
|
||||
record.CreateIntegrationVersion,
|
||||
record.Snapshot,
|
||||
replaceGeneration: false));
|
||||
runtime.InventoryOwner.Objects.AddOrUpdate(item);
|
||||
}
|
||||
|
||||
private static WorldSession.EntitySpawn Spawn(
|
||||
uint guid,
|
||||
uint landblock,
|
||||
float x,
|
||||
float y,
|
||||
PhysicsStateFlags state)
|
||||
{
|
||||
var position = new CreateObject.ServerPosition(
|
||||
landblock,
|
||||
x,
|
||||
y,
|
||||
5f,
|
||||
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)state,
|
||||
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,
|
||||
guid.ToString("X8"),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
PhysicsState: physics.RawState,
|
||||
InstanceSequence: 1,
|
||||
MovementSequence: 1,
|
||||
ServerControlSequence: 1,
|
||||
PositionSequence: 1,
|
||||
Physics: physics);
|
||||
}
|
||||
|
||||
private sealed class Operations :
|
||||
IRuntimeCombatAttackOperations,
|
||||
IRuntimeCombatTargetOperations,
|
||||
IRuntimeCombatModeOperations,
|
||||
IRuntimeSpellCastOperations
|
||||
{
|
||||
public bool CanStartAttack() => false;
|
||||
public void PrepareAttackRequest() { }
|
||||
public bool SendAttack(AttackHeight height, float power) => false;
|
||||
public void SendCancelAttack() { }
|
||||
public bool IsDualWield => false;
|
||||
public bool PlayerReadyForAttack => false;
|
||||
public bool AutoRepeatAttack => false;
|
||||
public bool AutoTarget => false;
|
||||
public uint? SelectClosestTarget() => null;
|
||||
public bool IsInWorld => false;
|
||||
public IReadOnlyList<ClientObject> GetOrderedEquipment() => [];
|
||||
public void NotifyExplicitCombatModeRequest() { }
|
||||
public void SendChangeCombatMode(CombatMode mode) { }
|
||||
public uint LocalPlayerId => 0u;
|
||||
public bool CanSend => false;
|
||||
public bool HasRequiredComponents(uint spellId) => false;
|
||||
public bool IsTargetCompatible(
|
||||
uint targetId,
|
||||
SpellMetadata spell,
|
||||
bool showMessage) => false;
|
||||
public void StopCompletely() { }
|
||||
public void SendUntargeted(uint spellId) { }
|
||||
public void SendTargeted(uint targetId, uint spellId) { }
|
||||
public void DisplayMessage(string message) { }
|
||||
public void IncrementBusy() { }
|
||||
}
|
||||
}
|
||||
|
|
@ -40,14 +40,87 @@ public sealed class RuntimeLocalPlayerMovementStateTests
|
|||
public void GraphicalAndDirectCommandsMutateOneAutorunLatch()
|
||||
{
|
||||
using var movement = new RuntimeLocalPlayerMovementState();
|
||||
var input = new MovementInput(
|
||||
Forward: true,
|
||||
Run: true);
|
||||
|
||||
Assert.True(movement.Execute(RuntimeMovementCommand.ToggleRunLock));
|
||||
movement.SetCommandInput(input);
|
||||
Assert.True(movement.AutoRunActive);
|
||||
Assert.True(movement.View.Snapshot.AutoRunActive);
|
||||
Assert.True(movement.HasCommandInput);
|
||||
Assert.Equal(input, movement.CommandInput);
|
||||
Assert.Equal(input, movement.View.Snapshot.CommandInput);
|
||||
|
||||
Assert.True(movement.Execute(RuntimeMovementCommand.Stop));
|
||||
Assert.False(movement.AutoRunActive);
|
||||
Assert.False(movement.View.Snapshot.AutoRunActive);
|
||||
Assert.False(movement.HasCommandInput);
|
||||
Assert.False(movement.View.Snapshot.HasCommandInput);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommandInputIsDeduplicatedAndResetWithSessionIntent()
|
||||
{
|
||||
using var movement = new RuntimeLocalPlayerMovementState();
|
||||
var input = new MovementInput(
|
||||
StrafeLeft: true,
|
||||
Run: true);
|
||||
|
||||
movement.SetCommandInput(input);
|
||||
long revision = movement.Revision;
|
||||
movement.SetCommandInput(input);
|
||||
|
||||
Assert.Equal(revision, movement.Revision);
|
||||
Assert.True(movement.Snapshot.HasCommandInput);
|
||||
|
||||
movement.ResetInputIntent();
|
||||
|
||||
Assert.False(movement.HasCommandInput);
|
||||
Assert.Equal(default, movement.CommandInput);
|
||||
Assert.Equal(revision + 1, movement.Revision);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(RuntimeMovementCommand.Ready, MotionCommand.Ready)]
|
||||
[InlineData(RuntimeMovementCommand.Crouch, MotionCommand.Crouch)]
|
||||
[InlineData(RuntimeMovementCommand.Sit, MotionCommand.Sitting)]
|
||||
[InlineData(RuntimeMovementCommand.Sleep, MotionCommand.Sleeping)]
|
||||
public void StanceCommandsUseCanonicalControllerAndEmitOneMovementEdge(
|
||||
RuntimeMovementCommand command,
|
||||
uint expectedMotion)
|
||||
{
|
||||
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||
using var movement = new RuntimeLocalPlayerMovementState
|
||||
{
|
||||
Controller = controller,
|
||||
};
|
||||
movement.SetCommandInput(
|
||||
new MovementInput(Forward: true, Run: true));
|
||||
|
||||
Assert.True(movement.Execute(command));
|
||||
Assert.False(movement.HasCommandInput);
|
||||
Assert.Equal(
|
||||
expectedMotion,
|
||||
controller.Motion.RawState.ForwardCommand);
|
||||
|
||||
MovementResult first = controller.Update(
|
||||
1f / 60f,
|
||||
default);
|
||||
MovementResult second = controller.Update(
|
||||
1f / 60f,
|
||||
default);
|
||||
|
||||
Assert.True(first.ShouldSendMovementEvent);
|
||||
Assert.False(second.ShouldSendMovementEvent);
|
||||
if (command == RuntimeMovementCommand.Ready)
|
||||
{
|
||||
Assert.Null(first.ForwardCommand);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(expectedMotion, first.ForwardCommand);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue