277 lines
9.8 KiB
C#
277 lines
9.8 KiB
C#
using AcDream.App.Plugins;
|
|
using AcDream.Core.Chat;
|
|
using AcDream.Core.Items;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Selection;
|
|
using AcDream.Core.Spells;
|
|
using AcDream.Plugin.Abstractions;
|
|
using AcDream.Runtime.Gameplay;
|
|
using System.Numerics;
|
|
|
|
namespace AcDream.App.Tests.Plugins;
|
|
|
|
public sealed class AppAutomationSurfaceTests
|
|
{
|
|
[Fact]
|
|
public void ProjectileDebugSamplesAreDetachedValidatedAndClearedOnUnbind()
|
|
{
|
|
using var surface = new AppAutomationSurface();
|
|
PluginProjectileDebugSample[] source =
|
|
[
|
|
new(new Vector3(1f, 2f, 3f), true, 0.4f),
|
|
new(new Vector3(float.NaN, 0f, 0f), false, 0.4f),
|
|
];
|
|
|
|
surface.Projectiles.ShowDebugSamples(source);
|
|
source[0] = default;
|
|
|
|
PluginProjectileDebugSample sample = Assert.Single(
|
|
surface.CaptureProjectileDebugSamples());
|
|
Assert.Equal(new Vector3(1f, 2f, 3f), sample.WorldPosition);
|
|
Assert.True(sample.IsClear);
|
|
|
|
surface.Unbind();
|
|
Assert.Empty(surface.CaptureProjectileDebugSamples());
|
|
}
|
|
|
|
[Fact]
|
|
public void SelectionAutomationUsesTheBoundCanonicalActionRoute()
|
|
{
|
|
using var surface = new AppAutomationSurface();
|
|
var actions = new List<PluginSelectionAction>();
|
|
surface.BindSelectionActions(action =>
|
|
{
|
|
actions.Add(action);
|
|
return true;
|
|
});
|
|
|
|
Assert.True(surface.Selection.Execute(
|
|
PluginSelectionAction.PreviousSelection));
|
|
Assert.True(surface.Selection.Execute(
|
|
PluginSelectionAction.NextPlayer));
|
|
Assert.Equal(
|
|
[
|
|
PluginSelectionAction.PreviousSelection,
|
|
PluginSelectionAction.NextPlayer,
|
|
],
|
|
actions);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData((uint)ItemType.MeleeWeapon, 0u, PluginObjectClass.MeleeWeapon)]
|
|
[InlineData((uint)ItemType.Armor, 0u, PluginObjectClass.Armor)]
|
|
[InlineData((uint)ItemType.Creature, 0x10u, PluginObjectClass.Monster)]
|
|
[InlineData((uint)ItemType.Creature, 0u, PluginObjectClass.Npc)]
|
|
[InlineData((uint)ItemType.Creature, 0x04000010u, PluginObjectClass.CombatPet)]
|
|
[InlineData((uint)ItemType.Creature, 0x8u, PluginObjectClass.Player)]
|
|
[InlineData((uint)ItemType.Misc, 0x200u, PluginObjectClass.Vendor)]
|
|
[InlineData((uint)ItemType.Misc, 0x1000u, PluginObjectClass.Door)]
|
|
public void ObjectClassProjectionMatchesVirindiPriority(
|
|
uint itemType,
|
|
uint publicFlags,
|
|
PluginObjectClass expected)
|
|
{
|
|
var item = new ClientObject
|
|
{
|
|
ObjectId = 1u,
|
|
Type = (ItemType)itemType,
|
|
PublicWeenieBitfield = publicFlags,
|
|
};
|
|
|
|
Assert.Equal(expected, AppAutomationSurface.ClassifyObject(item));
|
|
}
|
|
|
|
[Fact]
|
|
public void NavigationProjectionUsesVtankMapCoordinatesAndCompassHeading()
|
|
{
|
|
PluginNavigationPosition center =
|
|
AppAutomationSurface.ProjectNavigationPosition(new Position(
|
|
0x7F7F0001u,
|
|
new Vector3(84f, 84f, 240f),
|
|
Quaternion.Identity));
|
|
|
|
Assert.Equal(0d, center.EastWest, 8);
|
|
Assert.Equal(0d, center.NorthSouth, 8);
|
|
Assert.Equal(1d, center.Elevation, 8);
|
|
Assert.Equal(0f, center.HeadingDegrees, 4);
|
|
Assert.True(center.IsOutdoor);
|
|
|
|
PluginNavigationPosition nextBlock =
|
|
AppAutomationSurface.ProjectNavigationPosition(new Position(
|
|
0x80800041u,
|
|
new Vector3(84f, 84f, 0f),
|
|
Quaternion.Identity));
|
|
|
|
Assert.Equal(0.8d, nextBlock.EastWest, 8);
|
|
Assert.Equal(0.8d, nextBlock.NorthSouth, 8);
|
|
Assert.False(nextBlock.IsOutdoor);
|
|
}
|
|
|
|
[Fact]
|
|
public void ChatCapture_isOrderedCursorBasedAndDetachesAcrossSessions()
|
|
{
|
|
using var first = GameRuntimeTestFactory.Create();
|
|
using var second = GameRuntimeTestFactory.Create();
|
|
using var surface = new AppAutomationSurface();
|
|
surface.Bind(
|
|
first,
|
|
first.CharacterOwner,
|
|
first.ActionOwner.SpellCast);
|
|
|
|
first.CommunicationOwner.AddText(
|
|
"You cast Imperil Other VII on Olthoi.",
|
|
RetailLogTextType.Magic);
|
|
PluginChatMessage one = Assert.Single(surface.CaptureMessages(0));
|
|
Assert.Equal("You cast Imperil Other VII on Olthoi.", one.Text);
|
|
Assert.Empty(surface.CaptureMessages(one.Sequence));
|
|
|
|
surface.Bind(
|
|
second,
|
|
second.CharacterOwner,
|
|
second.ActionOwner.SpellCast);
|
|
first.CommunicationOwner.AddText(
|
|
"stale first-session line",
|
|
RetailLogTextType.Magic);
|
|
second.CommunicationOwner.AddText(
|
|
"You cast Fester Other VII on Olthoi.",
|
|
RetailLogTextType.Magic);
|
|
|
|
PluginChatMessage two = Assert.Single(
|
|
surface.CaptureMessages(one.Sequence));
|
|
Assert.True(two.Sequence > one.Sequence);
|
|
Assert.Equal("You cast Fester Other VII on Olthoi.", two.Text);
|
|
Assert.Equal(1, second.CommunicationOwner.SubscriberCount);
|
|
|
|
surface.Dispose();
|
|
Assert.Equal(0, second.CommunicationOwner.SubscriberCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void InventoryCompletionProjectsTheCanonicalRequestReceipt()
|
|
{
|
|
using var runtime = GameRuntimeTestFactory.Create();
|
|
using var surface = new AppAutomationSurface();
|
|
surface.Bind(runtime, runtime.CharacterOwner, runtime.ActionOwner.SpellCast);
|
|
ClientObjectTable objects = runtime.InventoryOwner.Objects;
|
|
const uint itemId = 0x50000123u;
|
|
objects.AddOrUpdate(new ClientObject
|
|
{
|
|
ObjectId = itemId,
|
|
Name = "Stack",
|
|
StackSize = 10,
|
|
StackSizeMax = 100,
|
|
});
|
|
|
|
Assert.True(runtime.InventoryOwner.Transactions.TryDispatch(
|
|
InventoryRequestKind.Merge,
|
|
itemId,
|
|
static () => true));
|
|
Assert.True(objects.UpdateStackSize(itemId, 9, 0));
|
|
|
|
PluginInventoryCompletion completion =
|
|
surface.Items.LastInventoryCompletion;
|
|
Assert.True(completion.Revision > 0);
|
|
Assert.Equal(PluginInventoryCommandKind.Merge, completion.Kind);
|
|
Assert.Equal(itemId, completion.SourceObjectId);
|
|
Assert.True(completion.IsSuccess);
|
|
}
|
|
|
|
[Fact]
|
|
public void RecoveryClearsExactlyOneCanonicalBusyReference()
|
|
{
|
|
using var runtime = GameRuntimeTestFactory.Create();
|
|
using var surface = new AppAutomationSurface();
|
|
surface.Bind(runtime, runtime.CharacterOwner, runtime.ActionOwner.SpellCast);
|
|
runtime.InventoryOwner.Transactions.IncrementBusyCount();
|
|
runtime.InventoryOwner.Transactions.IncrementBusyCount();
|
|
|
|
PluginRecoveryResult first = surface.Recovery.ClearOneBusyReference();
|
|
PluginRecoveryResult second = surface.Recovery.ClearOneBusyReference();
|
|
PluginRecoveryResult alreadyClear =
|
|
surface.Recovery.ClearOneBusyReference();
|
|
|
|
Assert.True(first.Accepted);
|
|
Assert.Equal((2, 1), (first.PreviousCount, first.CurrentCount));
|
|
Assert.Equal((1, 0), (second.PreviousCount, second.CurrentCount));
|
|
Assert.Equal((0, 0),
|
|
(alreadyClear.PreviousCount, alreadyClear.CurrentCount));
|
|
Assert.Equal(0, runtime.InventoryOwner.Transactions.BusyCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void EnchantmentLedgerSharesReportedAndConfirmedLocalDurationCasts()
|
|
{
|
|
var operations = new SpellOperations();
|
|
using var runtime = GameRuntimeTestFactory.Create(spellCast: operations);
|
|
runtime.CharacterOwner.InstallSpellMetadata(SpellTable.Create([DurationSpell()]));
|
|
runtime.CharacterOwner.Spellbook.OnSpellLearned(42u);
|
|
using var surface = new AppAutomationSurface();
|
|
surface.Bind(runtime, runtime.CharacterOwner, runtime.ActionOwner.SpellCast);
|
|
|
|
Assert.True(surface.Enchantments.ReportCast(100u, 42u, 30d));
|
|
PluginTrackedEnchantment reported = Assert.Single(
|
|
surface.Enchantments.Capture(100u));
|
|
Assert.Equal(7u, reported.Family);
|
|
Assert.Equal(350, reported.Quality);
|
|
Assert.InRange(reported.SecondsRemaining, 29d, 30d);
|
|
|
|
runtime.ActionOwner.Selection.Select(
|
|
200u,
|
|
SelectionChangeSource.Plugin);
|
|
Assert.Equal(
|
|
CastRequestResult.Sent,
|
|
runtime.ActionOwner.SpellCast.Cast(42u));
|
|
Assert.True(runtime.ActionOwner.SpellCast.CompleteUse(0u));
|
|
|
|
Assert.True(surface.Magic.LastCompletion.IsSuccess);
|
|
PluginTrackedEnchantment local = Assert.Single(
|
|
surface.Enchantments.Capture(200u));
|
|
Assert.Equal(42u, local.SpellId);
|
|
Assert.InRange(local.SecondsRemaining, 59d, 60d);
|
|
|
|
surface.Unbind();
|
|
Assert.Empty(surface.Enchantments.Capture(100u));
|
|
Assert.Empty(surface.Enchantments.Capture(200u));
|
|
}
|
|
|
|
private static SpellMetadata DurationSpell() => new(
|
|
42u,
|
|
"Fire Vulnerability Other VII",
|
|
"Life Magic",
|
|
7u,
|
|
0u,
|
|
string.Empty,
|
|
60f,
|
|
10,
|
|
true,
|
|
false,
|
|
string.Empty,
|
|
0,
|
|
350,
|
|
0u,
|
|
7,
|
|
false,
|
|
true,
|
|
false,
|
|
0f,
|
|
0u,
|
|
0u,
|
|
1u,
|
|
0);
|
|
|
|
private sealed class SpellOperations : IRuntimeSpellCastOperations
|
|
{
|
|
public uint LocalPlayerId => 1u;
|
|
public bool CanSend => true;
|
|
public bool HasRequiredComponents(uint spellId) => true;
|
|
public bool IsTargetCompatible(
|
|
uint targetId,
|
|
SpellMetadata spell,
|
|
bool showMessage) => true;
|
|
public void StopCompletely() { }
|
|
public void SendUntargeted(uint spellId) { }
|
|
public void SendTargeted(uint targetId, uint spellId) { }
|
|
public void DisplayMessage(string message) { }
|
|
public void IncrementBusy() { }
|
|
}
|
|
}
|