diff --git a/src/AcDream.Plugins.MossTank/MetafSerializer.cs b/src/AcDream.Plugins.MossTank/MetafSerializer.cs index a8263160..a6817431 100644 --- a/src/AcDream.Plugins.MossTank/MetafSerializer.cs +++ b/src/AcDream.Plugins.MossTank/MetafSerializer.cs @@ -573,11 +573,109 @@ internal static class MetafSerializer { var settings = new NavigationSettings(); ApplyNavBody(settings, navType, nodes, followTargetId, followTargetName); - return VtankNavRouteSerializer.Save(settings); + return WriteBinaryNavBlob(settings); } private static string EmptyNavBlob() => "uTank2 NAV 1.2\r\n1\r\n0\r\n"; + /// + /// Writes the "uTank2 NAV 1.2" binary-format TEXT blob + /// already expects + /// in (per MetaEngine's existing + /// contract, previously produced by + /// VtankMetaProfileSerializer.ReadEmbeddedNavigation when + /// importing a real binary .met). This is a small, deliberate + /// duplicate of what used to be VtankNavRouteSerializer.Save — + /// that public writer was deleted per the owner's "MossTank does not + /// implement .met and does not author .nav" direction (only the reader + /// stays, as a one-shot import), but an EmbedNav action still needs + /// this exact in-memory blob shape, which is a MossTank runtime + /// contract, not a VTank file on disk. + /// + private static string WriteBinaryNavBlob(NavigationSettings source) + { + var lines = new List { "uTank2 NAV 1.2" }; + lines.Add(source.Mode switch + { + RouteMode.Circular => "1", + RouteMode.Linear => "2", + RouteMode.Target => "3", + RouteMode.Once => "4", + _ => throw new InvalidOperationException("Unknown navigation type."), + }); + if (source.Mode == RouteMode.Target) + { + lines.Add(source.FollowTargetName ?? string.Empty); + lines.Add(unchecked((int)source.FollowTargetObjectId) + .ToString(CultureInfo.InvariantCulture)); + return string.Join("\r\n", lines) + "\r\n"; + } + lines.Add(source.Waypoints.Count.ToString(CultureInfo.InvariantCulture)); + foreach (RouteWaypoint waypoint in source.Waypoints) + WriteBinaryWaypoint(lines, waypoint); + return string.Join("\r\n", lines) + "\r\n"; + } + + private static void WriteBinaryWaypoint(List lines, RouteWaypoint waypoint) + { + lines.Add(((int)waypoint.Type).ToString(CultureInfo.InvariantCulture)); + lines.Add(FormatBinaryDouble(waypoint.Position.EastWest)); + lines.Add(FormatBinaryDouble(waypoint.Position.NorthSouth)); + lines.Add(FormatBinaryDouble(waypoint.Position.Elevation)); + lines.Add("0"); + switch (waypoint.Type) + { + case RouteWaypointType.Point: + case RouteWaypointType.Checkpoint: + break; + case RouteWaypointType.Portal: + lines.Add(unchecked((int)waypoint.ObjectId).ToString(CultureInfo.InvariantCulture)); + break; + case RouteWaypointType.Recall: + lines.Add(waypoint.RecallSpellId.ToString(CultureInfo.InvariantCulture)); + break; + case RouteWaypointType.Pause: + lines.Add(waypoint.DurationMilliseconds.ToString(CultureInfo.InvariantCulture)); + break; + case RouteWaypointType.ChatCommand: + lines.Add(waypoint.Text ?? string.Empty); + break; + case RouteWaypointType.OpenVendor: + lines.Add(unchecked((int)waypoint.ObjectId).ToString(CultureInfo.InvariantCulture)); + lines.Add(waypoint.ObjectName ?? string.Empty); + break; + case RouteWaypointType.PortalByName: + case RouteWaypointType.UseNpc: + lines.Add(waypoint.ObjectName ?? string.Empty); + int objectClass = waypoint.LegacyObjectClass != 0 + ? waypoint.LegacyObjectClass + : waypoint.Type == RouteWaypointType.PortalByName ? 14 : 37; + lines.Add(objectClass.ToString(CultureInfo.InvariantCulture)); + lines.Add(waypoint.LegacyReferenceValid.ToString(CultureInfo.InvariantCulture)); + lines.Add(FormatBinaryDouble(waypoint.Position.EastWest)); + lines.Add(FormatBinaryDouble(waypoint.Position.NorthSouth)); + lines.Add(FormatBinaryDouble(waypoint.Position.Elevation)); + break; + case RouteWaypointType.Jump: + lines.Add(FormatBinaryDouble(waypoint.JumpHeadingDegrees)); + lines.Add(waypoint.JumpRun.ToString(CultureInfo.InvariantCulture)); + string suffix = waypoint.JumpDirection switch + { + RouteJumpDirection.StrafeLeft => "4", + RouteJumpDirection.StrafeRight => "5", + _ => "3", + }; + lines.Add(waypoint.JumpChargeMilliseconds.ToString( + "0.0000", CultureInfo.InvariantCulture) + suffix); + break; + default: + throw new InvalidOperationException($"Unknown waypoint type {waypoint.Type}."); + } + } + + private static string FormatBinaryDouble(double value) => + Convert.ToString(value, CultureInfo.InvariantCulture) ?? "0"; + // ================================================================== // NAV: blocks (both inside a full .af meta and in a nav-only file). // ================================================================== diff --git a/src/AcDream.Plugins.MossTank/MossTankMetaProfileStore.cs b/src/AcDream.Plugins.MossTank/MossTankMetaProfileStore.cs index 35eecd04..e6c4cc2f 100644 --- a/src/AcDream.Plugins.MossTank/MossTankMetaProfileStore.cs +++ b/src/AcDream.Plugins.MossTank/MossTankMetaProfileStore.cs @@ -190,6 +190,12 @@ internal sealed class MossTankMetaProfileStore private void SaveIndex() => Write(IndexKey, _index); + /// + /// .af is the only VTank-compatible export format now (Campaign + /// VT slice 1 Part A — MossTank no longer authors the binary + /// .met format at all, matching 's + /// demotion to a one-shot import). + /// private void WriteLegacyExport(string name, MetaProfile profile) { if (!_host.Storage.IsAvailable) @@ -197,8 +203,8 @@ internal sealed class MossTankMetaProfileStore try { _host.Storage.WriteText( - $"exports/{LegacyFileName(name)}.met", - VtankMetaProfileSerializer.Save(profile)); + $"exports/{LegacyFileName(name)}.af", + MetafSerializer.SaveMeta(profile)); } catch (Exception error) { diff --git a/src/AcDream.Plugins.MossTank/MossTankRouteProfileStore.cs b/src/AcDream.Plugins.MossTank/MossTankRouteProfileStore.cs index e1773615..1eca05e0 100644 --- a/src/AcDream.Plugins.MossTank/MossTankRouteProfileStore.cs +++ b/src/AcDream.Plugins.MossTank/MossTankRouteProfileStore.cs @@ -260,6 +260,12 @@ internal sealed class MossTankRouteProfileStore private void SaveIndex() => Write(IndexKey, _index); + /// + /// .af is the only VTank-compatible export format now (Campaign + /// VT slice 1 Part A — MossTank no longer authors the binary + /// .nav format at all, matching 's + /// demotion to a one-shot import). + /// private void WriteLegacyExport(string name, NavigationSettings settings) { if (!_host.Storage.IsAvailable) @@ -267,8 +273,8 @@ internal sealed class MossTankRouteProfileStore try { _host.Storage.WriteText( - $"exports/{LegacyFileName(name)}.nav", - VtankNavRouteSerializer.Save(settings)); + $"exports/{LegacyFileName(name)}.af", + MetafSerializer.SaveNav(settings)); } catch (Exception error) { diff --git a/src/AcDream.Plugins.MossTank/VtankMetaProfileSerializer.cs b/src/AcDream.Plugins.MossTank/VtankMetaProfileSerializer.cs index faa3a5bb..342f397c 100644 --- a/src/AcDream.Plugins.MossTank/VtankMetaProfileSerializer.cs +++ b/src/AcDream.Plugins.MossTank/VtankMetaProfileSerializer.cs @@ -3,9 +3,14 @@ using System.Globalization; namespace AcDream.Plugins.MossTank; /// -/// Reads and writes VTank's exact line-encoded CondAct Meta database. -/// The format is the public interchange contract used by legacy .met -/// profiles; it is deliberately independent from MossTank's native JSON store. +/// Reads VTank's exact line-encoded CondAct Meta database — a +/// one-shot import path only. Per the owner's 2026-09-06 "MossTank does not +/// implement .met" direction (Campaign VT slice 1 Part A), the +/// writer that used to live here was deleted: .af +/// () is the only storage/authoring format for +/// meta profiles now. This class only converts a legacy binary .met +/// file into a so it can be saved straight back +/// out as .af. /// internal static class VtankMetaProfileSerializer { @@ -62,23 +67,6 @@ internal static class VtankMetaProfileSerializer } } - 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); @@ -304,195 +292,12 @@ internal static class VtankMetaProfileSerializer 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, @@ -526,27 +331,6 @@ internal static class VtankMetaProfileSerializer _ => 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, @@ -568,32 +352,12 @@ internal static class VtankMetaProfileSerializer _ => 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 _lines; @@ -703,40 +467,4 @@ internal static class VtankMetaProfileSerializer public FormatException Error(string message) => new($"VTank Meta line {Math.Min(_index + 1, _lines.Count + 1)}: {message}"); } - - private sealed class LineWriter - { - private readonly List _lines = []; - private readonly List _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().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"; - } - } } diff --git a/src/AcDream.Plugins.MossTank/VtankNavRouteSerializer.cs b/src/AcDream.Plugins.MossTank/VtankNavRouteSerializer.cs index 88c8561a..aa995081 100644 --- a/src/AcDream.Plugins.MossTank/VtankNavRouteSerializer.cs +++ b/src/AcDream.Plugins.MossTank/VtankNavRouteSerializer.cs @@ -3,40 +3,21 @@ using AcDream.Plugin.Abstractions; namespace AcDream.Plugins.MossTank; -/// Reader for VTank's verbatim uTank2 NAV 1.2 format. +/// +/// Reader for VTank's verbatim uTank2 NAV 1.2 format — a one-shot +/// import path only. Per the owner's 2026-09-06 "MossTank does not author +/// .nav" direction (Campaign VT slice 1 Part A), the writer that +/// used to live here was deleted: .af () +/// is the only storage/authoring format for navigation routes now. The +/// small binary-blob writer 's embedded-navigation +/// contract still needs lives in itself +/// (WriteBinaryNavBlob) rather than here, since that shape is a +/// MossTank runtime contract, not a VTank file on disk. +/// internal static class VtankNavRouteSerializer { private const string Header = "uTank2 NAV 1.2"; - public static string Save(NavigationSettings source) - { - ArgumentNullException.ThrowIfNull(source); - var writer = new StringWriter(CultureInfo.InvariantCulture) - { - NewLine = "\r\n", - }; - writer.WriteLine(Header); - writer.WriteLine(source.Mode switch - { - RouteMode.Circular => 1, - RouteMode.Linear => 2, - RouteMode.Target => 3, - RouteMode.Once => 4, - _ => throw new InvalidOperationException("Unknown navigation type."), - }); - if (source.Mode == RouteMode.Target) - { - writer.WriteLine(source.FollowTargetName ?? string.Empty); - writer.WriteLine(unchecked((int)source.FollowTargetObjectId)); - return writer.ToString(); - } - - writer.WriteLine(source.Waypoints.Count); - foreach (RouteWaypoint waypoint in source.Waypoints) - WriteWaypoint(writer, waypoint); - return writer.ToString(); - } - public static bool TryLoad( string source, NavigationSettings target, @@ -165,76 +146,6 @@ internal static class VtankNavRouteSerializer return waypoint; } - private static void WriteWaypoint(TextWriter writer, RouteWaypoint waypoint) - { - int type = (int)waypoint.Type; - writer.WriteLine(type.ToString(CultureInfo.InvariantCulture)); - WriteDouble(writer, waypoint.Position.EastWest); - WriteDouble(writer, waypoint.Position.NorthSouth); - WriteDouble(writer, waypoint.Position.Elevation); - writer.WriteLine("0"); - switch (waypoint.Type) - { - case RouteWaypointType.Point: - case RouteWaypointType.Checkpoint: - break; - case RouteWaypointType.Portal: - writer.WriteLine(unchecked((int)waypoint.ObjectId) - .ToString(CultureInfo.InvariantCulture)); - break; - case RouteWaypointType.Recall: - writer.WriteLine(waypoint.RecallSpellId - .ToString(CultureInfo.InvariantCulture)); - break; - case RouteWaypointType.Pause: - writer.WriteLine(waypoint.DurationMilliseconds - .ToString(CultureInfo.InvariantCulture)); - break; - case RouteWaypointType.ChatCommand: - writer.WriteLine(waypoint.Text ?? string.Empty); - break; - case RouteWaypointType.OpenVendor: - writer.WriteLine(unchecked((int)waypoint.ObjectId) - .ToString(CultureInfo.InvariantCulture)); - writer.WriteLine(waypoint.ObjectName ?? string.Empty); - break; - case RouteWaypointType.PortalByName: - case RouteWaypointType.UseNpc: - writer.WriteLine(waypoint.ObjectName ?? string.Empty); - int objectClass = waypoint.LegacyObjectClass != 0 - ? waypoint.LegacyObjectClass - : waypoint.Type == RouteWaypointType.PortalByName ? 14 : 37; - writer.WriteLine(objectClass.ToString(CultureInfo.InvariantCulture)); - writer.WriteLine(waypoint.LegacyReferenceValid - .ToString(CultureInfo.InvariantCulture)); - WriteDouble(writer, waypoint.Position.EastWest); - WriteDouble(writer, waypoint.Position.NorthSouth); - WriteDouble(writer, waypoint.Position.Elevation); - break; - case RouteWaypointType.Jump: - WriteDouble(writer, waypoint.JumpHeadingDegrees); - writer.WriteLine(waypoint.JumpRun.ToString(CultureInfo.InvariantCulture)); - string suffix = waypoint.JumpDirection switch - { - RouteJumpDirection.StrafeLeft => "4", - RouteJumpDirection.StrafeRight => "5", - _ => "3", - }; - writer.WriteLine( - waypoint.JumpChargeMilliseconds.ToString( - "0.0000", - CultureInfo.InvariantCulture) - + suffix); - break; - default: - throw new InvalidOperationException( - $"Unknown waypoint type {waypoint.Type}."); - } - } - - private static void WriteDouble(TextWriter writer, double value) => - writer.WriteLine(Convert.ToString(value, CultureInfo.InvariantCulture)); - private static void ParseJump(string source, RouteWaypoint target) { string value = source.Trim(); diff --git a/tests/AcDream.Plugins.MossTank.Tests/MossTankPanelTests.cs b/tests/AcDream.Plugins.MossTank.Tests/MossTankPanelTests.cs index 6df87d0d..2fab57bd 100644 --- a/tests/AcDream.Plugins.MossTank.Tests/MossTankPanelTests.cs +++ b/tests/AcDream.Plugins.MossTank.Tests/MossTankPanelTests.cs @@ -672,10 +672,13 @@ public sealed class MossTankPanelTests Assert.Single(panel.RouteRows); Assert.Contains("12.5", panel.RouteRows[0], StringComparison.Ordinal); + // .af is the only VTank-compatible export format now (Campaign VT + // slice 1 Part A — the .nav writer was deleted, one-shot import + // only). Command(panel, "nav save Exported.nav"); Assert.StartsWith( - "uTank2 NAV 1.2\r\n", - storage.Text["exports/Exported.nav"], + "NAV: ", + storage.Text["exports/Exported.af"], StringComparison.Ordinal); } @@ -683,23 +686,16 @@ public sealed class MossTankPanelTests public void MetaCommandsImportAndExportExactVtankMetFiles() { var storage = new MemoryStorage(); - storage.Text["imports/Legacy.met"] = VtankMetaProfileSerializer.Save( - new MetaProfile - { - Rules = - [ - new MetaRule - { - State = "Default", - Condition = MetaCondition.Always(), - Action = new MetaAction - { - Kind = MetaActionKind.ChatCommand, - Text = "/say imported", - }, - }, - ], - }); + // Hand-authored CondAct binary payload (one rule: Always -> Chat + // "/say imported", state "Default") — VtankMetaProfileSerializer's + // writer was deleted (one-shot import only now), so this is built + // directly from the exact format its TryLoad still parses, matching + // condition type 1 (Always) / action type 2 (ChatCommand). + storage.Text["imports/Legacy.met"] = + "1\r\nCondAct\r\n5\r\nCType\r\nAType\r\nCData\r\nAData\r\nState\r\n" + + "n\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 imported\r\n" + + "s\r\nDefault\r\n"; var panel = new MossTankPanel(new FakeHost( new FakeAutomation(), storage)); @@ -710,18 +706,33 @@ public sealed class MossTankPanelTests Assert.Single(panel.MetaRows); Assert.Contains("/say imported", panel.MetaRows[0], StringComparison.Ordinal); + // .af is the only VTank-compatible export format now (Campaign VT + // slice 1 Part A — the .met writer was deleted, one-shot import + // only). Command(panel, "meta save Exported.met"); Assert.StartsWith( - "1\r\nCondAct\r\n5\r\n", - storage.Text["exports/Exported.met"], + "STATE: ", + storage.Text["exports/Exported.af"], StringComparison.Ordinal); - Assert.True(VtankMetaProfileSerializer.TryLoad( - storage.Text["exports/Exported.met"], + Assert.True(MetafSerializer.TryLoadMeta( + storage.Text["exports/Exported.af"], + NoOpSpellCatalogForExport.Instance, out MetaProfile exported, out string error), error); Assert.Single(exported.Rules); } + private sealed class NoOpSpellCatalogForExport : ISpellCatalog + { + public static NoOpSpellCatalogForExport Instance { get; } = new(); + public IReadOnlyList KnownSelfBuffs => []; + public bool TryGet(uint spellId, out PluginSpellInfo info) + { + info = default; + return false; + } + } + [Fact] public void ByCharacterProfilesAreIsolatedByCharacterName() { diff --git a/tests/AcDream.Plugins.MossTank.Tests/VtankNavRouteSerializerTests.cs b/tests/AcDream.Plugins.MossTank.Tests/VtankNavRouteSerializerTests.cs index 5722c0dd..4d5a7c62 100644 --- a/tests/AcDream.Plugins.MossTank.Tests/VtankNavRouteSerializerTests.cs +++ b/tests/AcDream.Plugins.MossTank.Tests/VtankNavRouteSerializerTests.cs @@ -112,21 +112,6 @@ public sealed class VtankNavRouteSerializerTests Assert.True(settings.Waypoints[9].JumpRun); Assert.Equal(1000, settings.Waypoints[9].JumpChargeMilliseconds); Assert.Equal(RouteJumpDirection.StrafeRight, settings.Waypoints[9].JumpDirection); - - string saved = VtankNavRouteSerializer.Save(settings); - Assert.StartsWith("uTank2 NAV 1.2\r\n", saved, StringComparison.Ordinal); - Assert.Contains("1000.00005\r\n", saved, StringComparison.Ordinal); - var roundTrip = new NavigationSettings(); - Assert.True(VtankNavRouteSerializer.TryLoad( - saved, - roundTrip, - NoOpSpellCatalog.Instance, - out error), error); - Assert.Equal(10, roundTrip.Waypoints.Count); - Assert.Equal(14, roundTrip.Waypoints[6].LegacyObjectClass); - Assert.Equal(RouteJumpDirection.StrafeRight, - roundTrip.Waypoints[9].JumpDirection); - Assert.Equal(1000, roundTrip.Waypoints[9].JumpChargeMilliseconds); } [Fact] @@ -163,29 +148,6 @@ public sealed class VtankNavRouteSerializerTests Assert.Single(settings.Waypoints); } - [Fact] - public void TargetRoutePreservesSignedRetailObjectIdBitPattern() - { - var original = new NavigationSettings - { - Mode = RouteMode.Target, - FollowTargetName = "High-bit fellow", - FollowTargetObjectId = 0xF1234567u, - }; - - string saved = VtankNavRouteSerializer.Save(original); - var loaded = new NavigationSettings(); - Assert.True(VtankNavRouteSerializer.TryLoad( - saved, - loaded, - NoOpSpellCatalog.Instance, - out string error), error); - - Assert.Equal(RouteMode.Target, loaded.Mode); - Assert.Equal("High-bit fellow", loaded.FollowTargetName); - Assert.Equal(0xF1234567u, loaded.FollowTargetObjectId); - } - private sealed class NoOpSpellCatalog : ISpellCatalog { public static NoOpSpellCatalog Instance { get; } = new();