Aetheria/Aetheria.State.Replica/Program.cs
2026-06-17 13:51:36 +01:00

120 lines
3.9 KiB
C#

using Aetheria.State;
using GameCult.Aetheria.State.Unity;
return await ProgramMainAsync(args).ConfigureAwait(false);
static async Task<int> ProgramMainAsync(string[] args)
{
if (args.Length == 0)
{
PrintUsage();
return 1;
}
var mode = args[0].Trim();
var options = ParseOptions(args.Skip(1));
var endpoint = RequireOption(options, "endpoint");
var runtimeId = ReadOption(options, "runtime-id", "aetheria-verse-replica");
var replicaPath = ResolveReplicaPath(options);
switch (mode)
{
case "sync":
{
var appliedSequence = await AetheriaVerseReplica
.SyncSnapshotAsync(replicaPath, endpoint, runtimeId)
.ConfigureAwait(false);
Console.WriteLine($"Synced Aetheria replica to {replicaPath} from {endpoint} at shard sequence {appliedSequence}.");
return 0;
}
case "follow":
{
var pollSeconds = ReadDoubleOption(options, "poll-seconds", 1d);
using var cancellation = new CancellationTokenSource();
Console.CancelKeyPress += (_, eventArgs) =>
{
eventArgs.Cancel = true;
cancellation.Cancel();
};
Console.WriteLine(
$"Following Aetheria replica at {replicaPath} from {endpoint} every {pollSeconds:0.###} seconds. Press Ctrl+C to stop.");
await AetheriaVerseReplica
.RunReplicaAsync(replicaPath, endpoint, TimeSpan.FromSeconds(pollSeconds), cancellation.Token, runtimeId)
.ConfigureAwait(false);
return 0;
}
default:
Console.Error.WriteLine($"Unknown mode '{mode}'.");
PrintUsage();
return 1;
}
}
static string ResolveReplicaPath(IReadOnlyDictionary<string, string> options)
{
var explicitPath = ReadOption(options, "replica", "");
if (!string.IsNullOrWhiteSpace(explicitPath))
return Path.GetFullPath(explicitPath);
var gameDataRoot = RequireOption(options, "game-data-root");
var verseId = RequireOption(options, "verse-id");
return AetheriaRuntimeStateBoundary.GetReplicaStateFilePath(
new DirectoryInfo(Path.GetFullPath(gameDataRoot)),
verseId);
}
static Dictionary<string, string> ParseOptions(IEnumerable<string> args)
{
var options = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
string? pendingKey = null;
foreach (var arg in args)
{
if (arg.StartsWith("--", StringComparison.Ordinal))
{
pendingKey = arg[2..];
options[pendingKey] = "true";
continue;
}
if (!string.IsNullOrWhiteSpace(pendingKey))
{
options[pendingKey] = arg;
pendingKey = null;
}
}
return options;
}
static string RequireOption(IReadOnlyDictionary<string, string> options, string key)
{
var value = ReadOption(options, key, "");
if (!string.IsNullOrWhiteSpace(value))
return value;
throw new InvalidOperationException($"Missing required option --{key}.");
}
static string ReadOption(IReadOnlyDictionary<string, string> options, string key, string fallback)
{
return options.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)
? value
: fallback;
}
static double ReadDoubleOption(IReadOnlyDictionary<string, string> options, string key, double fallback)
{
var value = ReadOption(options, key, "");
return double.TryParse(value, out var parsed) && parsed > 0
? parsed
: fallback;
}
static void PrintUsage()
{
Console.WriteLine("Usage:");
Console.WriteLine(" sync --endpoint cultnet://host:3075 --replica path-to-replica.cc");
Console.WriteLine(" sync --endpoint cultnet://host:3075 --game-data-root GameData --verse-id verse.id");
Console.WriteLine(" follow --endpoint cultnet://host:3075 --replica path-to-replica.cc [--poll-seconds 1]");
}