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

@ -0,0 +1,190 @@
namespace AcDream.Plugins.MossTank.Tests;
public sealed class VtankMetaProfileSerializerTests
{
[Fact]
public void LoadsKnownTypedCondActRecord()
{
const string source = "1\r\nCondAct\r\n5\r\nCType\r\nAType\r\n"
+ "CData\r\nAData\r\nState\r\nn\r\nn\r\nn\r\nn\r\nn\r\n1\r\n"
+ "i\r\n1\r\ni\r\n2\r\ni\r\n0\r\ns\r\n/say ready\r\n"
+ "s\r\nDefault\r\n";
Assert.True(VtankMetaProfileSerializer.TryLoad(
source, out MetaProfile profile, out string error), error);
MetaRule rule = Assert.Single(profile.Rules);
Assert.Equal(MetaConditionKind.Always, rule.Condition.Kind);
Assert.Equal(MetaActionKind.ChatCommand, rule.Action.Kind);
Assert.Equal("/say ready", rule.Action.Text);
Assert.Equal("Default", rule.State);
Assert.Equal(source, VtankMetaProfileSerializer.Save(profile));
}
[Fact]
public void RoundTripPreservesEveryVtankConditionActionAndEmbeddedNav()
{
MetaCondition[] conditions = BuildConditions();
MetaAction[] actions = BuildActions();
var profile = new MetaProfile();
for (int index = 0; index < conditions.Length; index++)
{
profile.Rules.Add(new MetaRule
{
State = $"State {index}",
Condition = conditions[index],
Action = actions[index % actions.Length],
});
}
for (int index = conditions.Length; index < actions.Length; index++)
{
profile.Rules.Add(new MetaRule
{
State = $"Action {index}",
Condition = MetaCondition.Always(),
Action = actions[index],
});
}
string first = VtankMetaProfileSerializer.Save(profile);
Assert.True(VtankMetaProfileSerializer.TryLoad(
first, out MetaProfile loaded, out string error), error);
string second = VtankMetaProfileSerializer.Save(loaded);
Assert.Equal(first, second);
Assert.Equal(profile.Rules.Count, loaded.Rules.Count);
MetaCondition priority = loaded.Rules.Single(rule =>
rule.Condition.Kind == MetaConditionKind.MonsterPriorityCountWithinDistance)
.Condition;
Assert.Equal(2, priority.Number);
Assert.Equal(18.5, priority.SecondaryNumber);
Assert.Equal(7, priority.TertiaryNumber);
MetaAction embedded = loaded.Rules.Select(static rule => rule.Action)
.First(action => action.Kind == MetaActionKind.LoadEmbeddedNavigationRoute);
Assert.Equal("One point", embedded.SecondaryText);
Assert.Equal("uTank2 NAV 1.2\r\n4\r\n1\r\n0\r\n1\r\n2\r\n3\r\n0\r\n",
embedded.Text);
Assert.Contains("<view width=\"120\" />s\r\n", first, StringComparison.Ordinal);
}
[Fact]
public void DisabledNativeRuleIsNotExportedAsExecutableLegacyRule()
{
var profile = new MetaProfile
{
Rules =
[
new MetaRule
{
Enabled = false,
Condition = MetaCondition.Always(),
Action = new MetaAction
{
Kind = MetaActionKind.ChatCommand,
Text = "/say must not run",
},
},
],
};
string source = VtankMetaProfileSerializer.Save(profile);
Assert.DoesNotContain("must not run", source, StringComparison.Ordinal);
Assert.True(VtankMetaProfileSerializer.TryLoad(
source, out MetaProfile loaded, out string error), error);
Assert.Empty(loaded.Rules);
}
private static MetaCondition[] BuildConditions() =>
[
C(MetaConditionKind.Never),
C(MetaConditionKind.Always),
C(MetaConditionKind.All, children: [C(MetaConditionKind.Always)]),
C(MetaConditionKind.Any, children: [C(MetaConditionKind.Never)]),
C(MetaConditionKind.ChatMessage, "^ready$"),
C(MetaConditionKind.PackSlotsLessThanOrEqual, number: 7),
C(MetaConditionKind.SecondsInStateGreaterThanOrEqual, number: 12),
C(MetaConditionKind.NavigationRouteEmpty),
C(MetaConditionKind.CharacterDeath),
C(MetaConditionKind.AnyVendorOpen),
C(MetaConditionKind.VendorClosed),
C(MetaConditionKind.InventoryItemCountLessThanOrEqual, "Prismatic Taper", 5),
C(MetaConditionKind.InventoryItemCountGreaterThanOrEqual, "Pyreal", 10),
C(MetaConditionKind.MonsterNameCountWithinDistance, "Olthoi.*", 3, 22.25),
C(MetaConditionKind.MonsterPriorityCountWithinDistance,
number: 2, secondaryNumber: 18.5, tertiaryNumber: 7),
C(MetaConditionKind.NeedToBuff),
C(MetaConditionKind.NoMonstersWithinDistance, number: 9.5),
C(MetaConditionKind.LandblockEquals, number: unchecked((int)0x8B370000u)),
C(MetaConditionKind.LandcellEquals, number: unchecked((int)0x8B37E3A1u)),
C(MetaConditionKind.PortalspaceEntered),
C(MetaConditionKind.PortalspaceExited),
C(MetaConditionKind.Not, children: [C(MetaConditionKind.Never)]),
C(MetaConditionKind.PersistentSecondsInStateGreaterThanOrEqual, number: 30),
C(MetaConditionKind.TimeLeftOnSpellGreaterThanOrEqual,
number: 3179, secondaryNumber: 45),
C(MetaConditionKind.BurdenPercentGreaterThanOrEqual, number: 110),
C(MetaConditionKind.DistanceFromAnyRoutePointGreaterThanOrEqual, number: 13.75),
C(MetaConditionKind.Expression, "getvar['go']==1"),
C(MetaConditionKind.ChatMessageCapture, "^(?<who>.+)$", secondary: "2;4"),
];
private static MetaAction[] BuildActions() =>
[
A(MetaActionKind.None),
A(MetaActionKind.SetMetaState, "Hunt"),
A(MetaActionKind.ChatCommand, "/say hello"),
A(MetaActionKind.All, children:
[
A(MetaActionKind.ChatCommand, "/say first"),
A(MetaActionKind.ExpressionAction, "setvar['done',1]"),
]),
A(MetaActionKind.LoadEmbeddedNavigationRoute,
"uTank2 NAV 1.2\r\n4\r\n1\r\n0\r\n1\r\n2\r\n3\r\n0\r\n",
"One point"),
A(MetaActionKind.CallMetaState, "Worker", "ReturnHere"),
A(MetaActionKind.ReturnFromCall),
A(MetaActionKind.ExpressionAction, "setvar['x',2]"),
A(MetaActionKind.ChatExpression, "cstr[getvar['x']]"),
A(MetaActionKind.SetWatchdog, "Recover", number: 12.5, secondaryNumber: 4.75),
A(MetaActionKind.ClearWatchdog),
A(MetaActionKind.GetVtankOption, "OpenDoors", "doors"),
A(MetaActionKind.SetVtankOption, "OpenDoors", "istrue[1]"),
A(MetaActionKind.CreateView, "myview", "<view width=\"120\" />"),
A(MetaActionKind.DestroyView, "myview"),
A(MetaActionKind.DestroyAllViews),
];
private static MetaCondition C(
MetaConditionKind kind,
string text = "",
double number = 0,
double secondaryNumber = 0,
double tertiaryNumber = 0,
string secondary = "",
List<MetaCondition>? children = null) => new()
{
Kind = kind,
Text = text,
SecondaryText = secondary,
Number = number,
SecondaryNumber = secondaryNumber,
TertiaryNumber = tertiaryNumber,
Children = children ?? [],
};
private static MetaAction A(
MetaActionKind kind,
string text = "",
string secondary = "",
double number = 0,
double secondaryNumber = 0,
List<MetaAction>? children = null) => new()
{
Kind = kind,
Text = text,
SecondaryText = secondary,
Number = number,
SecondaryNumber = secondaryNumber,
Children = children ?? [],
};
}