feat(mosstank): add VTank-style automation PoC

This commit is contained in:
Erik 2026-08-27 18:57:21 +02:00
parent f6fe0f2a4f
commit 4e6e9bc9d9
212 changed files with 49462 additions and 416 deletions

View file

@ -40,6 +40,7 @@ public sealed class ItemInteractionControllerTests
public readonly List<(uint VendorGuid, IReadOnlyList<(int Amount, uint ItemGuid)> Items, uint AlternateCurrencyId)> BuyAlls = new();
public bool SendBuyAllSucceeds = true;
public readonly List<(uint VendorGuid, IReadOnlyList<(int Amount, uint ItemGuid)> Items)> Sells = new();
public readonly List<(uint ToolGuid, IReadOnlyList<uint> ItemGuids)> Salvages = new();
public bool SendSellSucceeds = true;
public readonly List<string> Toasts = new();
public readonly List<string> SystemMessages = new();
@ -133,7 +134,12 @@ public sealed class ItemInteractionControllerTests
},
interfaceText: (text, type) => InterfaceTexts.Add((text, type)),
sendStackableMerge: (source, target, amount) =>
Merges.Add((source, target, amount)));
Merges.Add((source, target, amount)),
sendSalvage: (tool, items) =>
{
Salvages.Add((tool, items.ToArray()));
return true;
});
}
public ItemInteractionController Controller { get; }
@ -171,6 +177,86 @@ public sealed class ItemInteractionControllerTests
Assert.Empty(h.UseWithTarget);
}
[Fact]
public void AutomationApply_dispatchesDirectlyWithoutInstallingTargetMode()
{
var h = new Harness();
const uint source = 0x50000A21u;
h.AddContained(source, item =>
{
item.Useability = HealthKitUseability;
item.TargetType = (uint)ItemType.Creature;
});
Assert.True(h.Controller.TryApplyItem(source, Player));
Assert.Equal(new[] { (source, Player) }, h.UseWithTarget);
Assert.False(h.Controller.IsAnyTargetModeActive);
Assert.Equal(1, h.Controller.BusyCount);
}
[Fact]
public void AutomationApply_reportsRefusalWhenTargetIsIncompatible()
{
var h = new Harness();
const uint source = 0x50000A21u;
const uint coat = 0x50000A22u;
h.AddContained(source, item =>
{
item.Useability = HealthKitUseability;
item.TargetType = (uint)ItemType.Creature;
});
h.AddContained(coat, item => item.Type = ItemType.Armor);
Assert.False(h.Controller.TryApplyItem(source, coat));
Assert.Empty(h.UseWithTarget);
Assert.Equal(0, h.Controller.BusyCount);
}
[Fact]
public void AutomationUse_dispatchesOnlyAnOrdinaryWireUse()
{
var h = new Harness();
const uint item = 0x50000A23u;
h.AddContained(item, candidate =>
candidate.Useability = ItemUseability.Contained);
Assert.True(h.Controller.TryUseItemForAutomation(item));
Assert.Equal(new[] { item }, h.Uses);
Assert.Equal(1, h.Controller.BusyCount);
}
[Fact]
public void AutomationUse_refusesTargetedItemInsteadOfOpeningModalCursor()
{
var h = new Harness();
const uint item = 0x50000A24u;
h.AddContained(item, candidate =>
candidate.Useability = HealthKitUseability);
Assert.False(h.Controller.TryUseItemForAutomation(item));
Assert.Empty(h.Uses);
Assert.False(h.Controller.IsAnyTargetModeActive);
Assert.Equal(0, h.Controller.BusyCount);
}
[Fact]
public void AutomationAppraisalUsesCanonicalOwnerWithoutChangingSelectionMode()
{
var h = new Harness();
const uint item = 0x50000A25u;
h.AddContained(item);
Assert.True(h.Controller.TryAppraiseForAutomation(item));
Assert.Equal(new[] { item }, h.Examines);
Assert.False(h.Controller.IsAnyTargetModeActive);
Assert.Equal(1, h.Controller.BusyCount);
}
[Fact]
public void ResetSession_RetryNotifiesEveryStateObserver()
{
@ -2085,6 +2171,110 @@ public sealed class ItemInteractionControllerTests
Assert.False(h.Controller.TryGetPendingInventoryRequest(out _));
}
[Fact]
public void AutomationMoveUsesWholeOrExactPartialRetailRequest()
{
var whole = new Harness();
const uint wholeItem = 0x50000A32u;
whole.AddContained(wholeItem, item => item.StackSize = 10);
Assert.True(whole.Controller.TryMoveItemForAutomation(
wholeItem, Player, amount: 0u, placement: 7));
Assert.Equal(new[] { (wholeItem, Player, 7) }, whole.Puts);
Assert.True(whole.Controller.TryGetPendingInventoryRequest(out var put));
Assert.Equal(InventoryRequestKind.PutInContainer, put.Kind);
var partial = new Harness();
const uint partialItem = 0x50000A33u;
partial.AddContained(partialItem, item => item.StackSize = 10);
Assert.True(partial.Controller.TryMoveItemForAutomation(
partialItem, Player, amount: 2u, placement: 3));
Assert.Equal(
new[] { (partialItem, Player, 3u, 2u) },
partial.SplitPuts);
Assert.True(partial.Controller.TryGetPendingInventoryRequest(out var split));
Assert.Equal(InventoryRequestKind.SplitToContainer, split.Kind);
}
[Fact]
public void AutomationMergeUsesRetailPlannerAndSharedGate()
{
var h = new Harness();
const uint source = 0x50000A34u;
const uint target = 0x50000A35u;
h.AddContained(source, item =>
{
item.WeenieClassId = 77u;
item.StackSize = 8;
item.StackSizeMax = 10;
});
h.AddContained(target, item =>
{
item.WeenieClassId = 77u;
item.StackSize = 7;
item.StackSizeMax = 10;
});
Assert.True(h.Controller.TryMergeItemsForAutomation(source, target));
Assert.Equal(new[] { (source, target, 3u) }, h.Merges);
Assert.True(h.Controller.TryGetPendingInventoryRequest(out var pending));
Assert.Equal(InventoryRequestKind.Merge, pending.Kind);
Assert.False(h.Controller.TryMoveItemForAutomation(source, Player));
Assert.Single(h.Merges);
}
[Fact]
public void AutomationDropAndGivePreserveExactStackAmounts()
{
var drop = new Harness();
const uint dropItem = 0x50000A36u;
drop.AddContained(dropItem, item => item.StackSize = 10);
Assert.True(drop.Controller.TryDropItemForAutomation(dropItem, 2u));
Assert.Equal(new[] { (dropItem, 2u) }, drop.SplitDrops);
Assert.Empty(drop.Drops);
var give = new Harness();
const uint giveItem = 0x50000A37u;
const uint recipient = 0x70000A38u;
give.AddContained(giveItem, item => item.StackSize = 10);
give.Objects.AddOrUpdate(new ClientObject
{
ObjectId = recipient,
Name = "Recipient",
Type = ItemType.Creature,
});
Assert.True(give.Controller.TryGiveItemForAutomation(
giveItem, recipient, 4u));
Assert.Equal(new[] { (recipient, giveItem, 4u) }, give.Gives);
}
[Fact]
public void AutomationSalvageRequiresRetailToolAndSuitableOwnedItems()
{
var h = new Harness();
const uint tool = 0x50000A40u;
const uint source = 0x50000A41u;
h.AddContained(tool, item => item.Type = ItemType.TinkeringTool);
h.AddContained(source, item =>
{
item.MaterialType = 12u;
item.Structure = 50;
});
Assert.True(h.Controller.TrySalvageItemsForAutomation(tool, [source]));
Assert.Single(h.Salvages);
Assert.Equal(tool, h.Salvages[0].ToolGuid);
Assert.Equal(new[] { source }, h.Salvages[0].ItemGuids);
h.Objects.Get(source)!.Structure = 100;
Assert.False(h.Controller.TrySalvageItemsForAutomation(tool, [source]));
Assert.Single(h.Salvages);
}
[Fact]
public void MatchingInventoryFailureReleasesGlobalRequest()
{