Establish shared Hangar deployment spine

This commit is contained in:
Meta Vens 2026-08-07 15:52:38 +01:00
commit e9d1e04897
11 changed files with 674 additions and 7 deletions

View file

@ -0,0 +1,114 @@
using System;
using System.Linq;
using GameCult.Caching;
using MessagePack;
#nullable enable
namespace GameCult.Aetheria.State.Verse
{
public static class AetheriaGameModes
{
public const string Terminus = "terminus";
public const string Starbridge = "starbridge";
public const string Arena = "arena";
public static bool IsKnown(string? mode) =>
string.Equals(mode, Terminus, StringComparison.Ordinal) ||
string.Equals(mode, Starbridge, StringComparison.Ordinal) ||
string.Equals(mode, Arena, StringComparison.Ordinal);
}
public static class AetheriaModePolicies
{
public const string TerminusLocal = "aetheria.mode.terminus.local.v1";
public const string StarbridgeMixed = "aetheria.mode.starbridge.mixed.v1";
public const string ArenaServer = "aetheria.mode.arena.server.v1";
public static string ForMode(string? mode)
{
if (string.Equals(mode, AetheriaGameModes.Terminus, StringComparison.Ordinal)) return TerminusLocal;
if (string.Equals(mode, AetheriaGameModes.Starbridge, StringComparison.Ordinal)) return StarbridgeMixed;
if (string.Equals(mode, AetheriaGameModes.Arena, StringComparison.Ordinal)) return ArenaServer;
return "";
}
}
public static class AetheriaHangarShipStatuses
{
public const string Available = "available";
public const string Deployed = "deployed";
public const string Lost = "lost";
}
[CultDocument("gamecult.aetheria.hangar", "gamecult.aetheria.hangar.v1")]
[CultGlobal]
[MessagePackObject]
public sealed class AetheriaHangarState
{
[Key(0), CultName] public string Name { get; set; } = "Aetheria Hangar";
[Key(1)] public string HangarId { get; set; } = "local";
[Key(2)] public string PlayerKey { get; set; } = "";
[Key(3)] public long Revision { get; set; }
[Key(4)] public AetheriaHangarShip[] Ships { get; set; } = Array.Empty<AetheriaHangarShip>();
[Key(5)] public AetheriaHangarItemStack[] Inventory { get; set; } = Array.Empty<AetheriaHangarItemStack>();
[Key(6)] public AetheriaHangarCurrency[] Currencies { get; set; } = Array.Empty<AetheriaHangarCurrency>();
[Key(7)] public string[] UnlockKeys { get; set; } = Array.Empty<string>();
[Key(8)] public string[] LoadoutTemplateKeys { get; set; } = Array.Empty<string>();
[Key(9)] public AetheriaDeploymentReceipt[] Deployments { get; set; } = Array.Empty<AetheriaDeploymentReceipt>();
[Key(10)] public string UpdatedAtUtc { get; set; } = "";
}
[MessagePackObject]
public sealed class AetheriaHangarShip
{
[Key(0)] public string ShipId { get; set; } = "";
[Key(1)] public string HullItemKey { get; set; } = "";
[Key(2)] public string LoadoutTemplateKey { get; set; } = "";
[Key(3)] public string Status { get; set; } = AetheriaHangarShipStatuses.Available;
[Key(4)] public string ActiveDeploymentId { get; set; } = "";
}
[MessagePackObject]
public sealed class AetheriaHangarItemStack
{
[Key(0)] public string ItemKey { get; set; } = "";
[Key(1)] public long Quantity { get; set; }
}
[MessagePackObject]
public sealed class AetheriaHangarCurrency
{
[Key(0)] public string CurrencyKey { get; set; } = "";
[Key(1)] public long Quantity { get; set; }
}
[MessagePackObject]
public sealed class AetheriaDeploymentRequest
{
[Key(0)] public string RequestId { get; set; } = "";
[Key(1)] public string PlayerKey { get; set; } = "";
[Key(2)] public string Mode { get; set; } = AetheriaGameModes.Terminus;
[Key(3)] public string ShipId { get; set; } = "";
[Key(4)] public string LoadoutTemplateKey { get; set; } = "";
[Key(5)] public long ExpectedHangarRevision { get; set; }
[Key(6)] public string ModePolicyId { get; set; } = "";
}
[MessagePackObject]
public sealed class AetheriaDeploymentReceipt
{
[Key(0)] public string DeploymentId { get; set; } = "";
[Key(1)] public string RequestId { get; set; } = "";
[Key(2)] public bool Accepted { get; set; }
[Key(3)] public string Diagnostic { get; set; } = "";
[Key(4)] public string PlayerKey { get; set; } = "";
[Key(5)] public string Mode { get; set; } = "";
[Key(6)] public string ShipId { get; set; } = "";
[Key(7)] public string LoadoutTemplateKey { get; set; } = "";
[Key(8)] public long HangarRevision { get; set; }
[Key(9)] public string ModePolicyId { get; set; } = "";
[Key(10)] public AetheriaRuntimeEntityLoadoutCommit Loadout { get; set; } = new AetheriaRuntimeEntityLoadoutCommit();
[Key(11)] public string CommittedAtUtc { get; set; } = "";
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d95d5e88e2194e88a2af84af6c5cd893

View file

@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
#nullable enable
namespace GameCult.Aetheria.State.Verse
{
public static class AetheriaRuntimeHangarCommands
{
public const string SurfaceId = "aetheria.hangar";
public const string SelectShip = "aetheria.hangar.select_ship";
public const string SelectTerminus = "aetheria.hangar.select_mode.terminus";
public const string SelectStarbridge = "aetheria.hangar.select_mode.starbridge";
public const string SelectArena = "aetheria.hangar.select_mode.arena";
public const string EditLoadout = "aetheria.hangar.edit_loadout";
public const string Launch = "aetheria.hangar.launch";
}
public static class AetheriaRuntimeHangarSurfaceBuilder
{
public static AetheriaRuntimeSurfaceDocument Build(
AetheriaHangarState hangar,
string selectedShipId,
string selectedMode,
string updatedAtUtc,
long version = 1)
{
if (hangar == null) throw new ArgumentNullException(nameof(hangar));
var ships = hangar.Ships ?? Array.Empty<AetheriaHangarShip>();
var selected = ships.FirstOrDefault(ship => string.Equals(ship.ShipId, selectedShipId, StringComparison.Ordinal))
?? ships.FirstOrDefault();
var mode = AetheriaGameModes.IsKnown(selectedMode) ? selectedMode : AetheriaGameModes.Terminus;
var canLaunch = selected != null && string.Equals(selected.Status, AetheriaHangarShipStatuses.Available, StringComparison.Ordinal);
var root = Component(
"aetheria.hangar.root",
"surface",
Props(("title", "HANGAR"), ("selectedMode", mode), ("hangarRevision", hangar.Revision.ToString(CultureInfo.InvariantCulture))),
new[]
{
Panel("aetheria.hangar.ship_summary", "SHIP", selected == null
? new[] { Text("aetheria.hangar.ship.none", "No ship selected") }
: new[]
{
Metric("aetheria.hangar.ship.id", "Ship", selected.ShipId),
Metric("aetheria.hangar.ship.hull", "Hull", selected.HullItemKey),
Metric("aetheria.hangar.ship.status", "Status", selected.Status),
Metric("aetheria.hangar.ship.loadout", "Loadout", selected.LoadoutTemplateKey)
}, "summary"),
Panel("aetheria.hangar.preview", "SHIP PREVIEW", new[]
{
Component("aetheria.hangar.preview.slot", "asset.preview", Props(
("assetRole", "ship.preview"),
("subjectKey", selected?.ShipId ?? "")), Array.Empty<AetheriaRuntimeSurfaceComponent>())
}, "preview"),
Panel("aetheria.hangar.fit", "FIT SUMMARY", new[]
{
Metric("aetheria.hangar.fit.hull", "Hull", selected?.HullItemKey ?? "-"),
Metric("aetheria.hangar.fit.template", "Template", selected?.LoadoutTemplateKey ?? "-"),
Metric("aetheria.hangar.fit.policy", "Authority", AetheriaModePolicies.ForMode(mode)),
Button("aetheria.hangar.fit.edit", "EDIT LOADOUT", AetheriaRuntimeHangarCommands.EditLoadout,
("targetSurfaceId", AetheriaRuntimeInventoryPanelSurfaceBuilder.SurfaceId))
}, "fit"),
Component("aetheria.hangar.bays", "row", Props(("label", "OWNED SHIPS")),
ships.Select(ship => Button(
"aetheria.hangar.bay." + StableToken(ship.ShipId),
ship.ShipId,
AetheriaRuntimeHangarCommands.SelectShip,
("shipId", ship.ShipId),
("selected", string.Equals(ship.ShipId, selected?.ShipId, StringComparison.Ordinal) ? "true" : "false"),
("status", ship.Status))).ToArray(),
Layout(("gridArea", "bays"), ("display", "flex"), ("overflowX", "auto"), ("gap", "8"))),
Component("aetheria.hangar.launcher", "row", Props(), new[]
{
Button("aetheria.hangar.mode.terminus", "TERMINUS", AetheriaRuntimeHangarCommands.SelectTerminus,
("selected", (mode == AetheriaGameModes.Terminus).ToString().ToLowerInvariant())),
Button("aetheria.hangar.mode.starbridge", "STARBRIDGE", AetheriaRuntimeHangarCommands.SelectStarbridge,
("selected", (mode == AetheriaGameModes.Starbridge).ToString().ToLowerInvariant())),
Button("aetheria.hangar.mode.arena", "ARENA", AetheriaRuntimeHangarCommands.SelectArena,
("selected", (mode == AetheriaGameModes.Arena).ToString().ToLowerInvariant())),
Button("aetheria.hangar.launch", "LAUNCH", AetheriaRuntimeHangarCommands.Launch,
("enabled", canLaunch.ToString().ToLowerInvariant()),
("shipId", selected?.ShipId ?? ""),
("mode", mode),
("expectedHangarRevision", hangar.Revision.ToString(CultureInfo.InvariantCulture)))
}, Layout(("gridArea", "launch"), ("display", "flex"), ("justifyContent", "flex-end"), ("gap", "8")))
},
Layout(
("display", "grid"),
("gridTemplateColumns", "minmax(260px, 0.75fr) minmax(480px, 1.6fr) minmax(280px, 0.8fr)"),
("gridTemplateRows", "auto minmax(360px, 1fr) auto"),
("gridTemplateAreas", "\"launch launch launch\" \"summary preview fit\" \"bays bays bays\""),
("gap", "10"),
("height", "100%")),
Style(("background", "#070b0d"), ("color", "#d8eef2")));
return new AetheriaRuntimeSurfaceDocument(
"aetheria",
"game.hangar",
"Aetheria Hangar",
version,
updatedAtUtc ?? "",
new AetheriaRuntimeSurfaceTree(AetheriaRuntimeHangarCommands.SurfaceId, root, Array.Empty<AetheriaRuntimeSurfaceStyleToken>()),
new[]
{
Command(AetheriaRuntimeHangarCommands.SelectShip, "Select Ship"),
Command(AetheriaRuntimeHangarCommands.SelectTerminus, "Select Terminus"),
Command(AetheriaRuntimeHangarCommands.SelectStarbridge, "Select Starbridge"),
Command(AetheriaRuntimeHangarCommands.SelectArena, "Select Arena"),
Command(AetheriaRuntimeHangarCommands.EditLoadout, "Edit Loadout"),
Command(AetheriaRuntimeHangarCommands.Launch, "Launch")
});
}
private static AetheriaRuntimeSurfaceComponent Panel(string id, string title, IReadOnlyList<AetheriaRuntimeSurfaceComponent> children, string area) =>
Component(id, "panel", Props(("title", title)), children, Layout(("gridArea", area)), Style(("background", "#11181c"), ("border", "1px solid #60737a"), ("padding", "12")));
private static AetheriaRuntimeSurfaceComponent Metric(string id, string label, string value) =>
Component(id, "metric", Props(("label", label), ("value", value)), Array.Empty<AetheriaRuntimeSurfaceComponent>());
private static AetheriaRuntimeSurfaceComponent Text(string id, string value) =>
Component(id, "text", Props(("value", value)), Array.Empty<AetheriaRuntimeSurfaceComponent>());
private static AetheriaRuntimeSurfaceComponent Button(string id, string label, string command, params (string Key, string Value)[] extra) =>
Component(id, "button", Props(new[] { ("label", label), ("command", command) }.Concat(extra).ToArray()), Array.Empty<AetheriaRuntimeSurfaceComponent>());
private static AetheriaRuntimeSurfaceCommandTemplate Command(string command, string label) =>
new AetheriaRuntimeSurfaceCommandTemplate(command, label, AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport);
private static AetheriaRuntimeSurfaceComponent Component(
string id,
string kind,
IReadOnlyDictionary<string, string> props,
IReadOnlyList<AetheriaRuntimeSurfaceComponent> children,
IReadOnlyDictionary<string, string>? layout = null,
IReadOnlyDictionary<string, string>? style = null) =>
new AetheriaRuntimeSurfaceComponent(id, kind, props, children, Array.Empty<GameCult.Mesh.CultMeshStateBindingDescriptor>(), Array.Empty<AetheriaRuntimeEmbeddedDocumentSlot>(), layout, style);
private static IReadOnlyDictionary<string, string> Props(params (string Key, string Value)[] values) =>
values.ToDictionary(value => value.Key, value => value.Value ?? "", StringComparer.Ordinal);
private static IReadOnlyDictionary<string, string> Layout(params (string Key, string Value)[] values) => Props(values);
private static IReadOnlyDictionary<string, string> Style(params (string Key, string Value)[] values) => Props(values);
private static string StableToken(string value) =>
new string((value ?? "").Select(character => char.IsLetterOrDigit(character) ? char.ToLowerInvariant(character) : '.').ToArray()).Trim('.');
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4c79a2f47dbf437c91d6cb4d4310f9c4