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
623
tests/AcDream.Plugins.MossTank.Tests/NavigationTests.cs
Normal file
623
tests/AcDream.Plugins.MossTank.Tests/NavigationTests.cs
Normal file
|
|
@ -0,0 +1,623 @@
|
|||
using AcDream.Plugin.Abstractions;
|
||||
|
||||
namespace AcDream.Plugins.MossTank.Tests;
|
||||
|
||||
public sealed class NavigationTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(0d, 1d, 0f)]
|
||||
[InlineData(1d, 0d, 90f)]
|
||||
[InlineData(0d, -1d, 180f)]
|
||||
[InlineData(-1d, 0d, 270f)]
|
||||
public void DesiredHeadingUsesVtankCompassConvention(
|
||||
double eastWest,
|
||||
double northSouth,
|
||||
float expected)
|
||||
{
|
||||
PluginNavigationPosition origin = Position(0d, 0d);
|
||||
PluginNavigationPosition target = Position(eastWest, northSouth);
|
||||
|
||||
Assert.Equal(expected, NavigationController.DesiredHeading(origin, target));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(350f, 10f, 20f)]
|
||||
[InlineData(10f, 350f, -20f)]
|
||||
[InlineData(90f, 270f, 180f)]
|
||||
public void SignedHeadingDeltaChoosesShortestRetailTurn(
|
||||
float current,
|
||||
float desired,
|
||||
float expected) =>
|
||||
Assert.Equal(expected, NavigationController.SignedHeadingDelta(current, desired));
|
||||
|
||||
[Fact]
|
||||
public void PointSteeringTurnsInPlaceOutsideFortyFiveDegrees()
|
||||
{
|
||||
var automation = new FakeAutomation
|
||||
{
|
||||
NavigationSnapshot = Snapshot(Position(0d, 0d, heading: 0f)),
|
||||
};
|
||||
NavigationController controller = Controller(
|
||||
automation,
|
||||
RouteMode.Circular,
|
||||
Waypoint(RouteWaypointType.Point, Position(1d, 0d)));
|
||||
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
|
||||
PluginMovementIntent intent = Assert.Single(automation.Intents);
|
||||
Assert.False(intent.Forward);
|
||||
Assert.True(intent.TurnRight);
|
||||
Assert.False(intent.TurnLeft);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PointSteeringMovesWhileTurningInsideFarFortyFiveDegreeCone()
|
||||
{
|
||||
var automation = new FakeAutomation
|
||||
{
|
||||
NavigationSnapshot = Snapshot(Position(0d, 0d, heading: 60f)),
|
||||
};
|
||||
NavigationController controller = Controller(
|
||||
automation,
|
||||
RouteMode.Circular,
|
||||
Waypoint(RouteWaypointType.Point, Position(1d, 0d)));
|
||||
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
|
||||
PluginMovementIntent intent = Assert.Single(automation.Intents);
|
||||
Assert.True(intent.Forward);
|
||||
Assert.True(intent.TurnRight);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CircularRouteWrapsAndOnceRouteStops()
|
||||
{
|
||||
RouteWaypoint first = Waypoint(RouteWaypointType.Point, Position(0d, 0d));
|
||||
RouteWaypoint second = Waypoint(RouteWaypointType.Point, Position(1d, 0d));
|
||||
var circularAutomation = new FakeAutomation
|
||||
{
|
||||
NavigationSnapshot = Snapshot(first.Position),
|
||||
};
|
||||
NavigationController circular = Controller(
|
||||
circularAutomation,
|
||||
RouteMode.Circular,
|
||||
first,
|
||||
second);
|
||||
|
||||
Assert.True(circular.Tick(0.05d, canAct: true));
|
||||
Assert.Equal(1, circular.CurrentWaypointIndex);
|
||||
circularAutomation.NavigationSnapshot = Snapshot(second.Position);
|
||||
Assert.True(circular.Tick(0.05d, canAct: true));
|
||||
Assert.Equal(0, circular.CurrentWaypointIndex);
|
||||
|
||||
var onceAutomation = new FakeAutomation
|
||||
{
|
||||
NavigationSnapshot = Snapshot(first.Position),
|
||||
};
|
||||
NavigationController once = Controller(
|
||||
onceAutomation,
|
||||
RouteMode.Once,
|
||||
first);
|
||||
|
||||
Assert.True(once.Tick(0.05d, canAct: true));
|
||||
Assert.False(once.Tick(0.05d, canAct: true));
|
||||
Assert.Equal("Once route complete.", once.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FollowReadsMovingTargetAndHoldsAtMinimumDistance()
|
||||
{
|
||||
var automation = new FakeAutomation
|
||||
{
|
||||
NavigationSnapshot = Snapshot(Position(0d, 0d, heading: 90f)),
|
||||
};
|
||||
automation.Objects[7u] = new PluginNavigationObject(
|
||||
7u,
|
||||
"Leader",
|
||||
Position(1d, 0d));
|
||||
var settings = new NavigationSettings
|
||||
{
|
||||
Enabled = true,
|
||||
Mode = RouteMode.Target,
|
||||
MinimumDistanceMeters = 2d,
|
||||
FollowTargetObjectId = 7u,
|
||||
};
|
||||
var controller = new NavigationController(new FakeHost(automation), settings);
|
||||
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
Assert.True(Assert.Single(automation.Intents).Forward);
|
||||
|
||||
automation.Objects[7u] = new PluginNavigationObject(
|
||||
7u,
|
||||
"Leader",
|
||||
Position(0.005d, 0d));
|
||||
Assert.False(controller.Tick(0.05d, canAct: true));
|
||||
Assert.Equal(1, automation.ClearCount);
|
||||
Assert.Contains("holding", controller.Status, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FollowAroundCornersUsesOldestUnreachedBreadcrumb()
|
||||
{
|
||||
var automation = new FakeAutomation
|
||||
{
|
||||
NavigationSnapshot = Snapshot(Position(0d, 0d, heading: 90f)),
|
||||
};
|
||||
automation.Objects[7u] = new PluginNavigationObject(
|
||||
7u,
|
||||
"Leader",
|
||||
Position(0.1d, 0d));
|
||||
var settings = new NavigationSettings
|
||||
{
|
||||
Enabled = true,
|
||||
Mode = RouteMode.Target,
|
||||
MinimumDistanceMeters = 2d,
|
||||
FollowTargetObjectId = 7u,
|
||||
FollowAroundCorners = true,
|
||||
};
|
||||
var controller = new NavigationController(new FakeHost(automation), settings);
|
||||
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
automation.Objects[7u] = new PluginNavigationObject(
|
||||
7u,
|
||||
"Leader",
|
||||
Position(0.1d, 0.1d));
|
||||
automation.Intents.Clear();
|
||||
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
|
||||
PluginMovementIntent intent = Assert.Single(automation.Intents);
|
||||
Assert.True(intent.Forward);
|
||||
Assert.False(intent.TurnLeft);
|
||||
Assert.False(intent.TurnRight);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckpointWaitsForServerAcceptedPosition()
|
||||
{
|
||||
PluginNavigationPosition point = Position(0d, 0d);
|
||||
var automation = new FakeAutomation
|
||||
{
|
||||
NavigationSnapshot = Snapshot(point) with
|
||||
{
|
||||
ConfirmedPosition = Position(0.1d, 0d),
|
||||
ConfirmedPositionRevision = 4UL,
|
||||
},
|
||||
};
|
||||
NavigationController controller = Controller(
|
||||
automation,
|
||||
RouteMode.Once,
|
||||
Waypoint(RouteWaypointType.Checkpoint, point));
|
||||
|
||||
Assert.True(controller.Tick(1d, canAct: true));
|
||||
Assert.Contains("waiting for server", controller.Status, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.True(controller.Tick(14d, canAct: true));
|
||||
Assert.True(Assert.Single(automation.Intents).Forward);
|
||||
|
||||
automation.NavigationSnapshot = Snapshot(point) with
|
||||
{
|
||||
ConfirmedPosition = point,
|
||||
ConfirmedPositionRevision = 5UL,
|
||||
};
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
Assert.False(controller.Tick(0.05d, canAct: true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClosedDoorPausesRouteAndUsesCanonicalItemAction()
|
||||
{
|
||||
var automation = new FakeAutomation
|
||||
{
|
||||
NavigationSnapshot = Snapshot(Position(0d, 0d, heading: 90f)),
|
||||
};
|
||||
automation.WorldObjects.Add(new PluginNavigationObject(
|
||||
55u,
|
||||
"Dungeon Door",
|
||||
Position(0.01d, 0d))
|
||||
{
|
||||
IsDoor = true,
|
||||
IsOpen = false,
|
||||
HasLockState = true,
|
||||
});
|
||||
var settings = new NavigationSettings
|
||||
{
|
||||
Enabled = true,
|
||||
OpenDoors = true,
|
||||
Mode = RouteMode.Circular,
|
||||
};
|
||||
settings.Waypoints.Add(Waypoint(
|
||||
RouteWaypointType.Point,
|
||||
Position(1d, 0d)));
|
||||
var controller = new NavigationController(new FakeHost(automation), settings);
|
||||
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
Assert.Equal([55u], automation.UsedObjects);
|
||||
Assert.Empty(automation.Intents);
|
||||
|
||||
automation.WorldObjects[0] = automation.WorldObjects[0] with
|
||||
{
|
||||
IsOpen = true,
|
||||
};
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
Assert.True(Assert.Single(automation.Intents).Forward);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PauseAndChatActionsObserveOfficialInitialDelay()
|
||||
{
|
||||
var automation = new FakeAutomation
|
||||
{
|
||||
NavigationSnapshot = Snapshot(Position(0d, 0d)),
|
||||
};
|
||||
RouteWaypoint pause = Waypoint(RouteWaypointType.Pause, Position(0d, 0d));
|
||||
pause.DurationMilliseconds = 100;
|
||||
RouteWaypoint chat = Waypoint(RouteWaypointType.ChatCommand, Position(0d, 0d));
|
||||
chat.Text = "/say route";
|
||||
NavigationController controller = Controller(
|
||||
automation,
|
||||
RouteMode.Once,
|
||||
pause,
|
||||
chat);
|
||||
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
Assert.Equal(0, controller.CurrentWaypointIndex);
|
||||
Assert.True(controller.Tick(0.19d, canAct: true));
|
||||
Assert.Empty(automation.SubmittedChat);
|
||||
Assert.True(controller.Tick(0.01d, canAct: true));
|
||||
Assert.Equal(["/say route"], automation.SubmittedChat);
|
||||
Assert.False(controller.Tick(0.01d, canAct: true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PortalWaypointWaitsForPortalExitRatherThanUseDispatch()
|
||||
{
|
||||
var automation = new FakeAutomation
|
||||
{
|
||||
NavigationSnapshot = Snapshot(Position(0d, 0d)),
|
||||
ItemCompletion = new PluginItemUseCompletion(4, 10u, 0u, 0u),
|
||||
};
|
||||
RouteWaypoint use = Waypoint(RouteWaypointType.Portal, Position(0d, 0d));
|
||||
use.ObjectId = 77u;
|
||||
use.ObjectName = "Town Crier";
|
||||
NavigationController controller = Controller(automation, RouteMode.Once, use);
|
||||
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
Assert.Equal([77u], automation.UsedObjects);
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
automation.ItemCompletion = new PluginItemUseCompletion(5, 77u, 0u, 0u);
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
automation.NavigationSnapshot = automation.NavigationSnapshot with
|
||||
{
|
||||
IsPortalSpace = true,
|
||||
};
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
automation.NavigationSnapshot = Snapshot(Position(0.1d, 0d));
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
Assert.False(controller.Tick(0.05d, canAct: true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseNpcRepeatsUntilTheNpcRespondsInChat()
|
||||
{
|
||||
var automation = new FakeAutomation
|
||||
{
|
||||
NavigationSnapshot = Snapshot(Position(0d, 0d)),
|
||||
FoundObject = new PluginNavigationObject(
|
||||
91u,
|
||||
"Town Crier",
|
||||
Position(0.01d, 0d)),
|
||||
};
|
||||
RouteWaypoint use = Waypoint(RouteWaypointType.UseNpc, Position(0d, 0d));
|
||||
use.ObjectName = "Town Crier";
|
||||
NavigationController controller = Controller(automation, RouteMode.Once, use);
|
||||
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
automation.ChatMessages.Add(new PluginChatMessage(
|
||||
1UL,
|
||||
91u,
|
||||
3,
|
||||
"Town Crier",
|
||||
"Town Crier tells you, Welcome.",
|
||||
string.Empty));
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
Assert.False(controller.Tick(0.05d, canAct: true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NamedNpcWaypointReacquiresChangedObjectId()
|
||||
{
|
||||
var automation = new FakeAutomation
|
||||
{
|
||||
NavigationSnapshot = Snapshot(Position(0d, 0d)),
|
||||
FoundObject = new PluginNavigationObject(
|
||||
91u,
|
||||
"Town Crier",
|
||||
Position(0.01d, 0d)),
|
||||
};
|
||||
RouteWaypoint use = Waypoint(RouteWaypointType.UseNpc, Position(0d, 0d));
|
||||
use.ObjectId = 77u;
|
||||
use.ObjectName = "Town Crier";
|
||||
NavigationController controller = Controller(automation, RouteMode.Once, use);
|
||||
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
|
||||
Assert.Equal(91u, use.ObjectId);
|
||||
Assert.Equal([91u], automation.UsedObjects);
|
||||
Assert.Equal("Town Crier", automation.FindName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JumpAlignsBeforeChargingAndWaitsForLanding()
|
||||
{
|
||||
var automation = new FakeAutomation
|
||||
{
|
||||
NavigationSnapshot = Snapshot(Position(0d, 0d, heading: 0f)),
|
||||
};
|
||||
RouteWaypoint jump = Waypoint(RouteWaypointType.Jump, Position(0d, 0d));
|
||||
jump.JumpHeadingDegrees = 90f;
|
||||
jump.JumpChargeMilliseconds = 100;
|
||||
NavigationController controller = Controller(automation, RouteMode.Once, jump);
|
||||
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
PluginMovementIntent turn = Assert.Single(automation.Intents);
|
||||
Assert.True(turn.TurnRight);
|
||||
Assert.False(turn.Jump);
|
||||
|
||||
automation.NavigationSnapshot = Snapshot(Position(0d, 0d, heading: 90f));
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
Assert.True(automation.Intents[^1].Jump);
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
Assert.False(automation.Intents[^1].Jump);
|
||||
|
||||
automation.NavigationSnapshot = Snapshot(
|
||||
Position(0d, 0d, heading: 90f),
|
||||
airborne: true);
|
||||
Assert.True(controller.Tick(0.05d, canAct: true));
|
||||
automation.NavigationSnapshot = Snapshot(Position(0d, 0d, heading: 90f));
|
||||
Assert.True(controller.Tick(0.25d, canAct: true));
|
||||
Assert.False(controller.Tick(0.01d, canAct: true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RouteProfilesRoundTripEveryWaypointField()
|
||||
{
|
||||
var storage = new MemoryStorage();
|
||||
var host = new FakeHost(new FakeAutomation(), storage);
|
||||
var source = new NavigationSettings
|
||||
{
|
||||
Enabled = true,
|
||||
Priority = true,
|
||||
Mode = RouteMode.Linear,
|
||||
MinimumDistanceMeters = 4.5d,
|
||||
FollowTargetObjectId = 99u,
|
||||
FollowTargetName = "Leader",
|
||||
FollowAroundCorners = false,
|
||||
OpenDoors = true,
|
||||
DoorIdentifyRangeMeters = 35d,
|
||||
DoorOpenRangeMeters = 3.5d,
|
||||
DoorLockpickExcessThreshold = 17,
|
||||
};
|
||||
source.Waypoints.Add(new RouteWaypoint
|
||||
{
|
||||
Type = RouteWaypointType.Jump,
|
||||
Position = new PluginNavigationPosition(0x7F7F0001u, 1.2d, -3.4d, 5.6d, 78f, true),
|
||||
ObjectId = 88u,
|
||||
ObjectName = "Portal",
|
||||
Text = "/say hello",
|
||||
DurationMilliseconds = 1234,
|
||||
Recall = RouteRecallKind.SecondaryPortal,
|
||||
JumpHeadingDegrees = 271.5f,
|
||||
JumpRun = true,
|
||||
JumpChargeMilliseconds = 875,
|
||||
JumpDirection = RouteJumpDirection.StrafeRight,
|
||||
});
|
||||
var first = new MossTankRouteProfileStore(host);
|
||||
Assert.True(first.BindCharacter("Test Character"));
|
||||
first.SaveCurrent(source);
|
||||
|
||||
var target = new NavigationSettings();
|
||||
var second = new MossTankRouteProfileStore(host);
|
||||
Assert.True(second.BindCharacter("Test Character"));
|
||||
Assert.True(second.LoadCurrent(target));
|
||||
|
||||
Assert.True(target.Enabled);
|
||||
Assert.True(target.Priority);
|
||||
Assert.Equal(RouteMode.Linear, target.Mode);
|
||||
Assert.Equal(4.5d, target.MinimumDistanceMeters);
|
||||
Assert.Equal(99u, target.FollowTargetObjectId);
|
||||
Assert.False(target.FollowAroundCorners);
|
||||
Assert.True(target.OpenDoors);
|
||||
Assert.Equal(35d, target.DoorIdentifyRangeMeters);
|
||||
Assert.Equal(3.5d, target.DoorOpenRangeMeters);
|
||||
Assert.Equal(17, target.DoorLockpickExcessThreshold);
|
||||
RouteWaypoint waypoint = Assert.Single(target.Waypoints);
|
||||
Assert.Equal(RouteWaypointType.Jump, waypoint.Type);
|
||||
Assert.Equal(271.5f, waypoint.JumpHeadingDegrees);
|
||||
Assert.True(waypoint.JumpRun);
|
||||
Assert.Equal(875, waypoint.JumpChargeMilliseconds);
|
||||
Assert.Equal(RouteJumpDirection.StrafeRight, waypoint.JumpDirection);
|
||||
Assert.Equal(0x7F7F0001u, waypoint.Position.CellId);
|
||||
}
|
||||
|
||||
private static NavigationController Controller(
|
||||
FakeAutomation automation,
|
||||
RouteMode mode,
|
||||
params RouteWaypoint[] waypoints)
|
||||
{
|
||||
var settings = new NavigationSettings
|
||||
{
|
||||
Enabled = true,
|
||||
Mode = mode,
|
||||
MinimumDistanceMeters = 2d,
|
||||
};
|
||||
settings.Waypoints.AddRange(waypoints);
|
||||
return new NavigationController(new FakeHost(automation), settings);
|
||||
}
|
||||
|
||||
private static RouteWaypoint Waypoint(
|
||||
RouteWaypointType type,
|
||||
PluginNavigationPosition position) => new()
|
||||
{
|
||||
Type = type,
|
||||
Position = position,
|
||||
};
|
||||
|
||||
private static PluginNavigationSnapshot Snapshot(
|
||||
PluginNavigationPosition position,
|
||||
bool airborne = false) => new(
|
||||
IsAvailable: true,
|
||||
IsPortalSpace: false,
|
||||
LocalObjectId: 1u,
|
||||
position,
|
||||
IsMoving: false,
|
||||
IsAirborne: airborne);
|
||||
|
||||
private static PluginNavigationPosition Position(
|
||||
double eastWest,
|
||||
double northSouth,
|
||||
float heading = 0f) => new(
|
||||
0x7F7F0001u,
|
||||
eastWest,
|
||||
northSouth,
|
||||
0d,
|
||||
heading,
|
||||
IsOutdoor: true);
|
||||
|
||||
private sealed class FakeHost(
|
||||
FakeAutomation automation,
|
||||
IPluginStorage? storage = null) : IPluginHost
|
||||
{
|
||||
public bool HasUi => false;
|
||||
public IPluginLogger Log { get; } = new FakeLogger();
|
||||
public IGameState State { get; } = new FakeState();
|
||||
public IEvents Events { get; } = new FakeEvents();
|
||||
public ISelectionService Selection { get; } = new FakeSelection();
|
||||
public IUiRegistry Ui => NoOpUiRegistry.Instance;
|
||||
public IPluginStorage Storage { get; } = storage ?? NoOpPluginStorage.Instance;
|
||||
public IAutomationSurface Automation { get; } = automation;
|
||||
}
|
||||
|
||||
private sealed class FakeAutomation
|
||||
: IAutomationSurface, INavigationAutomation, IPluginChat, IItemAutomation
|
||||
{
|
||||
public bool IsAvailable => true;
|
||||
public ICharacterInfo Character => NoOpAutomationSurface.Instance;
|
||||
public ISpellCatalog Spells => NoOpAutomationSurface.Instance;
|
||||
public IMagicCommands Magic => NoOpAutomationSurface.Instance;
|
||||
public IPluginChat Chat => this;
|
||||
public IItemAutomation Items => this;
|
||||
public INavigationAutomation Navigation => this;
|
||||
public PluginNavigationSnapshot NavigationSnapshot { get; set; }
|
||||
PluginNavigationSnapshot INavigationAutomation.Snapshot => NavigationSnapshot;
|
||||
public Dictionary<uint, PluginNavigationObject> Objects { get; } = [];
|
||||
public List<PluginNavigationObject> WorldObjects { get; } = [];
|
||||
public List<PluginMovementIntent> Intents { get; } = [];
|
||||
public List<string> SubmittedChat { get; } = [];
|
||||
public List<PluginChatMessage> ChatMessages { get; } = [];
|
||||
public List<uint> UsedObjects { get; } = [];
|
||||
public int ClearCount { get; private set; }
|
||||
public PluginItemUseCompletion ItemCompletion { get; set; }
|
||||
public PluginItemUseCompletion LastCompletion => ItemCompletion;
|
||||
public PluginNavigationObject? FoundObject { get; set; }
|
||||
public string? FindName { get; private set; }
|
||||
|
||||
public bool TryGetObject(uint objectId, out PluginNavigationObject value) =>
|
||||
Objects.TryGetValue(objectId, out value);
|
||||
|
||||
public bool TryFindObject(
|
||||
string name,
|
||||
in PluginNavigationPosition near,
|
||||
double maximumDistanceMeters,
|
||||
out PluginNavigationObject value)
|
||||
{
|
||||
FindName = name;
|
||||
value = FoundObject ?? default;
|
||||
return FoundObject.HasValue;
|
||||
}
|
||||
|
||||
public IReadOnlyList<PluginNavigationObject> CaptureObjects() =>
|
||||
WorldObjects;
|
||||
|
||||
public PluginNavigationCommandStatus SetMovementIntent(
|
||||
in PluginMovementIntent intent)
|
||||
{
|
||||
Intents.Add(intent);
|
||||
return PluginNavigationCommandStatus.Accepted;
|
||||
}
|
||||
|
||||
public PluginNavigationCommandStatus ClearMovementIntent()
|
||||
{
|
||||
ClearCount++;
|
||||
return PluginNavigationCommandStatus.Accepted;
|
||||
}
|
||||
|
||||
public bool Submit(string text)
|
||||
{
|
||||
SubmittedChat.Add(text);
|
||||
return true;
|
||||
}
|
||||
|
||||
public IReadOnlyList<PluginChatMessage> CaptureMessages(ulong afterSequence) =>
|
||||
ChatMessages.Where(message => message.Sequence > afterSequence).ToArray();
|
||||
|
||||
public void PostSystemMessage(string text) { }
|
||||
|
||||
public PluginItemCommandResult Use(uint objectId)
|
||||
{
|
||||
UsedObjects.Add(objectId);
|
||||
return new PluginItemCommandResult(PluginItemCommandStatus.Started);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class MemoryStorage : IPluginStorage
|
||||
{
|
||||
private readonly Dictionary<string, string> _text = new(StringComparer.Ordinal);
|
||||
public bool IsAvailable => true;
|
||||
public string? ReadText(string key) =>
|
||||
_text.TryGetValue(key, out string? value) ? value : null;
|
||||
public void WriteText(string key, string content) => _text[key] = content;
|
||||
public bool Delete(string key) => _text.Remove(key);
|
||||
}
|
||||
|
||||
private sealed class FakeLogger : IPluginLogger
|
||||
{
|
||||
public void Info(string message) { }
|
||||
public void Warn(string message) { }
|
||||
public void Error(string message, Exception? exception = null) { }
|
||||
}
|
||||
|
||||
private sealed class FakeState : IGameState
|
||||
{
|
||||
public IReadOnlyList<WorldEntitySnapshot> Entities => [];
|
||||
}
|
||||
|
||||
private sealed class FakeEvents : IEvents
|
||||
{
|
||||
public event Action<WorldEntitySnapshot> EntitySpawned
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public event Action<double> Tick
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeSelection : ISelectionService
|
||||
{
|
||||
public uint? SelectedObjectId => null;
|
||||
public uint? PreviousObjectId => null;
|
||||
public event Action<SelectionChangedEvent> Changed
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public bool Select(uint objectId) => false;
|
||||
public bool Clear() => false;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue