diff --git a/src/AcDream.Plugins.MossTank/MossTankRouteProfileStore.cs b/src/AcDream.Plugins.MossTank/MossTankRouteProfileStore.cs
index 94c3505bd..9eeb4a83e 100644
--- a/src/AcDream.Plugins.MossTank/MossTankRouteProfileStore.cs
+++ b/src/AcDream.Plugins.MossTank/MossTankRouteProfileStore.cs
@@ -375,8 +375,15 @@ internal sealed class MossTankRouteProfileStore
// File naming, storage plumbing.
// ------------------------------------------------------------------
+ // Round 3 item 3: the hidden "--" prefix MUST come before the "nav_"
+ // kind marker (--nav_Name_Server.af) — putting the marker first
+ // (nav_--Name_Server.af, the pre-fix shape) means the filename does not
+ // start with "--" at all, defeating the StartsWith("--") hidden-file
+ // check both ListNavigationProfiles and ListMetaProfiles rely on and
+ // leaking this character's private per-character route to every other
+ // character's picker.
private string CurrentFileName() => _selected.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase)
- ? NavPrefix + VtankProfileDirectory.AutoCharacterFileName(_characterName, Server, "af")
+ ? VtankProfileDirectory.AutoCharacterFileName(_characterName, Server, "af", NavPrefix)
: _selected;
private static string ToFileName(string bareName) =>
diff --git a/src/AcDream.Plugins.MossTank/VtankProfileDirectory.cs b/src/AcDream.Plugins.MossTank/VtankProfileDirectory.cs
index 53d80ff86..b093482da 100644
--- a/src/AcDream.Plugins.MossTank/VtankProfileDirectory.cs
+++ b/src/AcDream.Plugins.MossTank/VtankProfileDirectory.cs
@@ -35,6 +35,22 @@ internal static class VtankProfileDirectory
internal const string DefaultLabel = "[Default]";
internal const string NoneLabel = "[None]";
+ ///
+ /// The nav_ marker (round 3 items 3/4) that distinguishes a
+ /// stand-alone route .af from a Meta profile sharing the same
+ /// flat directory and extension
+ /// — metaf's own observed convention for a nav-only .af (see the
+ /// committed nav_*.af fixtures). MUST be checked AFTER stripping
+ /// when the marker is combined with it (see
+ /// 's
+ /// marker parameter): a hidden per-character route file is named
+ /// --nav_Name_Server.af — hidden prefix FIRST, marker SECOND —
+ /// never nav_--Name_Server.af, which does not start with
+ /// at all and so defeats every
+ /// StartsWith("--") hidden-file check in this class.
+ ///
+ internal const string NavMarker = "nav_";
+
///
/// VTank's single per-character default filename
/// (uTank2/PluginCore.cs:3863-3865): --Name_Server.ext.
@@ -45,6 +61,21 @@ internal static class VtankProfileDirectory
string extension) =>
$"{HiddenPrefix}{characterName}_{server}.{extension.TrimStart('.')}";
+ ///
+ /// Overload for a per-character auto file that ALSO carries a kind
+ /// marker (currently only , for
+ /// 's route .af): the
+ /// hidden prefix always comes first so the file stays hidden from every
+ /// other character's picker exactly like the unmarked overload above —
+ /// --{marker}Name_Server.ext, e.g. --nav_Name_Server.af.
+ ///
+ public static string AutoCharacterFileName(
+ string characterName,
+ string server,
+ string extension,
+ string marker) =>
+ $"{HiddenPrefix}{marker}{characterName}_{server}.{extension.TrimStart('.')}";
+
///
/// The longer, trailing-underscore prefix
/// (uTank2/PluginCore.cs:933,3866, field dw) that marks a
@@ -155,7 +186,10 @@ internal static class VtankProfileDirectory
/// VTank's navigation-profile list (l(),
/// uTank2/PluginCore.cs:7156-7185): seeds
/// /, then every
- /// .af file that starts with neither -- nor ~~.
+ /// -marked .af file that starts with
+ /// neither -- nor ~~. Round 3 item 4: a Meta profile and a
+ /// route share this same flat, single-extension directory — without
+ /// the marker check this returned every Meta .af too.
///
public static IReadOnlyList ListNavigationProfiles(IPluginStorage storage)
{
@@ -171,6 +205,8 @@ internal static class VtankProfileDirectory
{
continue;
}
+ if (!fileName.StartsWith(NavMarker, StringComparison.Ordinal))
+ continue;
entries.Add(new ProfileEntry(fileName, fileName));
}
return entries;
@@ -180,11 +216,12 @@ internal static class VtankProfileDirectory
/// VTank's meta-profile list (ac(),
/// uTank2/PluginCore.cs:7187+): seeds
/// /, then every
- /// non--- file (a meta and a nav profile share the same
- /// directory and extension here — .af — so callers pass a
- /// distinguishing sub-extension convention if they need one; VTank
- /// itself distinguished by the separate .met/.nav
- /// extensions).
+ /// non---, non--marked file (a meta and a
+ /// nav profile share the same directory and extension here —
+ /// .af — so the marker is what VTank's own separate
+ /// .met/.nav extensions used to provide; round 3 item 4:
+ /// without excluding files this returned every
+ /// route .af too).
///
public static IReadOnlyList ListMetaProfiles(IPluginStorage storage)
{
@@ -197,6 +234,8 @@ internal static class VtankProfileDirectory
{
if (fileName.StartsWith(HiddenPrefix, StringComparison.Ordinal))
continue;
+ if (fileName.StartsWith(NavMarker, StringComparison.Ordinal))
+ continue;
entries.Add(new ProfileEntry(fileName, fileName));
}
return entries;
diff --git a/tests/AcDream.Plugins.MossTank.Tests/NavigationTests.cs b/tests/AcDream.Plugins.MossTank.Tests/NavigationTests.cs
index e4c71ff64..0aec9e989 100644
--- a/tests/AcDream.Plugins.MossTank.Tests/NavigationTests.cs
+++ b/tests/AcDream.Plugins.MossTank.Tests/NavigationTests.cs
@@ -514,8 +514,9 @@ public sealed class NavigationTests
var storage = new MemoryStorage();
string legacyKey = LegacyRouteByCharacterKey("Barris");
storage.Text[legacyKey] = """{ "Mode": 1, "Waypoints": [] }""";
- string realKey = "nav_" + VtankProfileDirectory.AutoCharacterFileName(
- "Barris", string.Empty, "af");
+ // Round 3 item 3: hidden prefix first, nav_ marker second.
+ string realKey = VtankProfileDirectory.AutoCharacterFileName(
+ "Barris", string.Empty, "af", VtankProfileDirectory.NavMarker);
var real = new NavigationSettings { Mode = RouteMode.Circular };
real.Waypoints.Add(new RouteWaypoint
{
diff --git a/tests/AcDream.Plugins.MossTank.Tests/VtankProfileDirectoryTests.cs b/tests/AcDream.Plugins.MossTank.Tests/VtankProfileDirectoryTests.cs
index f5907cf38..76862fabe 100644
--- a/tests/AcDream.Plugins.MossTank.Tests/VtankProfileDirectoryTests.cs
+++ b/tests/AcDream.Plugins.MossTank.Tests/VtankProfileDirectoryTests.cs
@@ -74,8 +74,10 @@ public sealed class VtankProfileDirectoryTests
public void ListNavigationProfilesFiltersBothReservedPrefixes()
{
var storage = new MemoryStorage();
- storage.WriteText("Hunt.af", "1\r\n");
- storage.WriteText("--Barris_Coldeve.af", "1\r\n");
+ // Round 3 item 4: a route .af must carry the nav_ marker to be
+ // listed at all — an un-marked file is a Meta profile, not a route.
+ storage.WriteText("nav_Hunt.af", "1\r\n");
+ storage.WriteText("--nav_Barris_Coldeve.af", "1\r\n");
storage.WriteText("~~backup.af", "1\r\n");
IReadOnlyList entries =
@@ -83,11 +85,74 @@ public sealed class VtankProfileDirectoryTests
Assert.Equal(VtankProfileDirectory.NoneLabel, entries[0].DisplayName);
Assert.Equal(VtankProfileDirectory.ByCharacterLabel, entries[1].DisplayName);
- Assert.Contains(entries, static e => e.DisplayName == "Hunt.af");
+ Assert.Contains(entries, static e => e.DisplayName == "nav_Hunt.af");
Assert.DoesNotContain(entries, static e => e.FileName.StartsWith("--", StringComparison.Ordinal));
Assert.DoesNotContain(entries, static e => e.FileName.StartsWith("~~", StringComparison.Ordinal));
}
+ ///
+ /// Round 3 items 3/4: a Meta profile and a route share the exact same
+ /// flat directory and .af extension, so the nav_ marker (and the
+ /// hidden "--" prefix coming BEFORE it on a per-character auto file) is
+ /// the only thing that keeps the two pickers from showing each other's
+ /// files. This is the disambiguation test both pickers must pass
+ /// together against one mixed directory.
+ ///
+ [Fact]
+ public void NavigationAndMetaPickersPartitionTheSameMixedDirectory()
+ {
+ var storage = new MemoryStorage();
+ storage.WriteText("nav_Hunt.af", "1\r\n");
+ storage.WriteText("MyMeta.af", "1\r\n");
+ // Barris's own hidden per-character route: hidden prefix FIRST,
+ // marker SECOND (item 3's fix) — must be invisible to BOTH pickers,
+ // not just the nav one.
+ storage.WriteText("--nav_Barris_Coldeve.af", "1\r\n");
+ // Barris's own hidden per-character Meta file (unmarked, as
+ // MossTankMetaProfileStore has always named it) — must likewise be
+ // invisible to both.
+ storage.WriteText("--Barris_Coldeve.af", "1\r\n");
+
+ IReadOnlyList navEntries =
+ VtankProfileDirectory.ListNavigationProfiles(storage);
+ IReadOnlyList metaEntries =
+ VtankProfileDirectory.ListMetaProfiles(storage);
+
+ Assert.Contains(navEntries, static e => e.FileName == "nav_Hunt.af");
+ Assert.DoesNotContain(navEntries, static e => e.FileName == "MyMeta.af");
+ Assert.DoesNotContain(navEntries, static e => e.FileName.StartsWith("--", StringComparison.Ordinal));
+
+ Assert.Contains(metaEntries, static e => e.FileName == "MyMeta.af");
+ Assert.DoesNotContain(metaEntries, static e => e.FileName == "nav_Hunt.af");
+ Assert.DoesNotContain(metaEntries, static e => e.FileName.StartsWith("--", StringComparison.Ordinal));
+ }
+
+ ///
+ /// Round 3 item 3 (BLOCKER-adjacent leak): before the fix, a hidden
+ /// per-character route file was named nav_--Name_Server.af
+ /// (marker first) — which does NOT start with "--" and so defeated the
+ /// hidden-file check in BOTH pickers, leaking another character's
+ /// private route/Meta binding.
+ ///
+ [Fact]
+ public void AnotherCharactersAutoRouteIsHiddenFromBothPickers()
+ {
+ var storage = new MemoryStorage();
+ // The CORRECT (post-fix) shape: hidden prefix first, marker second.
+ storage.WriteText(
+ VtankProfileDirectory.AutoCharacterFileName(
+ "Someone", "Coldeve", "af", VtankProfileDirectory.NavMarker),
+ "1\r\n");
+
+ IReadOnlyList navEntries =
+ VtankProfileDirectory.ListNavigationProfiles(storage);
+ IReadOnlyList metaEntries =
+ VtankProfileDirectory.ListMetaProfiles(storage);
+
+ Assert.DoesNotContain(navEntries, static e => e.FileName.Contains("Someone", StringComparison.Ordinal));
+ Assert.DoesNotContain(metaEntries, static e => e.FileName.Contains("Someone", StringComparison.Ordinal));
+ }
+
[Fact]
public void ListingsOnUnavailableStorageOnlySeedTheBuiltInEntries()
{