Advertise Aetheria Eve provider surfaces

This commit is contained in:
Meta Vens 2026-06-13 15:59:26 +01:00
commit 42500ea45c
9 changed files with 237 additions and 3 deletions

Binary file not shown.

View file

@ -103,6 +103,7 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s
};
await node.PutRuntimeCommitDrainStatusAsync(drainStatus);
await node.PutOperationsSurfaceAsync(AetheriaOperationsSurfaceProjector.Build(drainStatus));
await node.PutProviderAdvertisementAsync(AetheriaProviderAdvertisementProjector.Build(statePath, now));
await node.PutLoadoutTemplateAsync(loadoutKey, new AetheriaLoadoutTemplate
{
@ -256,6 +257,7 @@ await using (var reopened = await AetheriaStateNode.OpenAsync(statePath, "aether
var catalogSurface = await reopened.GetCatalogSurfaceAsync();
var drainStatus = await reopened.GetRuntimeCommitDrainStatusAsync();
var operationsSurface = await reopened.GetOperationsSurfaceAsync();
var advertisement = await reopened.GetProviderAdvertisementAsync();
var playerSettings = await reopened.GetPlayerSettingsAsync();
var loadout = await reopened.GetLoadoutTemplateAsync(loadoutKey);
var runState = await reopened.GetRunStateAsync(runKey);
@ -300,6 +302,14 @@ await using (var reopened = await AetheriaStateNode.OpenAsync(statePath, "aether
throw new InvalidOperationException("Runtime commit drain status or operations surface did not survive flush/reopen.");
}
if (advertisement?.ProviderId != AetheriaProviderAdvertisementProjector.ProviderId ||
advertisement.Surfaces.Length < 2 ||
!advertisement.Surfaces.Any(surface => surface.SurfaceId == AetheriaCatalogSurfaceProjector.SurfaceId) ||
!advertisement.Surfaces.Any(surface => surface.SurfaceId == AetheriaOperationsSurfaceProjector.SurfaceId))
{
throw new InvalidOperationException("Aetheria Eve provider advertisement did not survive flush/reopen.");
}
if (playerSettings?.ActiveRunKey != runKey.ToString() ||
playerSettings.PlayerName != "Smoke Pilot" ||
!playerSettings.TutorialPassed ||

View file

@ -38,7 +38,8 @@ public static class AetheriaDocumentRegistry
CultNetDocumentBinding.ForDocument<AetheriaZoneState>(registry),
CultNetDocumentBinding.ForDocument<AetheriaEntitySnapshot>(registry),
CultNetDocumentBinding.ForDocument<AetheriaRuntimeCommitDrainStatus>(registry),
CultNetDocumentBinding.ForDocument<EveSurfaceState>(registry)
CultNetDocumentBinding.ForDocument<EveSurfaceState>(registry),
CultNetDocumentBinding.ForDocument<EveProviderAdvertisementState>(registry)
});
}
@ -58,6 +59,7 @@ public static class AetheriaDocumentRegistry
typeof(AetheriaZoneState),
typeof(AetheriaEntitySnapshot),
typeof(AetheriaRuntimeCommitDrainStatus),
typeof(EveSurfaceState)
typeof(EveSurfaceState),
typeof(EveProviderAdvertisementState)
];
}

View file

@ -0,0 +1,81 @@
using Aetheria.State.Documents;
namespace Aetheria.State;
public static class AetheriaProviderAdvertisementProjector
{
public const string AdvertisementKey = "eve:provider:aetheria";
public const string ProviderId = "aetheria";
public static EveProviderAdvertisementState Build(string statePath, string updatedAtUtc)
{
return new EveProviderAdvertisementState
{
ProviderId = ProviderId,
ServiceId = "aetheria.runtime",
VerseId = "aetheria.local",
RootVerse = "asgard",
CanonicalService = "asgard.aetheria",
LocatedService = "asgard.local.aetheria",
CultMeshAddress = "asgard.local.aetheria/eve",
Title = "Aetheria",
Kind = "game.runtime",
UpdatedAtUtc = updatedAtUtc,
Freshness = new EveProviderFreshness
{
State = "fresh",
LastSeenAtUtc = updatedAtUtc,
MaxAgeMs = 15000
},
Schemas =
[
"aetheria.world_state.v1",
"aetheria.item_definition.v1",
"aetheria.corporation.v2",
"aetheria.name_file.v2",
"aetheria.player_settings.v1",
"aetheria.loadout_template.v1",
"aetheria.run_state.v1",
"aetheria.zone_state.v1",
"aetheria.entity_snapshot.v1",
"aetheria.runtime_commit_drain_status.v1",
"gamecult.eve.surface.v1"
],
Witnesses =
[
new EveProviderWitness
{
Kind = "cultcache",
Ref = statePath,
Summary = "Aetheria typed CultCache state file"
}
],
Surfaces =
[
new EveProviderSurfaceRef
{
SurfaceId = AetheriaCatalogSurfaceProjector.SurfaceId,
Key = AetheriaCatalogSurfaceProjector.SurfaceKey
},
new EveProviderSurfaceRef
{
SurfaceId = AetheriaOperationsSurfaceProjector.SurfaceId,
Key = AetheriaOperationsSurfaceProjector.SurfaceKey
}
],
Commands =
[
new EveProviderCommandRef
{
Command = "aetheria.catalog.refresh",
Summary = "Refresh catalog projection"
},
new EveProviderCommandRef
{
Command = "aetheria.operations.refresh",
Summary = "Refresh operations projection"
}
]
};
}
}

View file

@ -183,6 +183,18 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
return Database.GetAsync<EveSurfaceState>(new CultRecordKey(AetheriaOperationsSurfaceProjector.SurfaceKey));
}
public Task<CultRecordHandle<EveProviderAdvertisementState>> PutProviderAdvertisementAsync(
EveProviderAdvertisementState advertisement)
{
return Database.PutAsync(new CultRecordKey(AetheriaProviderAdvertisementProjector.AdvertisementKey), advertisement);
}
public Task<EveProviderAdvertisementState?> GetProviderAdvertisementAsync()
{
return Database.GetAsync<EveProviderAdvertisementState>(
new CultRecordKey(AetheriaProviderAdvertisementProjector.AdvertisementKey));
}
public Task<CultRecordHandle<AetheriaPlayerSettings>> PutPlayerSettingsAsync(AetheriaPlayerSettings settings)
{
return Database.PutAsync(new CultRecordKey("global:aetheria.player_settings.v1"), settings);

View file

@ -0,0 +1,118 @@
using GameCult.Caching;
using MessagePack;
namespace Aetheria.State.Documents;
[CultDocument("gamecult.eve.provider_advertisement", "gamecult.eve.provider_advertisement.v1")]
[CultGlobal]
[MessagePackObject]
public sealed class EveProviderAdvertisementState
{
[Key(0)]
[CultIndex("schema")]
public string Schema { get; set; } = "gamecult.eve.provider_advertisement.v1";
[Key(1)]
[CultName]
public string ProviderId { get; set; } = "";
[Key(2)]
public string ServiceId { get; set; } = "";
[Key(3)]
public string VerseId { get; set; } = "";
[Key(4)]
public string RootVerse { get; set; } = "asgard";
[Key(5)]
public string CanonicalService { get; set; } = "";
[Key(6)]
public string LocatedService { get; set; } = "";
[Key(7)]
public string CultMeshAddress { get; set; } = "";
[Key(8)]
public string Title { get; set; } = "";
[Key(9)]
public string Kind { get; set; } = "game.runtime";
[Key(10)]
public string UpdatedAtUtc { get; set; } = "";
[Key(11)]
public EveProviderFreshness Freshness { get; set; } = new();
[Key(12)]
public string[] Schemas { get; set; } = [];
[Key(13)]
public EveProviderWitness[] Witnesses { get; set; } = [];
[Key(14)]
public EveProviderSurfaceRef[] Surfaces { get; set; } = [];
[Key(15)]
public EveProviderCommandRef[] Commands { get; set; } = [];
}
[MessagePackObject]
public sealed class EveProviderFreshness
{
[Key(0)]
public string State { get; set; } = "fresh";
[Key(1)]
public string LastSeenAtUtc { get; set; } = "";
[Key(2)]
public int MaxAgeMs { get; set; } = 15000;
}
[MessagePackObject]
public sealed class EveProviderWitness
{
[Key(0)]
public string Kind { get; set; } = "";
[Key(1)]
public string Ref { get; set; } = "";
[Key(2)]
public string Summary { get; set; } = "";
}
[MessagePackObject]
public sealed class EveProviderSurfaceRef
{
[Key(0)]
public string Schema { get; set; } = "gamecult.eve.surface.v1";
[Key(1)]
public string SurfaceId { get; set; } = "";
[Key(2)]
public string Key { get; set; } = "";
[Key(3)]
public string Transport { get; set; } = "cultmesh";
[Key(4)]
public string Status { get; set; } = "available";
}
[MessagePackObject]
public sealed class EveProviderCommandRef
{
[Key(0)]
public string Command { get; set; } = "";
[Key(1)]
public string Transport { get; set; } = "cultmesh";
[Key(2)]
public string Summary { get; set; } = "";
}

View file

@ -140,7 +140,9 @@ dotnet run --project .\Economy.Server\Economy.Server.csproj -- --apply-pending-o
The server publishes `AetheriaRuntimeCommitDrainStatus` and the
`aetheria.operations` Eve surface after each drain attempt, so pending depth,
applied counts, failures, and timestamps are typed state rather than console-only
status.
status. It also publishes `gamecult.eve.provider_advertisement.v1` for the
`aetheria` provider, advertising the catalog and operations surfaces plus the
typed schemas witnessed by the local `.cc` state file.
`Aetheria.State.ApplyPending` is the smaller local operator applicator for
queued Unity runtime commits when the server host is not being used:

View file

@ -130,6 +130,8 @@ internal static class Program
{
await node.PutRuntimeCommitDrainStatusAsync(status).ConfigureAwait(false);
await node.PutOperationsSurfaceAsync(AetheriaOperationsSurfaceProjector.Build(status)).ConfigureAwait(false);
await node.PutProviderAdvertisementAsync(
AetheriaProviderAdvertisementProjector.Build(node.StatePath, status.LastPollAtUtc)).ConfigureAwait(false);
await node.FlushAsync().ConfigureAwait(false);
}

View file

@ -178,6 +178,11 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted.
- `AetheriaRuntimeCommitDrainStatus` and the `aetheria.operations` Eve surface
publish pending-drain health, pending depth, applied counts, failures, and
timestamps as typed state. Console logs are notification-only.
- `AetheriaProviderAdvertisementProjector` publishes
`gamecult.eve.provider_advertisement.v1` for the `aetheria` provider,
advertising the catalog and operations surfaces, command boundaries, schemas,
and `.cc` witness path. This is the discovery map for Odin/Eve, not a health
page.
- `AetheriaCatalogSurfaceProjector` now emits the first provider-owned Eve
surface from typed catalog state. The importer materializes a
`gamecult.eve.surface.v1` catalog operator document at
@ -542,6 +547,8 @@ First Aetheria surfaces to publish:
and daemon polling, with `--apply-pending-once` for bounded operation.
- Done: publish pending-drain health as typed CultCache state and an Eve
operations surface.
- Done: publish an Eve provider advertisement so Odin/Eve can discover
Aetheria surfaces and command boundaries through typed state.
- Done: stop legacy catalog pull/read paths from writing entries back to
their source backing store.
- Done: delete legacy catalog backing-store write/realtime APIs.