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
742
src/AcDream.Plugins.MossTank/VtankMetaProfileSerializer.cs
Normal file
742
src/AcDream.Plugins.MossTank/VtankMetaProfileSerializer.cs
Normal file
|
|
@ -0,0 +1,742 @@
|
|||
using System.Globalization;
|
||||
|
||||
namespace AcDream.Plugins.MossTank;
|
||||
|
||||
/// <summary>
|
||||
/// Reads and writes VTank's exact line-encoded <c>CondAct</c> Meta database.
|
||||
/// The format is the public interchange contract used by legacy <c>.met</c>
|
||||
/// profiles; it is deliberately independent from MossTank's native JSON store.
|
||||
/// </summary>
|
||||
internal static class VtankMetaProfileSerializer
|
||||
{
|
||||
private static readonly string[] Header =
|
||||
[
|
||||
"1", "CondAct", "5", "CType", "AType", "CData", "AData",
|
||||
"State", "n", "n", "n", "n", "n",
|
||||
];
|
||||
|
||||
private static readonly string[] TablePrefix = ["TABLE", "2", "k", "v", "n", "n"];
|
||||
private static readonly string[] RecursiveTablePrefix = ["TABLE", "2", "K", "V", "n", "n"];
|
||||
private const int MaximumRules = 100_000;
|
||||
private const int MaximumNesting = 256;
|
||||
|
||||
public static bool TryLoad(string source, out MetaProfile profile, out string error)
|
||||
{
|
||||
try
|
||||
{
|
||||
var reader = new LineReader(source);
|
||||
reader.Expect(Header);
|
||||
int count = reader.ReadInt();
|
||||
if (count is < 0 or > MaximumRules)
|
||||
throw reader.Error("Invalid VTank Meta rule count.");
|
||||
|
||||
var parsed = new MetaProfile();
|
||||
for (int index = 0; index < count; index++)
|
||||
{
|
||||
reader.Expect("i");
|
||||
int conditionType = reader.ReadInt();
|
||||
reader.Expect("i");
|
||||
int actionType = reader.ReadInt();
|
||||
MetaCondition condition = ReadCondition(reader, conditionType, 0);
|
||||
MetaAction action = ReadAction(reader, actionType, 0);
|
||||
reader.Expect("s");
|
||||
parsed.Rules.Add(new MetaRule
|
||||
{
|
||||
State = reader.Read(),
|
||||
Condition = condition,
|
||||
Action = action,
|
||||
Enabled = true,
|
||||
});
|
||||
}
|
||||
reader.ExpectEnd();
|
||||
profile = parsed;
|
||||
error = string.Empty;
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception) when (exception is FormatException
|
||||
or OverflowException or ArgumentOutOfRangeException)
|
||||
{
|
||||
profile = new MetaProfile();
|
||||
error = exception.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static string Save(MetaProfile source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
var writer = new LineWriter();
|
||||
writer.Add(Header);
|
||||
MetaRule[] rules = source.Rules.Where(static rule => rule.Enabled).ToArray();
|
||||
writer.Add(rules.Length);
|
||||
foreach (MetaRule rule in rules)
|
||||
{
|
||||
writer.Add("i", ConditionType(rule.Condition.Kind), "i", ActionType(rule.Action.Kind));
|
||||
WriteCondition(writer, rule.Condition, 0);
|
||||
WriteAction(writer, rule.Action, 0);
|
||||
writer.Add("s", rule.State ?? string.Empty);
|
||||
}
|
||||
return writer.Finish();
|
||||
}
|
||||
|
||||
private static MetaCondition ReadCondition(LineReader reader, int type, int depth)
|
||||
{
|
||||
CheckDepth(reader, depth);
|
||||
var value = new MetaCondition { Kind = ConditionKind(type) };
|
||||
switch (type)
|
||||
{
|
||||
case 0 or 1 or 7 or 8 or 9 or 10 or 15 or 19 or 20:
|
||||
reader.Expect("i", "0");
|
||||
break;
|
||||
case 2 or 3:
|
||||
reader.Expect(RecursiveTablePrefix);
|
||||
ReadConditions(reader, value, reader.ReadCount(), depth);
|
||||
break;
|
||||
case 4:
|
||||
reader.Expect("s");
|
||||
value.Text = reader.Read();
|
||||
break;
|
||||
case 5 or 6 or 17 or 18 or 22 or 24:
|
||||
reader.Expect("i");
|
||||
value.Number = reader.ReadInt();
|
||||
break;
|
||||
case 11 or 12:
|
||||
reader.Expect(TablePrefix, "2", "s", "n", "s");
|
||||
value.Text = reader.Read();
|
||||
reader.Expect("s", "c", "i");
|
||||
value.Number = reader.ReadInt();
|
||||
break;
|
||||
case 13:
|
||||
reader.Expect(TablePrefix, "3", "s", "n", "s");
|
||||
value.Text = reader.Read();
|
||||
reader.Expect("s", "c", "i");
|
||||
value.Number = reader.ReadInt();
|
||||
reader.Expect("s", "r", "d");
|
||||
value.SecondaryNumber = reader.ReadDouble();
|
||||
break;
|
||||
case 14:
|
||||
reader.Expect(TablePrefix, "3", "s", "p", "i");
|
||||
value.TertiaryNumber = reader.ReadInt();
|
||||
reader.Expect("s", "c", "i");
|
||||
value.Number = reader.ReadInt();
|
||||
reader.Expect("s", "r", "d");
|
||||
value.SecondaryNumber = reader.ReadDouble();
|
||||
break;
|
||||
case 16:
|
||||
reader.Expect(TablePrefix, "1", "s", "r", "d");
|
||||
value.Number = reader.ReadDouble();
|
||||
break;
|
||||
case 21:
|
||||
reader.Expect(RecursiveTablePrefix);
|
||||
if (reader.ReadCount() != 1)
|
||||
throw reader.Error("VTank Meta Not requires exactly one condition.");
|
||||
reader.Expect("i");
|
||||
value.Children.Add(ReadCondition(reader, reader.ReadInt(), depth + 1));
|
||||
break;
|
||||
case 23:
|
||||
reader.Expect(TablePrefix, "2", "s", "sid", "i");
|
||||
value.Number = reader.ReadInt();
|
||||
reader.Expect("s", "sec", "i");
|
||||
value.SecondaryNumber = reader.ReadInt();
|
||||
break;
|
||||
case 25:
|
||||
reader.Expect(TablePrefix, "1", "s", "dist", "d");
|
||||
value.Number = reader.ReadDouble();
|
||||
break;
|
||||
case 26:
|
||||
reader.Expect(TablePrefix, "1", "s", "e", "s");
|
||||
value.Text = reader.Read();
|
||||
break;
|
||||
case 28:
|
||||
reader.Expect(TablePrefix, "2", "s", "p", "s");
|
||||
value.Text = reader.Read();
|
||||
reader.Expect("s", "c", "s");
|
||||
value.SecondaryText = reader.Read();
|
||||
break;
|
||||
default:
|
||||
throw reader.Error($"Unknown VTank Meta condition type {type}.");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void ReadConditions(
|
||||
LineReader reader,
|
||||
MetaCondition target,
|
||||
int count,
|
||||
int depth)
|
||||
{
|
||||
for (int index = 0; index < count; index++)
|
||||
{
|
||||
reader.Expect("i");
|
||||
target.Children.Add(ReadCondition(reader, reader.ReadInt(), depth + 1));
|
||||
}
|
||||
}
|
||||
|
||||
private static MetaAction ReadAction(LineReader reader, int type, int depth)
|
||||
{
|
||||
CheckDepth(reader, depth);
|
||||
var value = new MetaAction { Kind = ActionKind(type) };
|
||||
switch (type)
|
||||
{
|
||||
case 0 or 6:
|
||||
reader.Expect("i", "0");
|
||||
break;
|
||||
case 1 or 2:
|
||||
reader.Expect("s");
|
||||
value.Text = reader.Read();
|
||||
break;
|
||||
case 3:
|
||||
reader.Expect(RecursiveTablePrefix);
|
||||
int count = reader.ReadCount();
|
||||
for (int index = 0; index < count; index++)
|
||||
{
|
||||
reader.Expect("i");
|
||||
value.Children.Add(ReadAction(reader, reader.ReadInt(), depth + 1));
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
ReadEmbeddedNavigation(reader, value);
|
||||
break;
|
||||
case 5:
|
||||
reader.Expect(TablePrefix, "2", "s", "st", "s");
|
||||
value.Text = reader.Read();
|
||||
reader.Expect("s", "ret", "s");
|
||||
value.SecondaryText = reader.Read();
|
||||
break;
|
||||
case 7 or 8:
|
||||
reader.Expect(TablePrefix, "1", "s", "e", "s");
|
||||
value.Text = reader.Read();
|
||||
break;
|
||||
case 9:
|
||||
reader.Expect(TablePrefix, "3", "s", "s", "s");
|
||||
value.Text = reader.Read();
|
||||
reader.Expect("s", "r", "d");
|
||||
value.Number = reader.ReadDouble();
|
||||
reader.Expect("s", "t", "d");
|
||||
value.SecondaryNumber = reader.ReadDouble();
|
||||
break;
|
||||
case 10 or 15:
|
||||
reader.Expect(TablePrefix, "0");
|
||||
break;
|
||||
case 11:
|
||||
reader.Expect(TablePrefix, "2", "s", "o", "s");
|
||||
value.Text = reader.Read();
|
||||
reader.Expect("s", "v", "s");
|
||||
value.SecondaryText = reader.Read();
|
||||
break;
|
||||
case 12:
|
||||
reader.Expect(TablePrefix, "2", "s", "o", "s");
|
||||
value.Text = reader.Read();
|
||||
reader.Expect("s", "v", "s");
|
||||
value.SecondaryText = reader.Read();
|
||||
break;
|
||||
case 13:
|
||||
reader.Expect(TablePrefix, "2", "s", "n", "s");
|
||||
value.Text = reader.Read();
|
||||
reader.Expect("s", "x", "ba");
|
||||
int length = reader.ReadCount();
|
||||
value.SecondaryText = reader.ReadByteArray(length);
|
||||
break;
|
||||
case 14:
|
||||
reader.Expect(TablePrefix, "1", "s", "n", "s");
|
||||
value.Text = reader.Read();
|
||||
break;
|
||||
default:
|
||||
throw reader.Error($"Unknown VTank Meta action type {type}.");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void ReadEmbeddedNavigation(LineReader reader, MetaAction target)
|
||||
{
|
||||
reader.Expect("ba");
|
||||
int serializedCharacters = reader.ReadCount();
|
||||
target.SecondaryText = reader.Read();
|
||||
int statedNodeCount = reader.ReadCount();
|
||||
if (serializedCharacters <= 5)
|
||||
{
|
||||
target.Text = EmptyNavigation();
|
||||
return;
|
||||
}
|
||||
|
||||
var lines = new List<string> { reader.ReadExpected("uTank2 NAV 1.2") };
|
||||
int mode = reader.ReadInt(out string modeLine);
|
||||
lines.Add(modeLine);
|
||||
int actualNodeCount;
|
||||
if (mode == 3)
|
||||
{
|
||||
lines.Add(reader.Read());
|
||||
lines.Add(reader.Read());
|
||||
actualNodeCount = 1;
|
||||
}
|
||||
else if (mode is 1 or 2 or 4)
|
||||
{
|
||||
actualNodeCount = reader.ReadCount(out string countLine);
|
||||
lines.Add(countLine);
|
||||
for (int index = 0; index < actualNodeCount; index++)
|
||||
ReadNavigationNode(reader, lines);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw reader.Error($"Unknown embedded VTank navigation type {mode}.");
|
||||
}
|
||||
if (actualNodeCount != statedNodeCount)
|
||||
throw reader.Error("Embedded VTank navigation node counts do not match.");
|
||||
target.Text = string.Join("\r\n", lines) + "\r\n";
|
||||
}
|
||||
|
||||
private static void ReadNavigationNode(LineReader reader, List<string> lines)
|
||||
{
|
||||
int type = reader.ReadInt(out string typeLine);
|
||||
lines.Add(typeLine);
|
||||
for (int index = 0; index < 4; index++)
|
||||
lines.Add(reader.Read());
|
||||
int extra = type switch
|
||||
{
|
||||
0 or 8 => 0,
|
||||
1 or 2 or 3 or 4 => 1,
|
||||
5 => 2,
|
||||
6 or 7 => 6,
|
||||
9 => 3,
|
||||
_ => throw reader.Error($"Unknown embedded VTank waypoint type {type}."),
|
||||
};
|
||||
for (int index = 0; index < extra; index++)
|
||||
lines.Add(reader.Read());
|
||||
}
|
||||
|
||||
private static void WriteCondition(LineWriter writer, MetaCondition value, int depth)
|
||||
{
|
||||
CheckDepth(depth);
|
||||
int type = ConditionType(value.Kind);
|
||||
switch (type)
|
||||
{
|
||||
case 0 or 1 or 7 or 8 or 9 or 10 or 15 or 19 or 20:
|
||||
writer.Add("i", "0");
|
||||
break;
|
||||
case 2 or 3:
|
||||
writer.Add(RecursiveTablePrefix);
|
||||
writer.Add(value.Children.Count);
|
||||
foreach (MetaCondition child in value.Children)
|
||||
{
|
||||
writer.Add("i", ConditionType(child.Kind));
|
||||
WriteCondition(writer, child, depth + 1);
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
writer.Add("s", value.Text);
|
||||
break;
|
||||
case 5 or 6 or 17 or 18 or 22 or 24:
|
||||
writer.Add("i", IntValue(value.Number));
|
||||
break;
|
||||
case 11 or 12:
|
||||
writer.Add(TablePrefix, "2", "s", "n", "s", value.Text,
|
||||
"s", "c", "i", IntValue(value.Number));
|
||||
break;
|
||||
case 13:
|
||||
writer.Add(TablePrefix, "3", "s", "n", "s", value.Text,
|
||||
"s", "c", "i", IntValue(value.Number),
|
||||
"s", "r", "d", Number(value.SecondaryNumber));
|
||||
break;
|
||||
case 14:
|
||||
writer.Add(TablePrefix, "3", "s", "p", "i", IntValue(value.TertiaryNumber),
|
||||
"s", "c", "i", IntValue(value.Number),
|
||||
"s", "r", "d", Number(value.SecondaryNumber));
|
||||
break;
|
||||
case 16:
|
||||
writer.Add(TablePrefix, "1", "s", "r", "d", Number(value.Number));
|
||||
break;
|
||||
case 21:
|
||||
if (value.Children.Count != 1)
|
||||
throw new InvalidOperationException("VTank Meta Not requires exactly one condition.");
|
||||
writer.Add(RecursiveTablePrefix, "1", "i", ConditionType(value.Children[0].Kind));
|
||||
WriteCondition(writer, value.Children[0], depth + 1);
|
||||
break;
|
||||
case 23:
|
||||
writer.Add(TablePrefix, "2", "s", "sid", "i", IntValue(value.Number),
|
||||
"s", "sec", "i", IntValue(value.SecondaryNumber));
|
||||
break;
|
||||
case 25:
|
||||
writer.Add(TablePrefix, "1", "s", "dist", "d", Number(value.Number));
|
||||
break;
|
||||
case 26:
|
||||
writer.Add(TablePrefix, "1", "s", "e", "s", value.Text);
|
||||
break;
|
||||
case 28:
|
||||
writer.Add(TablePrefix, "2", "s", "p", "s", value.Text,
|
||||
"s", "c", "s", value.SecondaryText);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown VTank Meta condition type {type}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteAction(LineWriter writer, MetaAction value, int depth)
|
||||
{
|
||||
CheckDepth(depth);
|
||||
int type = ActionType(value.Kind);
|
||||
switch (type)
|
||||
{
|
||||
case 0 or 6:
|
||||
writer.Add("i", "0");
|
||||
break;
|
||||
case 1 or 2:
|
||||
writer.Add("s", value.Text);
|
||||
break;
|
||||
case 3:
|
||||
writer.Add(RecursiveTablePrefix);
|
||||
writer.Add(value.Children.Count);
|
||||
foreach (MetaAction child in value.Children)
|
||||
{
|
||||
writer.Add("i", ActionType(child.Kind));
|
||||
WriteAction(writer, child, depth + 1);
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
WriteEmbeddedNavigation(writer, value);
|
||||
break;
|
||||
case 5:
|
||||
writer.Add(TablePrefix, "2", "s", "st", "s", value.Text,
|
||||
"s", "ret", "s", value.SecondaryText);
|
||||
break;
|
||||
case 7 or 8:
|
||||
writer.Add(TablePrefix, "1", "s", "e", "s", value.Text);
|
||||
break;
|
||||
case 9:
|
||||
writer.Add(TablePrefix, "3", "s", "s", "s", value.Text,
|
||||
"s", "r", "d", Number(value.Number),
|
||||
"s", "t", "d", Number(value.SecondaryNumber));
|
||||
break;
|
||||
case 10 or 15:
|
||||
writer.Add(TablePrefix, "0");
|
||||
break;
|
||||
case 11 or 12:
|
||||
writer.Add(TablePrefix, "2", "s", "o", "s", value.Text,
|
||||
"s", "v", "s", value.SecondaryText);
|
||||
break;
|
||||
case 13:
|
||||
writer.Add(TablePrefix, "2", "s", "n", "s", value.Text,
|
||||
"s", "x", "ba", value.SecondaryText.Length);
|
||||
writer.AddBuggedByteArray(value.SecondaryText);
|
||||
break;
|
||||
case 14:
|
||||
writer.Add(TablePrefix, "1", "s", "n", "s", value.Text);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown VTank Meta action type {type}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteEmbeddedNavigation(LineWriter writer, MetaAction value)
|
||||
{
|
||||
string nav = string.IsNullOrWhiteSpace(value.Text) ? EmptyNavigation() : value.Text;
|
||||
string normalized = NormalizeNewlines(nav);
|
||||
string[] navLines = normalized.Split('\n', StringSplitOptions.None);
|
||||
if (navLines.Length != 0 && navLines[^1].Length == 0)
|
||||
navLines = navLines[..^1];
|
||||
int nodes = NavigationNodeCount(navLines);
|
||||
string name = string.IsNullOrEmpty(value.SecondaryText) ? "[None]" : value.SecondaryText;
|
||||
int characters = name.Length + 2
|
||||
+ nodes.ToString(CultureInfo.InvariantCulture).Length + 2
|
||||
+ navLines.Sum(static line => line.Length + 2);
|
||||
writer.Add("ba", characters, name, nodes);
|
||||
writer.Add(navLines);
|
||||
}
|
||||
|
||||
private static int NavigationNodeCount(string[] lines)
|
||||
{
|
||||
if (lines.Length < 2 || !lines[0].Equals("uTank2 NAV 1.2", StringComparison.Ordinal))
|
||||
throw new InvalidOperationException("Embedded Meta route is not uTank2 NAV 1.2 data.");
|
||||
int mode = int.Parse(lines[1], NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||
if (mode == 3)
|
||||
return 1;
|
||||
if (mode is not (1 or 2 or 4) || lines.Length < 3)
|
||||
throw new InvalidOperationException("Embedded Meta route has an invalid navigation type.");
|
||||
return int.Parse(lines[2], NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static string EmptyNavigation() => "uTank2 NAV 1.2\r\n1\r\n0\r\n";
|
||||
|
||||
private static string NormalizeNewlines(string value) => value
|
||||
.Replace("\r\n", "\n", StringComparison.Ordinal)
|
||||
.Replace('\r', '\n');
|
||||
|
||||
private static int ConditionType(MetaConditionKind kind) => kind switch
|
||||
{
|
||||
MetaConditionKind.Never => 0,
|
||||
MetaConditionKind.Always => 1,
|
||||
MetaConditionKind.All => 2,
|
||||
MetaConditionKind.Any => 3,
|
||||
MetaConditionKind.ChatMessage => 4,
|
||||
MetaConditionKind.PackSlotsLessThanOrEqual => 5,
|
||||
MetaConditionKind.SecondsInStateGreaterThanOrEqual => 6,
|
||||
MetaConditionKind.NavigationRouteEmpty => 7,
|
||||
MetaConditionKind.CharacterDeath => 8,
|
||||
MetaConditionKind.AnyVendorOpen => 9,
|
||||
MetaConditionKind.VendorClosed => 10,
|
||||
MetaConditionKind.InventoryItemCountLessThanOrEqual => 11,
|
||||
MetaConditionKind.InventoryItemCountGreaterThanOrEqual => 12,
|
||||
MetaConditionKind.MonsterNameCountWithinDistance => 13,
|
||||
MetaConditionKind.MonsterPriorityCountWithinDistance => 14,
|
||||
MetaConditionKind.NeedToBuff => 15,
|
||||
MetaConditionKind.NoMonstersWithinDistance => 16,
|
||||
MetaConditionKind.LandblockEquals => 17,
|
||||
MetaConditionKind.LandcellEquals => 18,
|
||||
MetaConditionKind.PortalspaceEntered => 19,
|
||||
MetaConditionKind.PortalspaceExited => 20,
|
||||
MetaConditionKind.Not => 21,
|
||||
MetaConditionKind.PersistentSecondsInStateGreaterThanOrEqual => 22,
|
||||
MetaConditionKind.TimeLeftOnSpellGreaterThanOrEqual => 23,
|
||||
MetaConditionKind.BurdenPercentGreaterThanOrEqual => 24,
|
||||
MetaConditionKind.DistanceFromAnyRoutePointGreaterThanOrEqual => 25,
|
||||
MetaConditionKind.Expression => 26,
|
||||
MetaConditionKind.ChatMessageCapture => 28,
|
||||
_ => throw new InvalidOperationException($"Unsupported Meta condition {kind}."),
|
||||
};
|
||||
|
||||
private static MetaConditionKind ConditionKind(int type) => type switch
|
||||
{
|
||||
0 => MetaConditionKind.Never,
|
||||
1 => MetaConditionKind.Always,
|
||||
2 => MetaConditionKind.All,
|
||||
3 => MetaConditionKind.Any,
|
||||
4 => MetaConditionKind.ChatMessage,
|
||||
5 => MetaConditionKind.PackSlotsLessThanOrEqual,
|
||||
6 => MetaConditionKind.SecondsInStateGreaterThanOrEqual,
|
||||
7 => MetaConditionKind.NavigationRouteEmpty,
|
||||
8 => MetaConditionKind.CharacterDeath,
|
||||
9 => MetaConditionKind.AnyVendorOpen,
|
||||
10 => MetaConditionKind.VendorClosed,
|
||||
11 => MetaConditionKind.InventoryItemCountLessThanOrEqual,
|
||||
12 => MetaConditionKind.InventoryItemCountGreaterThanOrEqual,
|
||||
13 => MetaConditionKind.MonsterNameCountWithinDistance,
|
||||
14 => MetaConditionKind.MonsterPriorityCountWithinDistance,
|
||||
15 => MetaConditionKind.NeedToBuff,
|
||||
16 => MetaConditionKind.NoMonstersWithinDistance,
|
||||
17 => MetaConditionKind.LandblockEquals,
|
||||
18 => MetaConditionKind.LandcellEquals,
|
||||
19 => MetaConditionKind.PortalspaceEntered,
|
||||
20 => MetaConditionKind.PortalspaceExited,
|
||||
21 => MetaConditionKind.Not,
|
||||
22 => MetaConditionKind.PersistentSecondsInStateGreaterThanOrEqual,
|
||||
23 => MetaConditionKind.TimeLeftOnSpellGreaterThanOrEqual,
|
||||
24 => MetaConditionKind.BurdenPercentGreaterThanOrEqual,
|
||||
25 => MetaConditionKind.DistanceFromAnyRoutePointGreaterThanOrEqual,
|
||||
26 => MetaConditionKind.Expression,
|
||||
28 => MetaConditionKind.ChatMessageCapture,
|
||||
_ => throw new FormatException($"Unknown VTank Meta condition type {type}."),
|
||||
};
|
||||
|
||||
private static int ActionType(MetaActionKind kind) => kind switch
|
||||
{
|
||||
MetaActionKind.None => 0,
|
||||
MetaActionKind.SetMetaState => 1,
|
||||
MetaActionKind.ChatCommand => 2,
|
||||
MetaActionKind.All => 3,
|
||||
MetaActionKind.LoadEmbeddedNavigationRoute => 4,
|
||||
MetaActionKind.CallMetaState => 5,
|
||||
MetaActionKind.ReturnFromCall => 6,
|
||||
MetaActionKind.ExpressionAction => 7,
|
||||
MetaActionKind.ChatExpression => 8,
|
||||
MetaActionKind.SetWatchdog => 9,
|
||||
MetaActionKind.ClearWatchdog => 10,
|
||||
MetaActionKind.GetVtankOption => 11,
|
||||
MetaActionKind.SetVtankOption => 12,
|
||||
MetaActionKind.CreateView => 13,
|
||||
MetaActionKind.DestroyView => 14,
|
||||
MetaActionKind.DestroyAllViews => 15,
|
||||
_ => throw new InvalidOperationException($"Unsupported Meta action {kind}."),
|
||||
};
|
||||
|
||||
private static MetaActionKind ActionKind(int type) => type switch
|
||||
{
|
||||
0 => MetaActionKind.None,
|
||||
1 => MetaActionKind.SetMetaState,
|
||||
2 => MetaActionKind.ChatCommand,
|
||||
3 => MetaActionKind.All,
|
||||
4 => MetaActionKind.LoadEmbeddedNavigationRoute,
|
||||
5 => MetaActionKind.CallMetaState,
|
||||
6 => MetaActionKind.ReturnFromCall,
|
||||
7 => MetaActionKind.ExpressionAction,
|
||||
8 => MetaActionKind.ChatExpression,
|
||||
9 => MetaActionKind.SetWatchdog,
|
||||
10 => MetaActionKind.ClearWatchdog,
|
||||
11 => MetaActionKind.GetVtankOption,
|
||||
12 => MetaActionKind.SetVtankOption,
|
||||
13 => MetaActionKind.CreateView,
|
||||
14 => MetaActionKind.DestroyView,
|
||||
15 => MetaActionKind.DestroyAllViews,
|
||||
_ => throw new FormatException($"Unknown VTank Meta action type {type}."),
|
||||
};
|
||||
|
||||
private static string Number(double value)
|
||||
{
|
||||
if (!double.IsFinite(value))
|
||||
throw new InvalidOperationException("VTank Meta numbers must be finite.");
|
||||
return value.ToString("R", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static int IntValue(double value)
|
||||
{
|
||||
if (!double.IsFinite(value) || value != Math.Truncate(value))
|
||||
throw new InvalidOperationException("VTank Meta integer fields require whole numbers.");
|
||||
return checked((int)value);
|
||||
}
|
||||
|
||||
private static void CheckDepth(LineReader reader, int depth)
|
||||
{
|
||||
if (depth > MaximumNesting)
|
||||
throw reader.Error("VTank Meta nesting is too deep.");
|
||||
}
|
||||
|
||||
private static void CheckDepth(int depth)
|
||||
{
|
||||
if (depth > MaximumNesting)
|
||||
throw new InvalidOperationException("VTank Meta nesting is too deep.");
|
||||
}
|
||||
|
||||
private sealed class LineReader
|
||||
{
|
||||
private readonly List<string> _lines;
|
||||
private int _index;
|
||||
|
||||
public LineReader(string source)
|
||||
{
|
||||
string normalized = NormalizeNewlines(source ?? string.Empty);
|
||||
_lines = normalized.Split('\n', StringSplitOptions.None).ToList();
|
||||
if (_lines.Count != 0 && _lines[^1].Length == 0)
|
||||
_lines.RemoveAt(_lines.Count - 1);
|
||||
}
|
||||
|
||||
public string Read()
|
||||
{
|
||||
if (_index >= _lines.Count)
|
||||
throw Error("Unexpected end of VTank Meta data.");
|
||||
return _lines[_index++];
|
||||
}
|
||||
|
||||
public string ReadExpected(string expected)
|
||||
{
|
||||
string actual = Read();
|
||||
if (!actual.Equals(expected, StringComparison.Ordinal))
|
||||
throw Error($"Expected '{expected}', found '{actual}'.");
|
||||
return actual;
|
||||
}
|
||||
|
||||
public void Expect(params string[] expected)
|
||||
{
|
||||
foreach (string value in expected)
|
||||
ReadExpected(value);
|
||||
}
|
||||
|
||||
public void Expect(string[] first, params string[] rest)
|
||||
{
|
||||
Expect(first);
|
||||
Expect(rest);
|
||||
}
|
||||
|
||||
public int ReadInt() => int.Parse(
|
||||
Read(), NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||
|
||||
public int ReadInt(out string line)
|
||||
{
|
||||
line = Read();
|
||||
return int.Parse(line, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public int ReadCount()
|
||||
{
|
||||
int value = ReadInt();
|
||||
if (value is < 0 or > MaximumRules)
|
||||
throw Error("Invalid VTank Meta collection count.");
|
||||
return value;
|
||||
}
|
||||
|
||||
public int ReadCount(out string line)
|
||||
{
|
||||
int value = ReadInt(out line);
|
||||
if (value is < 0 or > MaximumRules)
|
||||
throw Error("Invalid VTank Meta collection count.");
|
||||
return value;
|
||||
}
|
||||
|
||||
public double ReadDouble() => double.Parse(
|
||||
Read(), NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
|
||||
public string ReadByteArray(int length)
|
||||
{
|
||||
if (length < 0)
|
||||
throw Error("Invalid VTank Meta byte-array length.");
|
||||
string first = Read();
|
||||
if (first.Length >= length)
|
||||
{
|
||||
string value = first[..length];
|
||||
string remainder = first[length..];
|
||||
if (remainder.Length != 0)
|
||||
_lines.Insert(_index, remainder);
|
||||
return value;
|
||||
}
|
||||
|
||||
var valueBuilder = new System.Text.StringBuilder(first);
|
||||
while (valueBuilder.Length < length && _index < _lines.Count)
|
||||
{
|
||||
valueBuilder.Append("\r\n");
|
||||
valueBuilder.Append(Read());
|
||||
}
|
||||
if (valueBuilder.Length < length)
|
||||
throw Error("Truncated VTank Meta byte array.");
|
||||
string combined = valueBuilder.ToString();
|
||||
string result = combined[..length];
|
||||
string remaining = combined[length..];
|
||||
if (remaining.Length != 0)
|
||||
_lines.Insert(_index, remaining.TrimStart('\r', '\n'));
|
||||
return result;
|
||||
}
|
||||
|
||||
public void ExpectEnd()
|
||||
{
|
||||
while (_index < _lines.Count && _lines[_index].Length == 0)
|
||||
_index++;
|
||||
if (_index != _lines.Count)
|
||||
throw Error($"Unexpected trailing VTank Meta data '{_lines[_index]}'.");
|
||||
}
|
||||
|
||||
public FormatException Error(string message) =>
|
||||
new($"VTank Meta line {Math.Min(_index + 1, _lines.Count + 1)}: {message}");
|
||||
}
|
||||
|
||||
private sealed class LineWriter
|
||||
{
|
||||
private readonly List<string> _lines = [];
|
||||
private readonly List<int> _buggedByteArrays = [];
|
||||
|
||||
public void Add(params object?[] values)
|
||||
{
|
||||
foreach (object? value in values)
|
||||
_lines.Add(Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty);
|
||||
}
|
||||
|
||||
public void Add(string[] first, params object?[] rest)
|
||||
{
|
||||
Add(first.Cast<object?>().ToArray());
|
||||
Add(rest);
|
||||
}
|
||||
|
||||
public void AddBuggedByteArray(string value)
|
||||
{
|
||||
_buggedByteArrays.Add(_lines.Count);
|
||||
_lines.Add(value ?? string.Empty);
|
||||
}
|
||||
|
||||
public string Finish()
|
||||
{
|
||||
foreach (int index in _buggedByteArrays.OrderDescending())
|
||||
{
|
||||
if (index + 1 >= _lines.Count)
|
||||
throw new InvalidOperationException("CreateView cannot terminate a VTank Meta record.");
|
||||
_lines[index] += _lines[index + 1];
|
||||
_lines.RemoveAt(index + 1);
|
||||
}
|
||||
return string.Join("\r\n", _lines) + "\r\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue