Serve Verse discovery from daemon host settings
This commit is contained in:
parent
87a8ac5eea
commit
c1267795bf
6 changed files with 188 additions and 4 deletions
|
|
@ -537,7 +537,7 @@ Console.WriteLine("Generic popup inspector shell: PropertiesPanel, PropertiesLis
|
|||
Console.WriteLine("Typed behavior metadata authority: live heat/mining/thermotoggle payload kinds stay owned by package metadata");
|
||||
Console.WriteLine("NameTools editor shell: the remaining name helper window lowers through UI Toolkit instead of IMGUI");
|
||||
Console.WriteLine("Runtime state reader authority: Unity gameplay/UI read typed state through a shared runtime reader instead of direct store spelunking");
|
||||
Console.WriteLine("Verse host authority: daemon-owned typed verse host settings now drive provider advertisement and operations telemetry");
|
||||
Console.WriteLine("Verse host authority: daemon-owned typed verse host settings now drive provider advertisement, operations telemetry, and served Verse discovery");
|
||||
Console.WriteLine("Client target boot authority: Unity boot resolves the active Verse through a typed client target instead of local path folklore");
|
||||
Console.WriteLine("Verse settings shell: client target edits and Verse-host visibility commands lower through typed Eve surfaces");
|
||||
Console.WriteLine("Main-menu Verse projection: the Unity Eve shell lowers daemon-owned verse identity instead of ad-libbing local menu copy");
|
||||
|
|
@ -3665,6 +3665,8 @@ static void RequireVerseHostSettingsAuthority(string root)
|
|||
{
|
||||
var settingsPath = Path.Combine(root, "Aetheria.State", "Documents", "AetheriaVerseHostSettings.cs");
|
||||
var normalizerPath = Path.Combine(root, "Aetheria.State", "AetheriaVerseHostSettingsNormalizer.cs");
|
||||
var verseCatalogProjectorPath = Path.Combine(root, "Aetheria.State", "AetheriaVerseCatalogProjector.cs");
|
||||
var verseDiscoveryHostPath = Path.Combine(root, "Aetheria.State", "AetheriaVerseDiscoveryHost.cs");
|
||||
var registryPath = Path.Combine(root, "Aetheria.State", "AetheriaDocumentRegistry.cs");
|
||||
var nodePath = Path.Combine(root, "Aetheria.State", "AetheriaStateNode.cs");
|
||||
var providerPath = Path.Combine(root, "Aetheria.State", "AetheriaProviderAdvertisementProjector.cs");
|
||||
|
|
@ -3677,6 +3679,8 @@ static void RequireVerseHostSettingsAuthority(string root)
|
|||
{
|
||||
settingsPath,
|
||||
normalizerPath,
|
||||
verseCatalogProjectorPath,
|
||||
verseDiscoveryHostPath,
|
||||
registryPath,
|
||||
nodePath,
|
||||
providerPath,
|
||||
|
|
@ -3698,6 +3702,8 @@ static void RequireVerseHostSettingsAuthority(string root)
|
|||
|
||||
var settings = File.ReadAllText(settingsPath);
|
||||
var normalizer = File.ReadAllText(normalizerPath);
|
||||
var verseCatalogProjector = File.ReadAllText(verseCatalogProjectorPath);
|
||||
var verseDiscoveryHost = File.ReadAllText(verseDiscoveryHostPath);
|
||||
var registry = File.ReadAllText(registryPath);
|
||||
var node = File.ReadAllText(nodePath);
|
||||
var provider = File.ReadAllText(providerPath);
|
||||
|
|
@ -3731,6 +3737,42 @@ static void RequireVerseHostSettingsAuthority(string root)
|
|||
"Verse-host settings normalizer is missing normalize/equivalence ownership.");
|
||||
}
|
||||
|
||||
var requiredVerseCatalogProjectorSymbols = new[]
|
||||
{
|
||||
"public static class AetheriaVerseCatalogProjector",
|
||||
"CultMeshVerseDescriptor",
|
||||
"BuildDiscoveryEndpoint",
|
||||
"CultMeshVerseDescriptor.ComputeRulesHash",
|
||||
"cultnet://"
|
||||
};
|
||||
var missingVerseCatalogProjectorSymbols = requiredVerseCatalogProjectorSymbols
|
||||
.Where(symbol => !verseCatalogProjector.Contains(symbol, StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
if (missingVerseCatalogProjectorSymbols.Length > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Verse catalog projector no longer derives public CultMesh discovery from typed host settings: " +
|
||||
string.Join(", ", missingVerseCatalogProjectorSymbols));
|
||||
}
|
||||
|
||||
var requiredVerseDiscoveryHostSymbols = new[]
|
||||
{
|
||||
"public sealed class AetheriaVerseDiscoveryHost",
|
||||
"AetheriaVerseCatalogProjector.Build(normalized)",
|
||||
"CultMesh.CreateVerseCatalog()",
|
||||
"CultMesh.ServeVerseCatalog(_node.MeshNode, _catalog)",
|
||||
"normalized.Visibility"
|
||||
};
|
||||
var missingVerseDiscoveryHostSymbols = requiredVerseDiscoveryHostSymbols
|
||||
.Where(symbol => !verseDiscoveryHost.Contains(symbol, StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
if (missingVerseDiscoveryHostSymbols.Length > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Verse discovery host no longer gates served Verse catalogs through the typed host settings: " +
|
||||
string.Join(", ", missingVerseDiscoveryHostSymbols));
|
||||
}
|
||||
|
||||
if (!registry.Contains("CultNetDocumentBinding.ForDocument<AetheriaVerseHostSettings>", StringComparison.Ordinal) ||
|
||||
!registry.Contains("typeof(AetheriaVerseHostSettings)", StringComparison.Ordinal))
|
||||
{
|
||||
|
|
@ -3740,6 +3782,7 @@ static void RequireVerseHostSettingsAuthority(string root)
|
|||
|
||||
if (!node.Contains("PutVerseHostSettingsAsync", StringComparison.Ordinal) ||
|
||||
!node.Contains("GetVerseHostSettingsAsync", StringComparison.Ordinal) ||
|
||||
!node.Contains("public CultMeshNode MeshNode => _node;", StringComparison.Ordinal) ||
|
||||
!node.Contains("global:aetheria.verse_host_settings.v1", StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
|
|
@ -3783,6 +3826,8 @@ static void RequireVerseHostSettingsAuthority(string root)
|
|||
var requiredServerSymbols = new[]
|
||||
{
|
||||
"EnsureVerseHostSettingsAsync",
|
||||
"AetheriaVerseDiscoveryHost",
|
||||
"RefreshVerseDiscoveryAsync",
|
||||
"LoadVerseHostOverrides",
|
||||
"GetVerseHostSettingsAsync",
|
||||
"PutVerseHostSettingsAsync",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
|
|||
|
||||
public string StatePath { get; }
|
||||
|
||||
public CultMeshNode MeshNode => _node;
|
||||
|
||||
public CultCache Cache => _node.Cache;
|
||||
|
||||
public CultNetDatabase Database => _node.Database;
|
||||
|
|
|
|||
54
Aetheria.State/AetheriaVerseCatalogProjector.cs
Normal file
54
Aetheria.State/AetheriaVerseCatalogProjector.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using Aetheria.State.Documents;
|
||||
using GameCult.Mesh;
|
||||
|
||||
namespace Aetheria.State;
|
||||
|
||||
public static class AetheriaVerseCatalogProjector
|
||||
{
|
||||
private const string TransportVersion = "cultmesh.v0";
|
||||
private const string RulesEpoch = "aetheria-runtime-state.v1";
|
||||
|
||||
public static CultMeshVerseDescriptor Build(AetheriaVerseHostSettings? settings)
|
||||
{
|
||||
var normalized = AetheriaVerseHostSettingsNormalizer.Normalize(settings);
|
||||
var discoveryEndpoint = BuildDiscoveryEndpoint(normalized);
|
||||
var rulesHash = CultMeshVerseDescriptor.ComputeRulesHash(
|
||||
"aetheria",
|
||||
RulesEpoch,
|
||||
normalized.RootVerse,
|
||||
normalized.CanonicalService);
|
||||
|
||||
return new CultMeshVerseDescriptor(
|
||||
normalized.VerseId,
|
||||
normalized.Title,
|
||||
CultMeshVerseAuthorityModel.OperatorCluster,
|
||||
new CultMeshVerseCompatibility(TransportVersion, rulesHash),
|
||||
discoveryEndpoints: new[] { discoveryEndpoint },
|
||||
authorityRuntimeIds: new[] { normalized.ServiceId },
|
||||
parentVerseId: normalized.RootVerse,
|
||||
description: BuildDescription(normalized));
|
||||
}
|
||||
|
||||
public static string BuildDiscoveryEndpoint(AetheriaVerseHostSettings? settings)
|
||||
{
|
||||
var normalized = AetheriaVerseHostSettingsNormalizer.Normalize(settings);
|
||||
var host = normalized.LocatedService;
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
host = normalized.CanonicalService;
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
host = "localhost";
|
||||
|
||||
host = host.Trim();
|
||||
var slashIndex = host.IndexOf('/', StringComparison.Ordinal);
|
||||
if (slashIndex >= 0)
|
||||
host = host.Substring(0, slashIndex);
|
||||
|
||||
return $"cultnet://{host}:3075";
|
||||
}
|
||||
|
||||
private static string BuildDescription(AetheriaVerseHostSettings settings)
|
||||
{
|
||||
var visibility = string.IsNullOrWhiteSpace(settings.Visibility) ? "private" : settings.Visibility;
|
||||
return $"{settings.Title} Verse host ({visibility})";
|
||||
}
|
||||
}
|
||||
67
Aetheria.State/AetheriaVerseDiscoveryHost.cs
Normal file
67
Aetheria.State/AetheriaVerseDiscoveryHost.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using Aetheria.State.Documents;
|
||||
using GameCult.Mesh;
|
||||
|
||||
namespace Aetheria.State;
|
||||
|
||||
public sealed class AetheriaVerseDiscoveryHost : IDisposable
|
||||
{
|
||||
private readonly AetheriaStateNode _node;
|
||||
private CultMeshVerseCatalog? _catalog;
|
||||
private CultMeshVerseDiscoveryServer? _server;
|
||||
private string _fingerprint = "";
|
||||
|
||||
public AetheriaVerseDiscoveryHost(AetheriaStateNode node)
|
||||
{
|
||||
_node = node ?? throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
public void Update(AetheriaVerseHostSettings? settings)
|
||||
{
|
||||
var normalized = AetheriaVerseHostSettingsNormalizer.Normalize(settings);
|
||||
if (!string.Equals(normalized.Visibility, "public", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
DisposeServer();
|
||||
return;
|
||||
}
|
||||
|
||||
var nextFingerprint = CreateFingerprint(normalized);
|
||||
if (string.Equals(_fingerprint, nextFingerprint, StringComparison.Ordinal))
|
||||
return;
|
||||
|
||||
DisposeServer();
|
||||
|
||||
_catalog = CultMesh.CreateVerseCatalog();
|
||||
_catalog.Upsert(AetheriaVerseCatalogProjector.Build(normalized));
|
||||
_server = CultMesh.ServeVerseCatalog(_node.MeshNode, _catalog);
|
||||
_fingerprint = nextFingerprint;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeServer();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void DisposeServer()
|
||||
{
|
||||
_server?.Dispose();
|
||||
_server = null;
|
||||
_catalog?.Dispose();
|
||||
_catalog = null;
|
||||
_fingerprint = "";
|
||||
}
|
||||
|
||||
private static string CreateFingerprint(AetheriaVerseHostSettings settings)
|
||||
{
|
||||
return string.Join(
|
||||
"\u001f",
|
||||
settings.ServiceId ?? "",
|
||||
settings.VerseId ?? "",
|
||||
settings.RootVerse ?? "",
|
||||
settings.CanonicalService ?? "",
|
||||
settings.LocatedService ?? "",
|
||||
settings.CultMeshAddress ?? "",
|
||||
settings.Title ?? "",
|
||||
settings.Visibility ?? "");
|
||||
}
|
||||
}
|
||||
|
|
@ -402,8 +402,11 @@ depth, accepted/applied/rejected counts, failures, timestamps, and player
|
|||
settings controls are typed state rather than console-only status. It also
|
||||
persists daemon-owned `aetheria.verse_host_settings.v1`, with boot-time
|
||||
defaults or overrides for service id, Verse id, visibility, and CultMesh
|
||||
address. The operations surface and provider advertisement project from that
|
||||
typed host document instead of hardcoded daemon identity strings. The provider
|
||||
address. When visibility is `public`, the long-running daemon also serves a
|
||||
derived CultMesh Verse catalog from that same typed host document, using the
|
||||
located service name as the discoverable `cultnet://...:3075` endpoint. The
|
||||
operations surface and provider advertisement project from that typed host
|
||||
document instead of hardcoded daemon identity strings. The provider
|
||||
advertisement still publishes `gamecult.eve.provider_advertisement.v1` for the
|
||||
`aetheria` provider, advertising the catalog, operations, and player-settings
|
||||
surfaces plus the typed schemas witnessed by the local `.cc` state file.
|
||||
|
|
|
|||
|
|
@ -17,13 +17,16 @@ internal static class Program
|
|||
statePath,
|
||||
runtimeId: RuntimeId,
|
||||
startServer: true).ConfigureAwait(false);
|
||||
using var verseDiscovery = new AetheriaVerseDiscoveryHost(node);
|
||||
|
||||
await EnsureVerseHostSettingsAsync(node).ConfigureAwait(false);
|
||||
await RefreshVerseDiscoveryAsync(node, verseDiscovery).ConfigureAwait(false);
|
||||
var startedAtUtc = DateTimeOffset.UtcNow.ToString("O");
|
||||
await PublishRuntimeSessionAsync(node, startedAtUtc, "starting").ConfigureAwait(false);
|
||||
await EnsureWorldDocumentAsync(node).ConfigureAwait(false);
|
||||
await ApplyPendingRuntimeCommitsAsync(node).ConfigureAwait(false);
|
||||
await ApplyPendingEveCommandsAsync(node).ConfigureAwait(false);
|
||||
await RefreshVerseDiscoveryAsync(node, verseDiscovery).ConfigureAwait(false);
|
||||
await PublishRuntimeSessionAsync(node, startedAtUtc, "running").ConfigureAwait(false);
|
||||
await node.FlushAsync().ConfigureAwait(false);
|
||||
|
||||
|
|
@ -34,7 +37,7 @@ internal static class Program
|
|||
}
|
||||
|
||||
Console.WriteLine("Aetheria CultMesh state host is running. Press Ctrl+C to stop.");
|
||||
await RunUntilShutdownAsync(node, startedAtUtc, PendingInterval(args)).ConfigureAwait(false);
|
||||
await RunUntilShutdownAsync(node, verseDiscovery, startedAtUtc, PendingInterval(args)).ConfigureAwait(false);
|
||||
|
||||
await PublishRuntimeSessionAsync(node, startedAtUtc, "stopping").ConfigureAwait(false);
|
||||
Console.WriteLine("Aetheria CultMesh state host stopping.");
|
||||
|
|
@ -286,6 +289,7 @@ internal static class Program
|
|||
|
||||
private static async Task RunUntilShutdownAsync(
|
||||
AetheriaStateNode node,
|
||||
AetheriaVerseDiscoveryHost verseDiscovery,
|
||||
string startedAtUtc,
|
||||
TimeSpan pendingInterval)
|
||||
{
|
||||
|
|
@ -305,10 +309,19 @@ internal static class Program
|
|||
|
||||
await ApplyPendingRuntimeCommitsAsync(node).ConfigureAwait(false);
|
||||
await ApplyPendingEveCommandsAsync(node).ConfigureAwait(false);
|
||||
await RefreshVerseDiscoveryAsync(node, verseDiscovery).ConfigureAwait(false);
|
||||
await PublishRuntimeSessionAsync(node, startedAtUtc, "running").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task RefreshVerseDiscoveryAsync(
|
||||
AetheriaStateNode node,
|
||||
AetheriaVerseDiscoveryHost verseDiscovery)
|
||||
{
|
||||
var settings = await node.GetVerseHostSettingsAsync().ConfigureAwait(false);
|
||||
verseDiscovery.Update(settings);
|
||||
}
|
||||
|
||||
private static TimeSpan PendingInterval(IReadOnlyList<string> args)
|
||||
{
|
||||
for (var i = 0; i < args.Count - 1; i++)
|
||||
|
|
|
|||
Loading…
Reference in a new issue