feat(platform): Campaign LA LA0 — extract ApplicationPathSet to AcDream.Platform
The launcher (LA3/LA4) needs the XDG/Windows path contract (ApplicationPathSet/IApplicationPathEnvironment) without pulling in any gameplay assembly. Move it out of AcDream.Runtime into a new BCL-only AcDream.Platform project so the launcher-side Launcher.Core project can reference it directly per the campaign plan (docs/plans/2026-08-14-launcher-campaign.md, LA0). Namespace renamed AcDream.Runtime.Platform -> AcDream.Platform; code is otherwise byte-identical (no logic changes). AcDream.Runtime now carries a ProjectReference to AcDream.Platform and re-exports it transitively, so App and Headless keep resolving the type without a direct reference and K0's Headless single-ProjectReference guard (HeadlessAssemblyReferencesOnlyTheRuntimeProject) stands unchanged. The sibling Runtime dependency-boundary guard (RuntimeProjectDeclaresOnlyApprovedProjectDependencies) does assert Runtime's own project-reference set, so it needed a deliberate, documented addition of AcDream.Platform to its expected list. Moved tests/AcDream.Runtime.Tests/Platform/ApplicationPathSetTests.cs to a new tests/AcDream.Platform.Tests/ project (namespace AcDream.Platform.Tests) referencing only AcDream.Platform. Registered both new projects in AcDream.slnx. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
7f24af1a37
commit
cb6502c8a5
14 changed files with 49 additions and 10 deletions
|
|
@ -19,6 +19,7 @@
|
|||
<ProjectReference Include="..\AcDream.Core\AcDream.Core.csproj" />
|
||||
<ProjectReference Include="..\AcDream.Core.Net\AcDream.Core.Net.csproj" />
|
||||
<ProjectReference Include="..\AcDream.Content\AcDream.Content.csproj" />
|
||||
<ProjectReference Include="..\AcDream.Platform\AcDream.Platform.csproj" />
|
||||
<ProjectReference Include="..\AcDream.Plugin.Abstractions\AcDream.Plugin.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,171 +0,0 @@
|
|||
namespace AcDream.Runtime.Platform;
|
||||
|
||||
/// <summary>
|
||||
/// BCL-only view of the process environment used to resolve portable
|
||||
/// application paths exactly once at a host composition root.
|
||||
/// </summary>
|
||||
public interface IApplicationPathEnvironment
|
||||
{
|
||||
bool IsWindows { get; }
|
||||
|
||||
string CurrentDirectory { get; }
|
||||
|
||||
string? GetEnvironmentVariable(string name);
|
||||
|
||||
string GetFolderPath(Environment.SpecialFolder folder);
|
||||
}
|
||||
|
||||
internal sealed class ApplicationPathEnvironment
|
||||
: IApplicationPathEnvironment
|
||||
{
|
||||
internal static ApplicationPathEnvironment Instance { get; } = new();
|
||||
|
||||
private ApplicationPathEnvironment()
|
||||
{
|
||||
}
|
||||
|
||||
public bool IsWindows => OperatingSystem.IsWindows();
|
||||
|
||||
public string CurrentDirectory => Environment.CurrentDirectory;
|
||||
|
||||
public string? GetEnvironmentVariable(string name) =>
|
||||
Environment.GetEnvironmentVariable(name);
|
||||
|
||||
public string GetFolderPath(Environment.SpecialFolder folder) =>
|
||||
Environment.GetFolderPath(folder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Canonical portable per-user paths shared by graphical and no-window hosts.
|
||||
/// Hosts may override the three roots, but feature code only receives the
|
||||
/// resolved immutable paths and never reads operating-system state itself.
|
||||
/// </summary>
|
||||
public sealed record ApplicationPathSet(
|
||||
string ConfigDirectory,
|
||||
string DataDirectory,
|
||||
string CacheDirectory,
|
||||
string? LegacyConfigDirectory)
|
||||
{
|
||||
public string SettingsFile =>
|
||||
Path.Combine(ConfigDirectory, "settings.json");
|
||||
|
||||
public string KeyBindingsFile =>
|
||||
Path.Combine(ConfigDirectory, "keybinds.json");
|
||||
|
||||
public string LogsDirectory =>
|
||||
Path.Combine(DataDirectory, "logs");
|
||||
|
||||
public string ScreenshotsDirectory =>
|
||||
Path.Combine(DataDirectory, "screenshots");
|
||||
|
||||
public string PluginsDirectory =>
|
||||
Path.Combine(DataDirectory, "plugins");
|
||||
|
||||
public string DiagnosticsDirectory =>
|
||||
Path.Combine(CacheDirectory, "diagnostics");
|
||||
|
||||
public static ApplicationPathSet Resolve(
|
||||
string? configDirectory = null,
|
||||
string? dataDirectory = null,
|
||||
string? cacheDirectory = null,
|
||||
IApplicationPathEnvironment? platform = null)
|
||||
{
|
||||
platform ??= ApplicationPathEnvironment.Instance;
|
||||
|
||||
string config;
|
||||
string data;
|
||||
string cache;
|
||||
if (platform.IsWindows)
|
||||
{
|
||||
string roaming = RequireFolder(
|
||||
platform,
|
||||
Environment.SpecialFolder.ApplicationData);
|
||||
string local = RequireFolder(
|
||||
platform,
|
||||
Environment.SpecialFolder.LocalApplicationData);
|
||||
config = Path.Combine(roaming, "acdream");
|
||||
data = Path.Combine(local, "acdream");
|
||||
cache = Path.Combine(local, "acdream", "cache");
|
||||
}
|
||||
else
|
||||
{
|
||||
string home = RequireFolder(
|
||||
platform,
|
||||
Environment.SpecialFolder.UserProfile);
|
||||
config = ResolveXdg(
|
||||
platform,
|
||||
"XDG_CONFIG_HOME",
|
||||
Path.Combine(home, ".config"),
|
||||
"acdream");
|
||||
data = ResolveXdg(
|
||||
platform,
|
||||
"XDG_DATA_HOME",
|
||||
Path.Combine(home, ".local", "share"),
|
||||
"acdream");
|
||||
cache = ResolveXdg(
|
||||
platform,
|
||||
"XDG_CACHE_HOME",
|
||||
Path.Combine(home, ".cache"),
|
||||
"acdream");
|
||||
}
|
||||
|
||||
string? legacyConfigDirectory =
|
||||
platform.IsWindows && configDirectory is null
|
||||
? Path.Combine(
|
||||
RequireFolder(
|
||||
platform,
|
||||
Environment.SpecialFolder.LocalApplicationData),
|
||||
"acdream")
|
||||
: null;
|
||||
|
||||
return new ApplicationPathSet(
|
||||
Normalize(configDirectory ?? config, platform),
|
||||
Normalize(dataDirectory ?? data, platform),
|
||||
Normalize(cacheDirectory ?? cache, platform),
|
||||
legacyConfigDirectory is null
|
||||
? null
|
||||
: Normalize(legacyConfigDirectory, platform));
|
||||
}
|
||||
|
||||
private static string ResolveXdg(
|
||||
IApplicationPathEnvironment platform,
|
||||
string variable,
|
||||
string fallback,
|
||||
string leaf)
|
||||
{
|
||||
string? configured = platform.GetEnvironmentVariable(variable);
|
||||
string root = string.IsNullOrWhiteSpace(configured)
|
||||
? fallback
|
||||
: configured;
|
||||
return Path.Combine(root, leaf);
|
||||
}
|
||||
|
||||
private static string RequireFolder(
|
||||
IApplicationPathEnvironment platform,
|
||||
Environment.SpecialFolder folder)
|
||||
{
|
||||
string value = platform.GetFolderPath(folder);
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The operating system did not provide {folder}.");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static string Normalize(
|
||||
string path,
|
||||
IApplicationPathEnvironment platform)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Application directories cannot be empty.",
|
||||
nameof(path));
|
||||
}
|
||||
|
||||
return Path.TrimEndingDirectorySeparator(
|
||||
Path.GetFullPath(path, platform.CurrentDirectory));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue