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