feat(mosstank): add VTank-style automation PoC
This commit is contained in:
parent
f6fe0f2a4f
commit
4e6e9bc9d9
212 changed files with 49462 additions and 416 deletions
|
|
@ -424,6 +424,63 @@ public sealed class PlayerMouseLookMovementTests
|
|||
Assert.False(controller.PrepareForAttackRequest());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PersistentCommandRetakesTurnAfterServerPostureAcknowledgement()
|
||||
{
|
||||
var controller = CreateController();
|
||||
var command = new MovementInput(
|
||||
TurnRight: true,
|
||||
IsPersistentCommand: true);
|
||||
|
||||
MovementResult started = controller.Update(1f / 60f, command);
|
||||
Assert.Equal(MotionCommand.TurnRight, started.TurnCommand);
|
||||
|
||||
// The live MossTank failure: ACE acknowledges an explicit combat-mode
|
||||
// change with a non-autonomous Magic + Ready movement event after the
|
||||
// plugin has already begun facing its target.
|
||||
controller.SetLastMoveWasAutonomous(false);
|
||||
controller.Motion.MoveToInterpretedState(new InboundInterpretedState
|
||||
{
|
||||
CurrentStyle = 0x80000049u,
|
||||
ForwardCommand = MotionCommand.Ready,
|
||||
ForwardSpeed = 1f,
|
||||
TurnCommand = 0u,
|
||||
TurnSpeed = 1f,
|
||||
});
|
||||
Assert.Equal(0u, controller.Motion.InterpretedState.TurnCommand);
|
||||
|
||||
MovementResult resumed = controller.Update(1f / 60f, command);
|
||||
|
||||
Assert.True(resumed.ShouldSendMovementEvent);
|
||||
Assert.Equal(
|
||||
MotionCommand.TurnRight,
|
||||
controller.Motion.InterpretedState.TurnCommand);
|
||||
Assert.Equal(MotionCommand.TurnRight, resumed.TurnCommand);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhysicalHeldKeyKeepsRetailEdgeBehaviorAfterServerPosture()
|
||||
{
|
||||
var controller = CreateController();
|
||||
var physical = new MovementInput(TurnRight: true);
|
||||
_ = controller.Update(1f / 60f, physical);
|
||||
|
||||
controller.SetLastMoveWasAutonomous(false);
|
||||
controller.Motion.MoveToInterpretedState(new InboundInterpretedState
|
||||
{
|
||||
CurrentStyle = 0x80000049u,
|
||||
ForwardCommand = MotionCommand.Ready,
|
||||
ForwardSpeed = 1f,
|
||||
TurnCommand = 0u,
|
||||
TurnSpeed = 1f,
|
||||
});
|
||||
|
||||
MovementResult held = controller.Update(1f / 60f, physical);
|
||||
|
||||
Assert.False(held.ShouldSendMovementEvent);
|
||||
Assert.Equal(0u, controller.Motion.InterpretedState.TurnCommand);
|
||||
}
|
||||
|
||||
private static PlayerMovementController CreateController()
|
||||
{
|
||||
var engine = new PhysicsEngine();
|
||||
|
|
|
|||
|
|
@ -7,6 +7,33 @@ namespace AcDream.Runtime.Tests.Gameplay;
|
|||
|
||||
public sealed class RuntimeActionStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void HealthActivityPublishesMonotonicRevisionAndAgeAndResets()
|
||||
{
|
||||
double now = 10d;
|
||||
using var inventory = NewInventoryTransactions();
|
||||
using var actions = RuntimeActionTestFactory.Create(inventory, () => now);
|
||||
|
||||
actions.Combat.OnUpdateHealth(0x50000001u, 0.75f);
|
||||
now = 12.5d;
|
||||
|
||||
Assert.True(actions.TryGetHealthActivity(
|
||||
0x50000001u, out long firstRevision, out double age));
|
||||
Assert.Equal(1, firstRevision);
|
||||
Assert.Equal(2.5d, age, 3);
|
||||
|
||||
actions.Combat.OnUpdateHealth(0x50000001u, 0.5f);
|
||||
Assert.True(actions.TryGetHealthActivity(
|
||||
0x50000001u, out long secondRevision, out age));
|
||||
Assert.True(secondRevision > firstRevision);
|
||||
Assert.Equal(0d, age, 3);
|
||||
|
||||
actions.ResetSession();
|
||||
Assert.False(actions.TryGetHealthActivity(
|
||||
0x50000001u, out _, out double missingAge));
|
||||
Assert.True(double.IsPositiveInfinity(missingAge));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ViewProjectsTheExactCanonicalChildrenAndRevisions()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -183,6 +183,27 @@ public sealed class RuntimeCombatAttackStateTests
|
|||
Assert.Equal(0, cancels);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttackDonePublishesOneCompletionReceiptAndResetClearsIt()
|
||||
{
|
||||
var combat = new CombatState();
|
||||
using var controller = new RuntimeCombatAttackState(
|
||||
combat,
|
||||
canStartAttack: () => true,
|
||||
sendAttack: (_, _) => true);
|
||||
|
||||
combat.OnAttackDone(47u, 0x1234u);
|
||||
|
||||
Assert.Equal(1, controller.CompletionRevision);
|
||||
Assert.Equal(47u, controller.CompletionSequence);
|
||||
Assert.Equal(0x1234u, controller.CompletionWeenieError);
|
||||
|
||||
controller.ResetSession();
|
||||
Assert.Equal(0, controller.CompletionRevision);
|
||||
Assert.Equal(0u, controller.CompletionSequence);
|
||||
Assert.Equal(0u, controller.CompletionWeenieError);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartAttackRequest_PreparesPlayerMovementBeforePowerBuild()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -96,6 +96,24 @@ public sealed class RuntimeCombatModeStateTests
|
|||
Assert.Equal(["intent", "equipment", "send:Melee"], operations.Trace);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExplicitRequestSendsChosenPluginModeWithoutEquipmentGuessing()
|
||||
{
|
||||
var combat = new CombatState();
|
||||
var operations = new Operations();
|
||||
combat.CombatModeChanged += mode =>
|
||||
operations.Trace.Add($"state:{mode}");
|
||||
var state = new RuntimeCombatModeState(combat, operations);
|
||||
|
||||
RuntimeCombatModeRequestResult result = state.Request(CombatMode.Magic);
|
||||
|
||||
Assert.Equal(RuntimeCombatModeRequestStatus.Sent, result.Status);
|
||||
Assert.Equal(CombatMode.Magic, result.Mode);
|
||||
Assert.Equal(
|
||||
["intent", "send:Magic", "state:Magic"],
|
||||
operations.Trace);
|
||||
}
|
||||
|
||||
private sealed class Operations : IRuntimeCombatModeOperations
|
||||
{
|
||||
public bool IsInWorld { get; init; } = true;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using AcDream.Core.Items;
|
|||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Properties;
|
||||
using AcDream.Core.Spells;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
|
@ -172,6 +173,43 @@ public sealed class RuntimeHostileTargetQueryTests
|
|||
Assert.Null(RuntimeHostileTargetQuery.FindClosest(runtime));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Capture_ProjectsRetailRelativeHeadingRangeNameAndHealth()
|
||||
{
|
||||
using GameRuntime runtime = Create();
|
||||
runtime.PlayerIdentity.ServerGuid = Player;
|
||||
Add(runtime, Player, 0x01010001u, 10f, 10f, PlayerObject(Player));
|
||||
ClientObject east = Hostile(0x50000020u);
|
||||
east.Name = "East Drudge";
|
||||
east.WeenieClassId = 1234u;
|
||||
east.Properties.Ints[(uint)PropertyInt.CreatureType] = 4;
|
||||
Add(runtime, east.ObjectId, 0x01010001u, 13f, 10f, east);
|
||||
runtime.InventoryOwner.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = 0x70000020u,
|
||||
Type = ItemType.Armor,
|
||||
WielderId = east.ObjectId,
|
||||
CurrentlyEquippedLocation = EquipMask.Shield,
|
||||
});
|
||||
runtime.ActionOwner.Combat.OnUpdateHealth(east.ObjectId, 0.75f);
|
||||
|
||||
IReadOnlyList<RuntimeHostileTargetSnapshot> captured =
|
||||
RuntimeHostileTargetQuery.Capture(runtime, 4f);
|
||||
|
||||
RuntimeHostileTargetSnapshot target = Assert.Single(captured);
|
||||
Assert.Equal(east.ObjectId, target.ObjectId);
|
||||
Assert.Equal("East Drudge", target.Name);
|
||||
Assert.Equal(1234u, target.WeenieClassId);
|
||||
Assert.Equal(3f, target.Distance, 3);
|
||||
Assert.Equal(90f, target.RelativeAngleDegrees, 3);
|
||||
Assert.True(target.IsHealthKnown);
|
||||
Assert.Equal(0.75f, target.HealthFraction, 3);
|
||||
Assert.Equal(4, target.SpeciesId);
|
||||
Assert.True(target.HasShield);
|
||||
Assert.Equal(0, target.MaximumHealth);
|
||||
Assert.Empty(RuntimeHostileTargetQuery.Capture(runtime, 2.9f));
|
||||
}
|
||||
|
||||
private static GameRuntime Create()
|
||||
{
|
||||
var operations = new Operations();
|
||||
|
|
|
|||
|
|
@ -54,6 +54,70 @@ public sealed class RuntimeInteractionTransactionStateTests
|
|||
|
||||
state.CompleteUse(0u);
|
||||
Assert.Equal(0, inventory.BusyCount);
|
||||
Assert.Equal(new RuntimeItemUseCompletion(1, Item, 0u, 0u),
|
||||
state.LastItemUseCompletion);
|
||||
Assert.False(state.CaptureOwnership().AwaitingItemUseCompletion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TargetedItemUsePublishesExactAuthoritativeFailureReceipt()
|
||||
{
|
||||
using var inventory = NewInventory(out _);
|
||||
using var state = new RuntimeInteractionTransactionState(inventory);
|
||||
|
||||
Assert.True(state.TryDispatchTargetedUse(
|
||||
Item,
|
||||
Container,
|
||||
static (_, _) => { },
|
||||
incrementBusy: true));
|
||||
Assert.True(state.CaptureOwnership().AwaitingItemUseCompletion);
|
||||
|
||||
state.CompleteUse(0x0402u);
|
||||
|
||||
Assert.Equal(
|
||||
new RuntimeItemUseCompletion(1, Item, Container, 0x0402u),
|
||||
state.LastItemUseCompletion);
|
||||
Assert.False(state.LastItemUseCompletion.IsSuccess);
|
||||
Assert.False(state.CaptureOwnership().AwaitingItemUseCompletion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseDoneWithoutPluginItemRequestCannotFabricateItemReceipt()
|
||||
{
|
||||
using var inventory = NewInventory(out _);
|
||||
using var state = new RuntimeInteractionTransactionState(inventory);
|
||||
state.IncrementBusyCount();
|
||||
|
||||
state.CompleteUse(0u);
|
||||
|
||||
Assert.Equal(default, state.LastItemUseCompletion);
|
||||
Assert.Equal(0, inventory.BusyCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItemUseReceiptIsGenerationScoped()
|
||||
{
|
||||
using var inventory = NewInventory(out _);
|
||||
using var state = new RuntimeInteractionTransactionState(inventory);
|
||||
Assert.True(state.TryDispatchTargetedUse(
|
||||
Item,
|
||||
Container,
|
||||
static (_, _) => { },
|
||||
incrementBusy: true));
|
||||
state.CompleteUse(0u);
|
||||
Assert.True(state.LastItemUseCompletion.IsSuccess);
|
||||
|
||||
state.ResetSession();
|
||||
|
||||
Assert.Equal(default, state.LastItemUseCompletion);
|
||||
Assert.True(state.TryDispatchTargetedUse(
|
||||
Container,
|
||||
Item,
|
||||
static (_, _) => { },
|
||||
incrementBusy: true));
|
||||
state.CompleteUse(0u);
|
||||
Assert.Equal(1, state.LastItemUseCompletion.Revision);
|
||||
Assert.Equal(Container, state.LastItemUseCompletion.SourceObjectId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
|
|||
|
|
@ -54,6 +54,40 @@ public sealed class RuntimeLocalPlayerMovementStateTests
|
|||
Assert.Equal(movement.Revision, snapshot.Revision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ViewProjectsLiveBodyOrientationAfterLocalTurn()
|
||||
{
|
||||
var controller = new PlayerMovementController(new PhysicsEngine())
|
||||
{
|
||||
LocalEntityId = 0x50000001u,
|
||||
};
|
||||
controller.SeedPlacementForTest(
|
||||
new Vector3(11f, 12f, 13f),
|
||||
0xA9B40001u,
|
||||
new Vector3(11f, 12f, 13f));
|
||||
using var movement = new RuntimeLocalPlayerMovementState
|
||||
{
|
||||
Controller = controller,
|
||||
};
|
||||
Quaternion turned = AcDream.Core.Physics.Motion.MoveToMath.SetHeading(
|
||||
Quaternion.Identity,
|
||||
90f);
|
||||
|
||||
controller.SetBodyOrientation(turned);
|
||||
|
||||
// PhysicsBody.CellPosition deliberately retains the carried frame
|
||||
// rotation. RuntimeMovementSnapshot is the live consumer projection
|
||||
// used by plugins and must expose the body's current facing instead.
|
||||
Assert.NotEqual(turned, controller.CellPosition.Frame.Orientation);
|
||||
RuntimeMovementSnapshot snapshot = movement.View.Snapshot;
|
||||
Assert.Equal(turned, snapshot.Position.Frame.Orientation);
|
||||
Assert.Equal(
|
||||
90f,
|
||||
AcDream.Core.Physics.Motion.MoveToMath.GetHeading(
|
||||
snapshot.Position.Frame.Orientation),
|
||||
precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GraphicalAndDirectCommandsMutateOneAutorunLatch()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -103,6 +103,40 @@ public sealed class RuntimeSpellCastStateTests
|
|||
Assert.Null(state.LastRequestedSpellId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompleteUse_PublishesOneExactServerReceipt()
|
||||
{
|
||||
Spellbook book = MakeBook(flags: 0, untargeted: false, targetMask: 0x10);
|
||||
var operations = new FakeOperations { LocalPlayerId = 42u };
|
||||
RuntimeSpellCastState state = Create(book, operations, selected: 99u);
|
||||
|
||||
Assert.Equal(CastRequestResult.Sent, state.Cast(1));
|
||||
Assert.Equal(1u, state.PendingSpellId);
|
||||
Assert.Equal(99u, state.PendingTargetId);
|
||||
Assert.True(state.CompleteUse(0x0402u));
|
||||
|
||||
Assert.Equal(
|
||||
new RuntimeSpellCastCompletion(1, 1u, 99u, 0x0402u),
|
||||
state.LastCompletion);
|
||||
Assert.Null(state.PendingSpellId);
|
||||
Assert.False(state.CompleteUse(0u));
|
||||
Assert.Equal(1, state.LastCompletion.Revision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cast_DoesNotOverwritePendingReceiptIdentity()
|
||||
{
|
||||
Spellbook book = MakeBook(flags: 0, untargeted: false, targetMask: 0x10);
|
||||
var operations = new FakeOperations { LocalPlayerId = 42u };
|
||||
RuntimeSpellCastState state = Create(book, operations, selected: 99u);
|
||||
|
||||
Assert.Equal(CastRequestResult.Sent, state.Cast(1));
|
||||
Assert.Equal(CastRequestResult.Unavailable, state.Cast(1));
|
||||
Assert.Equal(1, operations.TargetedSends);
|
||||
Assert.Equal(1u, state.PendingSpellId);
|
||||
Assert.Equal(99u, state.PendingTargetId);
|
||||
}
|
||||
|
||||
private static RuntimeSpellCastState Create(
|
||||
Spellbook book,
|
||||
FakeOperations operations,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue