feat(vt): A2 VTank profile directory resolution + naming rules
Campaign VT slice 1 Part A, deliverable 2 (foundation only - see the
closeout note in the final report for what is not yet wired up).
VtankProfileDirectory.cs resolves the on-disk VTank profile directory
through IPluginHost.VtankProfileDirectory (a new, minimal, default-null
interface member - never a hard-coded Windows path in the plugin itself;
an App-composed host may point it at a real installed VTank's own profile
folder for direct interop, but that discovery belongs entirely to the
host) and falls back to a portable default built with Path.Combine only
(LocalApplicationData/acdream/vtank, which resolves through .NET's
XDG-aware base-directory logic on Linux). It also ports VTank's real
naming/selection rules from docs/research/vtank-kb/01-settings-and-
profiles.md section 3: the per-character auto file (--Name_Server.ext),
the longer --Name_Server_ sub-profile prefix and its "[Char] suffix"
display form, the "--"/"~~" hidden-prefix filtering for settings/nav/meta
profile listings, and the seeded [Default]/[By char]/[None] entries -
verified against the owner's own live directory listing
(--Barris_Coldeve*.usd family).
Owed: this lands the directory+naming foundation and its own test
coverage, but does not yet wire MossTankProfileStore's Create/Select/Load/
Save (still JSON-indexed) to read/write real .usd files through it, nor
MossTankMetaProfileStore/MossTankRouteProfileStore to make .af their
primary directory-backed storage rather than a legacy-export sidecar
(commit 3ff9461ef). That deeper rewrite of already-widely-used,
already-tested profile stores was judged too large a change to land
correctly under this slice's remaining time without a real risk of
destabilizing them; flagged in the closeout for the owner/next slice.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
3ff9461efe
commit
0d10399e0e
4 changed files with 408 additions and 190 deletions
|
|
@ -42,4 +42,18 @@ public interface IPluginHost
|
|||
/// host kind.
|
||||
/// </summary>
|
||||
IAutomationSurface Automation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Absolute filesystem directory a VTank-compatible plugin should treat
|
||||
/// as the VTank profile folder (real <c>.usd</c>/<c>.ast</c>/<c>.af</c>
|
||||
/// files, VTank's own naming rules). <see langword="null"/> when the
|
||||
/// host has no opinion, in which case the plugin falls back to its own
|
||||
/// portable per-OS default (never a hard-coded Windows path) — see
|
||||
/// <c>AcDream.Plugins.MossTank.VtankProfileDirectory</c>. A graphical
|
||||
/// host may point this at a real installed VTank's own profile
|
||||
/// directory for direct interop; that platform-specific discovery
|
||||
/// belongs entirely to the host composing this property, never to the
|
||||
/// plugin reading it.
|
||||
/// </summary>
|
||||
string? VtankProfileDirectory => null;
|
||||
}
|
||||
|
|
|
|||
214
src/AcDream.Plugins.MossTank/VtankProfileDirectory.cs
Normal file
214
src/AcDream.Plugins.MossTank/VtankProfileDirectory.cs
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
using AcDream.Plugin.Abstractions;
|
||||
|
||||
namespace AcDream.Plugins.MossTank;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the on-disk VTank profile directory and implements VTank's real
|
||||
/// naming/selection rules (<c>docs/research/vtank-kb/01-settings-and-profiles.md</c>
|
||||
/// section 3): the per-character auto file, the longer <c>--Name_Server_</c>
|
||||
/// sub-profile prefix, and which filenames a given character can see.
|
||||
///
|
||||
/// The directory itself is never hard-coded here: <see cref="Resolve"/>
|
||||
/// prefers <see cref="IPluginHost.VtankProfileDirectory"/> (an App-composed
|
||||
/// path — on Windows that may be a real installed VTank's own profile
|
||||
/// folder for direct interop; that discovery belongs to the host, not this
|
||||
/// plugin) and only falls back to a portable, cross-platform default under
|
||||
/// the user's own local-app-data directory when the host has no opinion.
|
||||
/// <see cref="Environment.SpecialFolder.LocalApplicationData"/> resolves to
|
||||
/// <c>%LOCALAPPDATA%</c> on Windows and (via .NET's XDG-aware base-directory
|
||||
/// resolution) <c>$XDG_DATA_HOME</c> (or <c>~/.local/share</c>) on Linux —
|
||||
/// built exclusively with <see cref="Path.Combine(string, string)"/>, so it
|
||||
/// never contains a literal backslash.
|
||||
/// </summary>
|
||||
internal static class VtankProfileDirectory
|
||||
{
|
||||
private const string PortableFolderName = "vtank";
|
||||
|
||||
/// <summary>The real VTank "--" reserved-prefix marker (section 3).</summary>
|
||||
internal const string HiddenPrefix = "--";
|
||||
|
||||
/// <summary>
|
||||
/// The nav-profile-only "~~" reserved prefix (section 3) — VTank's own
|
||||
/// producer/purpose was not determined by the KB research pass either;
|
||||
/// this only reproduces the filter.
|
||||
/// </summary>
|
||||
internal const string NavHiddenPrefix = "~~";
|
||||
|
||||
internal const string ByCharacterLabel = "[By char]";
|
||||
internal const string DefaultLabel = "[Default]";
|
||||
internal const string NoneLabel = "[None]";
|
||||
|
||||
public static string Resolve(IPluginHost host)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(host);
|
||||
return !string.IsNullOrWhiteSpace(host.VtankProfileDirectory)
|
||||
? host.VtankProfileDirectory
|
||||
: PortableDefault();
|
||||
}
|
||||
|
||||
internal static string PortableDefault() => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"acdream",
|
||||
PortableFolderName);
|
||||
|
||||
/// <summary>
|
||||
/// VTank's single per-character default filename
|
||||
/// (<c>uTank2/PluginCore.cs:3863-3865</c>): <c>--Name_Server.ext</c>.
|
||||
/// </summary>
|
||||
public static string AutoCharacterFileName(
|
||||
string characterName,
|
||||
string server,
|
||||
string extension) =>
|
||||
$"{HiddenPrefix}{characterName}_{server}.{extension.TrimStart('.')}";
|
||||
|
||||
/// <summary>
|
||||
/// The longer, trailing-underscore prefix
|
||||
/// (<c>uTank2/PluginCore.cs:933,3866</c>, field <c>dw</c>) that marks a
|
||||
/// *named sub-profile* belonging to one character, distinct from that
|
||||
/// character's single auto file above.
|
||||
/// </summary>
|
||||
public static string SubProfilePrefix(string characterName, string server) =>
|
||||
$"{HiddenPrefix}{characterName}_{server}_";
|
||||
|
||||
/// <summary>
|
||||
/// True when <paramref name="fileName"/> must be hidden from the
|
||||
/// cross-character profile picker for <paramref name="characterName"/>/
|
||||
/// <paramref name="server"/>: any <c>--</c>-prefixed name that is not
|
||||
/// this character's own sub-profile family
|
||||
/// (<c>uTank2/PluginCore.cs:7020,7072,7144</c>).
|
||||
/// </summary>
|
||||
public static bool IsHiddenFromOtherCharacters(
|
||||
string fileName,
|
||||
string characterName,
|
||||
string server) =>
|
||||
fileName.StartsWith(HiddenPrefix, StringComparison.Ordinal)
|
||||
&& !fileName.StartsWith(
|
||||
SubProfilePrefix(characterName, server),
|
||||
StringComparison.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// The "[Char] suffix" display form for one of this character's own
|
||||
/// sub-profiles (<c>uTank2/PluginCore.cs:7016-7046</c>): the
|
||||
/// <c>--Name_Server_</c> prefix and the file extension are both
|
||||
/// stripped. Returns <see langword="null"/> for a filename that is not
|
||||
/// one of this character's sub-profiles.
|
||||
/// </summary>
|
||||
public static string? TryDisplayName(
|
||||
string fileName,
|
||||
string characterName,
|
||||
string server)
|
||||
{
|
||||
string prefix = SubProfilePrefix(characterName, server);
|
||||
if (!fileName.StartsWith(prefix, StringComparison.Ordinal))
|
||||
return null;
|
||||
string withoutPrefix = fileName[prefix.Length..];
|
||||
int dot = withoutPrefix.LastIndexOf('.');
|
||||
string suffix = dot >= 0 ? withoutPrefix[..dot] : withoutPrefix;
|
||||
return suffix.Length == 0 ? null : $"[Char] {suffix}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One entry in a profile picker: the real on-disk file name, and the
|
||||
/// label VTank would show for it.
|
||||
/// </summary>
|
||||
public readonly record struct ProfileEntry(string FileName, string DisplayName);
|
||||
|
||||
/// <summary>
|
||||
/// VTank's settings-profile list (<c>a0()</c>,
|
||||
/// <c>uTank2/PluginCore.cs:7057-7115</c>): seeds
|
||||
/// <see cref="DefaultLabel"/> and <see cref="ByCharacterLabel"/> first,
|
||||
/// then every non-<c>--</c> <c>.usd</c> file (optionally filtered to
|
||||
/// only this character's own, the "Mine only" checkbox), plus every one
|
||||
/// of this character's own <c>--Name_Server_*</c> sub-profiles shown as
|
||||
/// <c>[Char] suffix</c>.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<ProfileEntry> ListSettingsProfiles(
|
||||
string directory,
|
||||
string characterName,
|
||||
string server,
|
||||
bool mineOnly)
|
||||
{
|
||||
var entries = new List<ProfileEntry>
|
||||
{
|
||||
new(string.Empty, DefaultLabel),
|
||||
new(string.Empty, ByCharacterLabel),
|
||||
};
|
||||
foreach (string fileName in EnumerateFileNames(directory, "*.usd"))
|
||||
{
|
||||
string? subProfileDisplay = TryDisplayName(fileName, characterName, server);
|
||||
if (subProfileDisplay is not null)
|
||||
{
|
||||
entries.Add(new ProfileEntry(fileName, subProfileDisplay));
|
||||
continue;
|
||||
}
|
||||
if (fileName.StartsWith(HiddenPrefix, StringComparison.Ordinal))
|
||||
continue; // someone else's --Name_Server(.usd|_*) family.
|
||||
if (mineOnly)
|
||||
continue; // "Mine only": only the sub-profiles handled above.
|
||||
entries.Add(new ProfileEntry(fileName, fileName));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VTank's navigation-profile list (<c>l()</c>,
|
||||
/// <c>uTank2/PluginCore.cs:7156-7185</c>): seeds
|
||||
/// <see cref="NoneLabel"/>/<see cref="ByCharacterLabel"/>, then every
|
||||
/// <c>.af</c> file that starts with neither <c>--</c> nor <c>~~</c>.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<ProfileEntry> ListNavigationProfiles(string directory)
|
||||
{
|
||||
var entries = new List<ProfileEntry>
|
||||
{
|
||||
new(string.Empty, NoneLabel),
|
||||
new(string.Empty, ByCharacterLabel),
|
||||
};
|
||||
foreach (string fileName in EnumerateFileNames(directory, "*.af"))
|
||||
{
|
||||
if (fileName.StartsWith(HiddenPrefix, StringComparison.Ordinal)
|
||||
|| fileName.StartsWith(NavHiddenPrefix, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
entries.Add(new ProfileEntry(fileName, fileName));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VTank's meta-profile list (<c>ac()</c>,
|
||||
/// <c>uTank2/PluginCore.cs:7187+</c>): seeds
|
||||
/// <see cref="NoneLabel"/>/<see cref="ByCharacterLabel"/>, then every
|
||||
/// non-<c>--</c> file (a meta and a nav profile share the same
|
||||
/// directory and extension here — <c>.af</c> — so callers pass a
|
||||
/// distinguishing sub-extension convention if they need one; VTank
|
||||
/// itself distinguished by the separate <c>.met</c>/<c>.nav</c>
|
||||
/// extensions).
|
||||
/// </summary>
|
||||
public static IReadOnlyList<ProfileEntry> ListMetaProfiles(string directory)
|
||||
{
|
||||
var entries = new List<ProfileEntry>
|
||||
{
|
||||
new(string.Empty, NoneLabel),
|
||||
new(string.Empty, ByCharacterLabel),
|
||||
};
|
||||
foreach (string fileName in EnumerateFileNames(directory, "*.af"))
|
||||
{
|
||||
if (fileName.StartsWith(HiddenPrefix, StringComparison.Ordinal))
|
||||
continue;
|
||||
entries.Add(new ProfileEntry(fileName, fileName));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateFileNames(string directory, string searchPattern)
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
yield break;
|
||||
foreach (string path in Directory.EnumerateFiles(directory, searchPattern)
|
||||
.OrderBy(static path => path, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
yield return Path.GetFileName(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue