2026-06-13 01:59:17 +01:00
|
|
|
using Aetheria.State;
|
|
|
|
|
using Aetheria.State.Documents;
|
2020-02-26 17:48:40 +00:00
|
|
|
|
2026-06-13 01:59:17 +01:00
|
|
|
namespace Aetheria.Server;
|
|
|
|
|
|
|
|
|
|
internal static class Program
|
2020-02-26 17:48:40 +00:00
|
|
|
{
|
2026-06-13 15:54:02 +01:00
|
|
|
private const string RuntimeId = "aetheria-economy-server";
|
|
|
|
|
|
2026-06-13 01:59:17 +01:00
|
|
|
private static async Task Main(string[] args)
|
2020-02-26 17:48:40 +00:00
|
|
|
{
|
2026-06-13 01:59:17 +01:00
|
|
|
var statePath = ResolveStatePath(args);
|
|
|
|
|
Console.WriteLine($"Aetheria CultMesh state host starting: {statePath}");
|
2020-04-09 07:48:45 +01:00
|
|
|
|
2026-06-13 01:59:17 +01:00
|
|
|
await using var node = await AetheriaStateNode.OpenAsync(
|
|
|
|
|
statePath,
|
2026-06-13 15:54:02 +01:00
|
|
|
runtimeId: RuntimeId,
|
2026-06-13 01:59:17 +01:00
|
|
|
startServer: true).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
await EnsureWorldDocumentAsync(node).ConfigureAwait(false);
|
2026-06-13 15:47:57 +01:00
|
|
|
await ApplyPendingRuntimeCommitsAsync(node).ConfigureAwait(false);
|
2026-06-13 17:02:52 +01:00
|
|
|
await ApplyPendingEveCommandsAsync(node).ConfigureAwait(false);
|
2026-06-13 01:59:17 +01:00
|
|
|
await node.FlushAsync().ConfigureAwait(false);
|
2020-03-27 01:53:03 +00:00
|
|
|
|
2026-06-13 15:47:57 +01:00
|
|
|
if (HasFlag(args, "--apply-pending-once"))
|
|
|
|
|
{
|
2026-06-13 17:02:52 +01:00
|
|
|
Console.WriteLine("Aetheria pending runtime and Eve command drains completed.");
|
2026-06-13 15:47:57 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 01:59:17 +01:00
|
|
|
Console.WriteLine("Aetheria CultMesh state host is running. Press Ctrl+C to stop.");
|
2026-06-13 15:47:57 +01:00
|
|
|
await RunUntilShutdownAsync(node, PendingInterval(args)).ConfigureAwait(false);
|
2020-04-09 07:48:45 +01:00
|
|
|
|
2026-06-13 01:59:17 +01:00
|
|
|
Console.WriteLine("Aetheria CultMesh state host stopping.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string ResolveStatePath(IReadOnlyList<string> args)
|
|
|
|
|
{
|
|
|
|
|
for (var i = 0; i < args.Count - 1; i++)
|
|
|
|
|
{
|
|
|
|
|
if (string.Equals(args[i], "--state", StringComparison.OrdinalIgnoreCase))
|
2020-03-27 01:53:03 +00:00
|
|
|
{
|
2026-06-13 01:59:17 +01:00
|
|
|
return Path.GetFullPath(args[i + 1]);
|
2020-03-27 01:53:03 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 01:59:17 +01:00
|
|
|
var repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", ".."));
|
|
|
|
|
return AetheriaStatePaths.ResolveDefaultStatePath(repoRoot);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task EnsureWorldDocumentAsync(AetheriaStateNode node)
|
|
|
|
|
{
|
|
|
|
|
var existing = await node.GetWorldAsync().ConfigureAwait(false);
|
|
|
|
|
if (existing != null)
|
2020-03-27 01:53:03 +00:00
|
|
|
{
|
2026-06-13 01:59:17 +01:00
|
|
|
existing.UpdatedAtUtc = DateTimeOffset.UtcNow.ToString("O");
|
|
|
|
|
await node.PutWorldAsync(existing).ConfigureAwait(false);
|
|
|
|
|
return;
|
2020-03-27 01:53:03 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-13 01:59:17 +01:00
|
|
|
var now = DateTimeOffset.UtcNow.ToString("O");
|
|
|
|
|
await node.PutWorldAsync(new AetheriaWorldState
|
2020-03-27 01:53:03 +00:00
|
|
|
{
|
2026-06-13 01:59:17 +01:00
|
|
|
Name = "Aetheria",
|
|
|
|
|
WorldId = "aetheria",
|
|
|
|
|
SchemaEpoch = 1,
|
|
|
|
|
CreatedAtUtc = now,
|
|
|
|
|
UpdatedAtUtc = now
|
|
|
|
|
}).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 15:47:57 +01:00
|
|
|
private static async Task ApplyPendingRuntimeCommitsAsync(AetheriaStateNode node)
|
|
|
|
|
{
|
2026-06-13 15:54:02 +01:00
|
|
|
var pendingBefore = CountPendingRuntimeCommits(node.StatePath);
|
|
|
|
|
var now = DateTimeOffset.UtcNow.ToString("O");
|
|
|
|
|
|
|
|
|
|
try
|
2026-06-13 15:47:57 +01:00
|
|
|
{
|
2026-06-13 15:54:02 +01:00
|
|
|
var report = await AetheriaRuntimeCommitLogApplier.ApplyPendingAsync(node).ConfigureAwait(false);
|
|
|
|
|
var status = new AetheriaRuntimeCommitDrainStatus
|
|
|
|
|
{
|
|
|
|
|
RuntimeId = RuntimeId,
|
|
|
|
|
StatePath = node.StatePath,
|
|
|
|
|
LastPollAtUtc = now,
|
|
|
|
|
LastAppliedAtUtc = report.AppliedPaths.Length > 0 ? now : "",
|
|
|
|
|
PendingBeforeApply = pendingBefore,
|
|
|
|
|
CommandsApplied = report.AppliedPaths.Length,
|
|
|
|
|
AppliedPlayerSettings = report.AppliedPlayerSettings,
|
|
|
|
|
AppliedLoadoutTemplates = report.AppliedLoadoutTemplates,
|
|
|
|
|
AppliedRunCheckpoints = report.AppliedRunCheckpoints,
|
|
|
|
|
ConsecutiveFailures = 0,
|
|
|
|
|
Status = "ok"
|
|
|
|
|
};
|
|
|
|
|
await PublishDrainStatusAsync(node, status).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
if (report.AppliedPaths.Length == 0)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine("No pending Aetheria runtime commits.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Console.WriteLine(
|
|
|
|
|
"Applied pending Aetheria runtime commits: " +
|
|
|
|
|
$"settings={report.AppliedPlayerSettings}, " +
|
|
|
|
|
$"loadouts={report.AppliedLoadoutTemplates}, " +
|
|
|
|
|
$"runs={report.AppliedRunCheckpoints}, " +
|
|
|
|
|
$"files={report.AppliedPaths.Length}");
|
2026-06-13 15:47:57 +01:00
|
|
|
}
|
2026-06-13 15:54:02 +01:00
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
var existing = await node.GetRuntimeCommitDrainStatusAsync().ConfigureAwait(false);
|
|
|
|
|
var status = new AetheriaRuntimeCommitDrainStatus
|
|
|
|
|
{
|
|
|
|
|
RuntimeId = RuntimeId,
|
|
|
|
|
StatePath = node.StatePath,
|
|
|
|
|
LastPollAtUtc = now,
|
|
|
|
|
LastAppliedAtUtc = existing?.LastAppliedAtUtc ?? "",
|
|
|
|
|
PendingBeforeApply = pendingBefore,
|
|
|
|
|
ConsecutiveFailures = (existing?.ConsecutiveFailures ?? 0) + 1,
|
|
|
|
|
LastError = ex.ToString(),
|
|
|
|
|
Status = "error"
|
|
|
|
|
};
|
|
|
|
|
await PublishDrainStatusAsync(node, status).ConfigureAwait(false);
|
|
|
|
|
throw;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-13 15:47:57 +01:00
|
|
|
|
2026-06-13 15:54:02 +01:00
|
|
|
private static async Task PublishDrainStatusAsync(
|
|
|
|
|
AetheriaStateNode node,
|
|
|
|
|
AetheriaRuntimeCommitDrainStatus status)
|
|
|
|
|
{
|
|
|
|
|
await node.PutRuntimeCommitDrainStatusAsync(status).ConfigureAwait(false);
|
2026-06-13 17:02:52 +01:00
|
|
|
await PublishOperationsStateAsync(node, status.LastPollAtUtc).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task ApplyPendingEveCommandsAsync(AetheriaStateNode node)
|
|
|
|
|
{
|
|
|
|
|
var pendingBefore = CountPendingEveCommands(node.StatePath);
|
|
|
|
|
var now = DateTimeOffset.UtcNow.ToString("O");
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var report = await AetheriaEveCommandBridge.ApplyPendingAsync(node).ConfigureAwait(false);
|
|
|
|
|
var status = new AetheriaEveCommandDrainStatus
|
|
|
|
|
{
|
|
|
|
|
RuntimeId = RuntimeId,
|
|
|
|
|
StatePath = node.StatePath,
|
|
|
|
|
LastPollAtUtc = now,
|
|
|
|
|
LastAcceptedAtUtc = report.AcceptedPaths.Length > 0 ? now : "",
|
|
|
|
|
PendingBeforeApply = pendingBefore,
|
|
|
|
|
CommandsAccepted = report.AcceptedPaths.Length,
|
|
|
|
|
CommandsRejected = report.RejectedCommands,
|
|
|
|
|
AppliedCatalogRefreshes = report.AppliedCatalogRefreshes,
|
|
|
|
|
AppliedOperationsRefreshes = report.AppliedOperationsRefreshes,
|
|
|
|
|
LastRejectedCommand = report.LastRejectedCommand,
|
|
|
|
|
LastRejectedReason = report.LastRejectedReason,
|
|
|
|
|
ConsecutiveFailures = 0,
|
|
|
|
|
Status = report.RejectedCommands > 0 ? "rejected" : "ok"
|
|
|
|
|
};
|
|
|
|
|
await PublishEveCommandStatusAsync(node, status).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
if (report.AcceptedPaths.Length == 0 && report.RejectedCommands == 0)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine("No pending Aetheria Eve commands.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Console.WriteLine(
|
|
|
|
|
"Drained pending Aetheria Eve commands: " +
|
|
|
|
|
$"accepted={report.AcceptedPaths.Length}, " +
|
|
|
|
|
$"rejected={report.RejectedCommands}, " +
|
|
|
|
|
$"catalogRefreshes={report.AppliedCatalogRefreshes}, " +
|
|
|
|
|
$"operationsRefreshes={report.AppliedOperationsRefreshes}");
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
var existing = await node.GetEveCommandDrainStatusAsync().ConfigureAwait(false);
|
|
|
|
|
var status = new AetheriaEveCommandDrainStatus
|
|
|
|
|
{
|
|
|
|
|
RuntimeId = RuntimeId,
|
|
|
|
|
StatePath = node.StatePath,
|
|
|
|
|
LastPollAtUtc = now,
|
|
|
|
|
LastAcceptedAtUtc = existing?.LastAcceptedAtUtc ?? "",
|
|
|
|
|
PendingBeforeApply = pendingBefore,
|
|
|
|
|
ConsecutiveFailures = (existing?.ConsecutiveFailures ?? 0) + 1,
|
|
|
|
|
LastError = ex.ToString(),
|
|
|
|
|
Status = "error"
|
|
|
|
|
};
|
|
|
|
|
await PublishEveCommandStatusAsync(node, status).ConfigureAwait(false);
|
|
|
|
|
throw;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task PublishEveCommandStatusAsync(
|
|
|
|
|
AetheriaStateNode node,
|
|
|
|
|
AetheriaEveCommandDrainStatus status)
|
|
|
|
|
{
|
|
|
|
|
await node.PutEveCommandDrainStatusAsync(status).ConfigureAwait(false);
|
|
|
|
|
await PublishOperationsStateAsync(node, status.LastPollAtUtc).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task PublishOperationsStateAsync(
|
|
|
|
|
AetheriaStateNode node,
|
|
|
|
|
string updatedAtUtc)
|
|
|
|
|
{
|
|
|
|
|
var commitStatus = await node.GetRuntimeCommitDrainStatusAsync().ConfigureAwait(false) ??
|
|
|
|
|
new AetheriaRuntimeCommitDrainStatus
|
|
|
|
|
{
|
|
|
|
|
RuntimeId = RuntimeId,
|
|
|
|
|
StatePath = node.StatePath,
|
|
|
|
|
LastPollAtUtc = updatedAtUtc,
|
|
|
|
|
Status = "idle"
|
|
|
|
|
};
|
|
|
|
|
var eveStatus = await node.GetEveCommandDrainStatusAsync().ConfigureAwait(false);
|
|
|
|
|
await node.PutOperationsSurfaceAsync(AetheriaOperationsSurfaceProjector.Build(commitStatus, eveStatus))
|
|
|
|
|
.ConfigureAwait(false);
|
2026-06-13 15:59:26 +01:00
|
|
|
await node.PutProviderAdvertisementAsync(
|
2026-06-13 17:02:52 +01:00
|
|
|
AetheriaProviderAdvertisementProjector.Build(node.StatePath, updatedAtUtc)).ConfigureAwait(false);
|
2026-06-13 15:54:02 +01:00
|
|
|
await node.FlushAsync().ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static int CountPendingRuntimeCommits(string statePath)
|
|
|
|
|
{
|
|
|
|
|
var pendingDirectory = statePath + ".pending";
|
|
|
|
|
return Directory.Exists(pendingDirectory)
|
|
|
|
|
? Directory.EnumerateFiles(pendingDirectory, "*.cc").Count()
|
|
|
|
|
: 0;
|
2026-06-13 15:47:57 +01:00
|
|
|
}
|
|
|
|
|
|
2026-06-13 17:02:52 +01:00
|
|
|
private static int CountPendingEveCommands(string statePath)
|
|
|
|
|
{
|
|
|
|
|
var pendingDirectory = AetheriaEveCommandBridge.GetPendingDirectory(statePath);
|
|
|
|
|
return Directory.Exists(pendingDirectory)
|
|
|
|
|
? Directory.EnumerateFiles(pendingDirectory, "*.cc").Count()
|
|
|
|
|
: 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 15:47:57 +01:00
|
|
|
private static async Task RunUntilShutdownAsync(AetheriaStateNode node, TimeSpan pendingInterval)
|
2026-06-13 01:59:17 +01:00
|
|
|
{
|
|
|
|
|
var stopped = new TaskCompletionSource<object?>();
|
|
|
|
|
Console.CancelKeyPress += (_, eventArgs) =>
|
|
|
|
|
{
|
|
|
|
|
eventArgs.Cancel = true;
|
|
|
|
|
stopped.TrySetResult(null);
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-13 15:47:57 +01:00
|
|
|
while (!stopped.Task.IsCompleted)
|
|
|
|
|
{
|
|
|
|
|
var delay = Task.Delay(pendingInterval);
|
|
|
|
|
var completed = await Task.WhenAny(stopped.Task, delay).ConfigureAwait(false);
|
|
|
|
|
if (completed == stopped.Task)
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
await ApplyPendingRuntimeCommitsAsync(node).ConfigureAwait(false);
|
2026-06-13 17:02:52 +01:00
|
|
|
await ApplyPendingEveCommandsAsync(node).ConfigureAwait(false);
|
2026-06-13 15:47:57 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static TimeSpan PendingInterval(IReadOnlyList<string> args)
|
|
|
|
|
{
|
|
|
|
|
for (var i = 0; i < args.Count - 1; i++)
|
|
|
|
|
{
|
|
|
|
|
if (string.Equals(args[i], "--pending-interval-ms", StringComparison.OrdinalIgnoreCase) &&
|
|
|
|
|
int.TryParse(args[i + 1], out var milliseconds) &&
|
|
|
|
|
milliseconds > 0)
|
|
|
|
|
{
|
|
|
|
|
return TimeSpan.FromMilliseconds(milliseconds);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return TimeSpan.FromSeconds(5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool HasFlag(IReadOnlyList<string> args, string flag)
|
|
|
|
|
{
|
|
|
|
|
return args.Any(arg => string.Equals(arg, flag, StringComparison.OrdinalIgnoreCase));
|
2020-02-26 17:48:40 +00:00
|
|
|
}
|
|
|
|
|
}
|