Own the portable Eve surface contract
This commit is contained in:
parent
7f51dfd6ac
commit
dd196508ac
33 changed files with 2878 additions and 0 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -10,6 +10,8 @@ packages/eve-contracts/node_modules/
|
||||||
packages/eve-contracts/*.tgz
|
packages/eve-contracts/*.tgz
|
||||||
!packages/org.gamecult.eve.surface/
|
!packages/org.gamecult.eve.surface/
|
||||||
!packages/org.gamecult.eve.surface/**
|
!packages/org.gamecult.eve.surface/**
|
||||||
|
packages/org.gamecult.eve.surface/bin~/
|
||||||
|
packages/org.gamecult.eve.surface/obj~/
|
||||||
!packages/org.gamecult.eve.unity-uitoolkit/
|
!packages/org.gamecult.eve.unity-uitoolkit/
|
||||||
!packages/org.gamecult.eve.unity-uitoolkit/**
|
!packages/org.gamecult.eve.unity-uitoolkit/**
|
||||||
obj/
|
obj/
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,11 @@ effects; Eve owns projection, input capture, timestamped sensor publication,
|
||||||
and renderer parity. If a pretty panel cannot say which fields are stale, denied,
|
and renderer parity. If a pretty panel cannot say which fields are stale, denied,
|
||||||
predicted, or authoritative, it is not done. It is flirting with the operator.
|
predicted, or authoritative, it is not done. It is flirting with the operator.
|
||||||
|
|
||||||
|
The renderer-neutral C# contract lives at
|
||||||
|
`packages/org.gamecult.eve.surface`. Provider daemons and lowerers consume that
|
||||||
|
Eve-owned package; renderer repositories do not own copies of the surface
|
||||||
|
schema or composition builders.
|
||||||
|
|
||||||
## Current Mechanism
|
## Current Mechanism
|
||||||
|
|
||||||
The current checked-in client is the iOS Theos app, still named `EveCanvas` at
|
The current checked-in client is the iOS Theos app, still named `EveCanvas` at
|
||||||
|
|
|
||||||
9
packages/org.gamecult.eve.surface/Directory.Build.props
Normal file
9
packages/org.gamecult.eve.surface/Directory.Build.props
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<BaseOutputPath>$(MSBuildThisFileDirectory)bin~\</BaseOutputPath>
|
||||||
|
<BaseIntermediateOutputPath>$(MSBuildThisFileDirectory)obj~\</BaseIntermediateOutputPath>
|
||||||
|
<MSBuildProjectExtensionsPath>$(MSBuildThisFileDirectory)obj~\</MSBuildProjectExtensionsPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Description>Portable Eve surface and provider contracts.</Description>
|
||||||
|
<CultLibRoot Condition="'$(CultLibRoot)' == ''">$(MSBuildThisFileDirectory)..\..\..\CultLib</CultLibRoot>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Caching\GameCult.Caching.csproj" />
|
||||||
|
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Mesh\GameCult.Mesh.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ce2d4913d64690c44ae68935014fd79d
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
||||||
|
|
||||||
36
packages/org.gamecult.eve.surface/README.md
Normal file
36
packages/org.gamecult.eve.surface/README.md
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Eve Surface Contract
|
||||||
|
|
||||||
|
Eve owns this renderer-neutral package. It defines the retained CultUI surface
|
||||||
|
document, provider advertisement, asset catalog, input vocabulary, and builder
|
||||||
|
primitives shared by providers and lowerers. Headless daemons depend on this
|
||||||
|
package without depending on EveUnity; Unity is one consumer among many.
|
||||||
|
|
||||||
|
Components own normal child layout through `Children`, live CultMesh value
|
||||||
|
bindings through `StateBindings`, and nested synced document regions through
|
||||||
|
`EmbeddedDocuments`. An embedded document slot names the child document id,
|
||||||
|
schema, presentation kind, and route hint; renderers resolve that child through
|
||||||
|
CultMesh instead of rebuilding it with runtime-local projection code.
|
||||||
|
|
||||||
|
See `../../docs/surface-contract-v1.md` for the full contract and
|
||||||
|
`../../web/fixtures/cultui-embedded-surface.json` for the parity fixture.
|
||||||
|
Runtime discovery and required feature coverage live in
|
||||||
|
`../../tools/parity/parity-manifest.json`; active GUI runtimes must list
|
||||||
|
`embeddedDocuments` as a supported feature and require the `embedded-surface`
|
||||||
|
fixture.
|
||||||
|
|
||||||
|
Verification entrypoints:
|
||||||
|
|
||||||
|
- C# serialization: `dotnet test tests/GameCult.Eve.Surface.Serialization.Tests/GameCult.Eve.Surface.Serialization.Tests.csproj`
|
||||||
|
- Browser/TypeScript: `node --test web\eve-dsl.test.mjs`
|
||||||
|
- Semantic runtime matrix: `powershell -ExecutionPolicy Bypass -File .\scripts\run-parity-harness.ps1`
|
||||||
|
- Flutter GUI contract, when Flutter is installed: `flutter test --plain-name embedded_surface_fixture_contract`
|
||||||
|
- Rust/CultMesh document sync: run `cargo test rust_preserves_cultui_embedded_surface_slots_through_typed_document_sync` from the sibling `CultLib/packages/cultnet-rs` directory.
|
||||||
|
|
||||||
|
The parity manifest is the discovery source for web, Flutter desktop/Linux/Android,
|
||||||
|
iOS/UIKit, Android/Kotlin, Unity UI Toolkit, and Rust contract coverage. Do not
|
||||||
|
add a new renderer claim without adding its nested-surface fixture requirement
|
||||||
|
there.
|
||||||
|
|
||||||
|
EveUnity must not carry another live copy of this package. Its Unity Scene and
|
||||||
|
UI Toolkit packages depend on the released Eve-owned package.
|
||||||
|
|
||||||
9
packages/org.gamecult.eve.surface/README.md.meta
Normal file
9
packages/org.gamecult.eve.surface/README.md.meta
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: fe5aa27636bead047b2aecb14fdc59f2
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
||||||
|
|
||||||
10
packages/org.gamecult.eve.surface/Runtime.meta
Normal file
10
packages/org.gamecult.eve.surface/Runtime.meta
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2164f6fea1216214d895e7dc99c46ecf
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using GameCult.Caching;
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace GameCult.Eve.Surface
|
||||||
|
{
|
||||||
|
[CultDocument("gamecult.eve.asset_catalog", SchemaId)]
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveAssetCatalogDocument
|
||||||
|
{
|
||||||
|
public const string SchemaId = "gamecult.eve.asset_catalog.v1";
|
||||||
|
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EveAssetCatalogDocument(
|
||||||
|
string schema,
|
||||||
|
string providerId,
|
||||||
|
string catalogId,
|
||||||
|
long version,
|
||||||
|
string updatedAtUtc,
|
||||||
|
EveAssetCatalogEntry[] assets)
|
||||||
|
{
|
||||||
|
Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema;
|
||||||
|
ProviderId = providerId ?? "";
|
||||||
|
CatalogId = catalogId ?? "";
|
||||||
|
Version = version;
|
||||||
|
UpdatedAtUtc = updatedAtUtc ?? "";
|
||||||
|
Assets = assets ?? Array.Empty<EveAssetCatalogEntry>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveAssetCatalogDocument(
|
||||||
|
string providerId,
|
||||||
|
string catalogId,
|
||||||
|
long version,
|
||||||
|
string updatedAtUtc,
|
||||||
|
EveAssetCatalogEntry[] assets)
|
||||||
|
: this(SchemaId, providerId, catalogId, version, updatedAtUtc, assets)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)] public string Schema { get; }
|
||||||
|
[Key(1)] public string ProviderId { get; }
|
||||||
|
[Key(2)] public string CatalogId { get; }
|
||||||
|
[Key(3)] public long Version { get; }
|
||||||
|
[Key(4)] public string UpdatedAtUtc { get; }
|
||||||
|
[Key(5)] public EveAssetCatalogEntry[] Assets { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveAssetCatalogEntry
|
||||||
|
{
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EveAssetCatalogEntry(
|
||||||
|
string assetRef,
|
||||||
|
string assetKind,
|
||||||
|
EveAssetVariant[] variants,
|
||||||
|
Dictionary<string, string> metadata)
|
||||||
|
{
|
||||||
|
AssetRef = assetRef ?? "";
|
||||||
|
AssetKind = assetKind ?? "";
|
||||||
|
Variants = variants ?? Array.Empty<EveAssetVariant>();
|
||||||
|
Metadata = metadata == null
|
||||||
|
? new Dictionary<string, string>(StringComparer.Ordinal)
|
||||||
|
: new Dictionary<string, string>(metadata, StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveAssetCatalogEntry(
|
||||||
|
string assetRef,
|
||||||
|
string assetKind,
|
||||||
|
EveAssetVariant[] variants,
|
||||||
|
IReadOnlyDictionary<string, string>? metadata = null)
|
||||||
|
: this(
|
||||||
|
assetRef,
|
||||||
|
assetKind,
|
||||||
|
variants,
|
||||||
|
metadata == null ? new Dictionary<string, string>() : new Dictionary<string, string>(metadata))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)] public string AssetRef { get; }
|
||||||
|
[Key(1)] public string AssetKind { get; }
|
||||||
|
[Key(2)] public EveAssetVariant[] Variants { get; }
|
||||||
|
[Key(3)] public Dictionary<string, string> Metadata { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveAssetVariant
|
||||||
|
{
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EveAssetVariant(
|
||||||
|
string runtimeId,
|
||||||
|
string platform,
|
||||||
|
string format,
|
||||||
|
string uri,
|
||||||
|
string contentHash,
|
||||||
|
long sizeBytes,
|
||||||
|
string assetKey,
|
||||||
|
Dictionary<string, string> metadata)
|
||||||
|
{
|
||||||
|
RuntimeId = runtimeId ?? "";
|
||||||
|
Platform = platform ?? "";
|
||||||
|
Format = format ?? "";
|
||||||
|
Uri = uri ?? "";
|
||||||
|
ContentHash = contentHash ?? "";
|
||||||
|
SizeBytes = sizeBytes;
|
||||||
|
AssetKey = assetKey ?? "";
|
||||||
|
Metadata = metadata == null
|
||||||
|
? new Dictionary<string, string>(StringComparer.Ordinal)
|
||||||
|
: new Dictionary<string, string>(metadata, StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveAssetVariant(
|
||||||
|
string runtimeId,
|
||||||
|
string platform,
|
||||||
|
string format,
|
||||||
|
string uri,
|
||||||
|
string contentHash,
|
||||||
|
long sizeBytes,
|
||||||
|
string assetKey,
|
||||||
|
IReadOnlyDictionary<string, string>? metadata = null)
|
||||||
|
: this(
|
||||||
|
runtimeId,
|
||||||
|
platform,
|
||||||
|
format,
|
||||||
|
uri,
|
||||||
|
contentHash,
|
||||||
|
sizeBytes,
|
||||||
|
assetKey,
|
||||||
|
metadata == null ? new Dictionary<string, string>() : new Dictionary<string, string>(metadata))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)] public string RuntimeId { get; }
|
||||||
|
[Key(1)] public string Platform { get; }
|
||||||
|
[Key(2)] public string Format { get; }
|
||||||
|
[Key(3)] public string Uri { get; }
|
||||||
|
[Key(4)] public string ContentHash { get; }
|
||||||
|
[Key(5)] public long SizeBytes { get; }
|
||||||
|
[Key(6)] public string AssetKey { get; }
|
||||||
|
[Key(7)] public Dictionary<string, string> Metadata { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8f53654f7c98d894588127ba001f11b4
|
||||||
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
using System;
|
||||||
|
using GameCult.Caching;
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace GameCult.Eve.Surface
|
||||||
|
{
|
||||||
|
[CultDocument("gamecult.eve.entity_soa_view", SchemaId)]
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveEntitySoaViewDocument
|
||||||
|
{
|
||||||
|
public const string SchemaId = "gamecult.eve.entity_soa_view.v2";
|
||||||
|
|
||||||
|
[Key(0)] public string Schema { get; set; } = SchemaId;
|
||||||
|
[Key(1)] public string ProviderId { get; set; } = "";
|
||||||
|
[Key(2)] public string ViewId { get; set; } = "";
|
||||||
|
[Key(3)] public string PublishedAtUtc { get; set; } = "";
|
||||||
|
[Key(4)] public string BodySchemaId { get; set; } = "";
|
||||||
|
[Key(5)] public int LayoutVersion { get; set; }
|
||||||
|
[Key(6)] public long ProducerEpoch { get; set; }
|
||||||
|
[Key(7)] public long Sequence { get; set; }
|
||||||
|
[Key(8)] public int Capacity { get; set; }
|
||||||
|
[Key(9)] public EveEntitySoaBuffer[] Buffers { get; set; } = Array.Empty<EveEntitySoaBuffer>();
|
||||||
|
[Key(10)] public EveEntitySoaColumn[] Columns { get; set; } = Array.Empty<EveEntitySoaColumn>();
|
||||||
|
[Key(11)] public EveEntitySoaDirtyRange[] DirtyRanges { get; set; } = Array.Empty<EveEntitySoaDirtyRange>();
|
||||||
|
[Key(12)] public EveEntityRenderGroup[] RenderGroups { get; set; } = Array.Empty<EveEntityRenderGroup>();
|
||||||
|
[Key(13)] public EveEntityIdentity[] Identities { get; set; } = Array.Empty<EveEntityIdentity>();
|
||||||
|
[Key(14)] public long FrameId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveEntitySoaBuffer
|
||||||
|
{
|
||||||
|
[Key(0)] public string BufferId { get; set; } = "";
|
||||||
|
[Key(1)] public long ByteOffset { get; set; }
|
||||||
|
[Key(2)] public long ByteLength { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveEntitySoaColumn
|
||||||
|
{
|
||||||
|
[Key(0)] public string ColumnId { get; set; } = "";
|
||||||
|
[Key(1)] public string Semantic { get; set; } = "";
|
||||||
|
[Key(2)] public string BufferId { get; set; } = "";
|
||||||
|
[Key(3)] public string ScalarType { get; set; } = "";
|
||||||
|
[Key(4)] public long ByteOffset { get; set; }
|
||||||
|
[Key(5)] public int ElementStride { get; set; }
|
||||||
|
[Key(6)] public int ElementCount { get; set; }
|
||||||
|
[Key(7)] public string Unit { get; set; } = "";
|
||||||
|
[Key(8)] public string CoordinateSpace { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveEntitySoaDirtyRange
|
||||||
|
{
|
||||||
|
[Key(0)] public string ColumnId { get; set; } = "";
|
||||||
|
[Key(1)] public int StartIndex { get; set; }
|
||||||
|
[Key(2)] public int Count { get; set; }
|
||||||
|
[Key(3)] public long Sequence { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveEntityIdentity
|
||||||
|
{
|
||||||
|
[Key(0)] public int Index { get; set; }
|
||||||
|
[Key(1)] public string EntityId { get; set; } = "";
|
||||||
|
[Key(2)] public string EntityKind { get; set; } = "";
|
||||||
|
[Key(3)] public string Label { get; set; } = "";
|
||||||
|
[Key(4)] public string Faction { get; set; } = "";
|
||||||
|
[Key(5)] public bool Selectable { get; set; }
|
||||||
|
[Key(6)] public bool Controllable { get; set; }
|
||||||
|
[Key(7)] public string AssetRef { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveEntityRenderGroup
|
||||||
|
{
|
||||||
|
[Key(0)] public int GroupId { get; set; }
|
||||||
|
[Key(1)] public string MeshAssetRef { get; set; } = "";
|
||||||
|
[Key(2)] public string MaterialAssetRef { get; set; } = "";
|
||||||
|
[Key(3)] public int SubMeshIndex { get; set; }
|
||||||
|
[Key(4)] public int Layer { get; set; }
|
||||||
|
[Key(5)] public int InstanceCount { get; set; }
|
||||||
|
[Key(6)] public float DefaultScale { get; set; } = 1f;
|
||||||
|
[Key(7)] public float BoundsCenterX { get; set; }
|
||||||
|
[Key(8)] public float BoundsCenterY { get; set; }
|
||||||
|
[Key(9)] public float BoundsCenterZ { get; set; }
|
||||||
|
[Key(10)] public float BoundsSizeX { get; set; }
|
||||||
|
[Key(11)] public float BoundsSizeY { get; set; }
|
||||||
|
[Key(12)] public float BoundsSizeZ { get; set; }
|
||||||
|
[Key(13)] public string ShadowMode { get; set; } = "on";
|
||||||
|
[Key(14)] public bool ReceiveShadows { get; set; } = true;
|
||||||
|
[Key(15)] public int Lod { get; set; } = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8e87e1be783846508efea45787f2d45c
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using GameCult.Caching;
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
namespace GameCult.Eve.Surface
|
||||||
|
{
|
||||||
|
[CultDocument("gamecult.eve.input_capability", SchemaId)]
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveInputCapabilityDocument
|
||||||
|
{
|
||||||
|
public const string SchemaId = "gamecult.eve.input_capability.v1";
|
||||||
|
[Key(0)] public string Schema { get; set; } = SchemaId;
|
||||||
|
[Key(1)] public string ProviderId { get; set; } = "";
|
||||||
|
[Key(2)] public string CapabilityId { get; set; } = "";
|
||||||
|
[Key(3)] public long Version { get; set; }
|
||||||
|
[Key(4)] public EveInputActionDocument[] Actions { get; set; } = Array.Empty<EveInputActionDocument>();
|
||||||
|
[Key(5)] public EveInputProfileDocument[] DefaultProfiles { get; set; } = Array.Empty<EveInputProfileDocument>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveInputActionDocument
|
||||||
|
{
|
||||||
|
[Key(0)] public string ActionId { get; set; } = "";
|
||||||
|
[Key(1)] public string Label { get; set; } = "";
|
||||||
|
[Key(2)] public string Operation { get; set; } = "";
|
||||||
|
[Key(3)] public string Context { get; set; } = "pilot";
|
||||||
|
[Key(4)] public string Category { get; set; } = "ship";
|
||||||
|
[Key(5)] public string Availability { get; set; } = "available";
|
||||||
|
[Key(6)] public string SourceRef { get; set; } = "";
|
||||||
|
[Key(7)] public Dictionary<string, string> Payload { get; set; } = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
[Key(8)] public EveInputValueDocument? InputValue { get; set; }
|
||||||
|
[Key(9)] public string IconRef { get; set; } = "";
|
||||||
|
[Key(10)] public bool ActionBar { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveInputValueDocument
|
||||||
|
{
|
||||||
|
[Key(0)] public string Model { get; set; } = "";
|
||||||
|
[Key(1)] public string PayloadKey { get; set; } = "";
|
||||||
|
[Key(2)] public string[] PayloadKeys { get; set; } = Array.Empty<string>();
|
||||||
|
[Key(3)] public double? CurrentValue { get; set; }
|
||||||
|
[Key(4)] public double? MinimumValue { get; set; }
|
||||||
|
[Key(5)] public double? MaximumValue { get; set; }
|
||||||
|
[Key(6)] public double? StepValue { get; set; }
|
||||||
|
[Key(7)] public string Unit { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveInputProfileDocument
|
||||||
|
{
|
||||||
|
[Key(0)] public string ProfileId { get; set; } = "";
|
||||||
|
[Key(1)] public string DeviceClass { get; set; } = "";
|
||||||
|
[Key(2)] public EveInputBindingDocument[] Bindings { get; set; } = Array.Empty<EveInputBindingDocument>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveInputBindingDocument
|
||||||
|
{
|
||||||
|
[Key(0)] public string BindingId { get; set; } = "";
|
||||||
|
[Key(1)] public string ActionId { get; set; } = "";
|
||||||
|
[Key(2)] public EveInputGestureDocument Gesture { get; set; } = new EveInputGestureDocument();
|
||||||
|
[Key(3)] public bool ActionBar { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveInputGestureDocument
|
||||||
|
{
|
||||||
|
[Key(0)] public string Kind { get; set; } = "direct";
|
||||||
|
[Key(1)] public string[] Controls { get; set; } = Array.Empty<string>();
|
||||||
|
[Key(2)] public int MaxStepIntervalMs { get; set; } = 650;
|
||||||
|
[Key(3)] public string CompletionControl { get; set; } = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4c297c7dbb64a0f49a7f805e770c5e55
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace GameCult.Eve.Surface
|
||||||
|
{
|
||||||
|
public sealed class EveInputDeviceLayout
|
||||||
|
{
|
||||||
|
public string LayoutId { get; set; } = "";
|
||||||
|
public string DeviceClass { get; set; } = "";
|
||||||
|
public IReadOnlyList<EveInputLayoutControl> Controls { get; set; } = Array.Empty<EveInputLayoutControl>();
|
||||||
|
public bool SupportsDirectionalStrings { get; set; }
|
||||||
|
|
||||||
|
public static EveInputDeviceLayout AnsiKeyboardMouse() => new EveInputDeviceLayout
|
||||||
|
{
|
||||||
|
LayoutId = "keyboard.ansi-104+mouse.standard",
|
||||||
|
DeviceClass = "keyboard-mouse",
|
||||||
|
Controls = new[]
|
||||||
|
{
|
||||||
|
Key("keyboard.escape", "Esc", 0, 0, 1.25f), Key("keyboard.f1", "F1", 2, 0), Key("keyboard.f2", "F2", 3, 0),
|
||||||
|
Key("keyboard.1", "1", 1, 1), Key("keyboard.2", "2", 2, 1), Key("keyboard.3", "3", 3, 1), Key("keyboard.4", "4", 4, 1), Key("keyboard.5", "5", 5, 1),
|
||||||
|
Key("keyboard.tab", "Tab", 0, 2, 1.5f), Key("keyboard.q", "Q", 1.5f, 2), Key("keyboard.w", "W", 2.5f, 2), Key("keyboard.e", "E", 3.5f, 2), Key("keyboard.r", "R", 4.5f, 2),
|
||||||
|
Key("keyboard.a", "A", 1.75f, 3), Key("keyboard.s", "S", 2.75f, 3), Key("keyboard.d", "D", 3.75f, 3), Key("keyboard.f", "F", 4.75f, 3),
|
||||||
|
Key("keyboard.leftShift", "Shift", 0, 4, 2.25f), Key("keyboard.space", "Space", 3.75f, 5, 6.25f),
|
||||||
|
Key("mouse.primary", "Mouse 1", 18, 1), Key("mouse.secondary", "Mouse 2", 19, 1), Key("mouse.middle", "Mouse 3", 18.5f, 2)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static EveInputDeviceLayout StandardGamepad() => new EveInputDeviceLayout
|
||||||
|
{
|
||||||
|
LayoutId = "gamepad.standard",
|
||||||
|
DeviceClass = "gamepad",
|
||||||
|
SupportsDirectionalStrings = true,
|
||||||
|
Controls = new[]
|
||||||
|
{
|
||||||
|
Key("gamepad.leftStick", "Left Stick", 2, 3, 2), Key("gamepad.rightStick", "Right Stick", 8, 4, 2),
|
||||||
|
Key("gamepad.dpad.up", "Up", 1, 1), Key("gamepad.dpad.left", "Left", 0, 2), Key("gamepad.dpad.right", "Right", 2, 2), Key("gamepad.dpad.down", "Down", 1, 3),
|
||||||
|
Key("gamepad.buttonSouth", "A / Cross", 10, 3), Key("gamepad.buttonEast", "B / Circle", 11, 2), Key("gamepad.buttonWest", "X / Square", 9, 2), Key("gamepad.buttonNorth", "Y / Triangle", 10, 1),
|
||||||
|
Key("gamepad.leftTrigger", "LT / L2", 2, 0, 2), Key("gamepad.rightTrigger", "RT / R2", 9, 0, 2)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static EveInputLayoutControl Key(string id, string label, float x, float y, float width = 1) =>
|
||||||
|
new EveInputLayoutControl { Control = id, Label = label, X = x, Y = y, Width = width, Height = 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class EveInputLayoutControl
|
||||||
|
{
|
||||||
|
public string Control { get; set; } = "";
|
||||||
|
public string Label { get; set; } = "";
|
||||||
|
public float X { get; set; }
|
||||||
|
public float Y { get; set; }
|
||||||
|
public float Width { get; set; } = 1;
|
||||||
|
public float Height { get; set; } = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8a2a1258551e42a9831a3e4f9483d928
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace GameCult.Eve.Surface
|
||||||
|
{
|
||||||
|
public sealed class EveInputGesture
|
||||||
|
{
|
||||||
|
public string Kind { get; set; } = "direct";
|
||||||
|
public IReadOnlyList<string> Controls { get; set; } = Array.Empty<string>();
|
||||||
|
public int MaxStepIntervalMs { get; set; } = 650;
|
||||||
|
public string CompletionControl { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class EveInputBinding
|
||||||
|
{
|
||||||
|
public string BindingId { get; set; } = "";
|
||||||
|
public string ActionId { get; set; } = "";
|
||||||
|
public EveInputGesture Gesture { get; set; } = new EveInputGesture();
|
||||||
|
public bool ActionBar { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class EveInputGestureResolver
|
||||||
|
{
|
||||||
|
private readonly IReadOnlyList<EveInputBinding> _bindings;
|
||||||
|
private readonly HashSet<string> _pressed = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
private readonly Dictionary<string, SequenceState> _sequences = new Dictionary<string, SequenceState>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
public EveInputGestureResolver(IEnumerable<EveInputBinding> bindings)
|
||||||
|
{
|
||||||
|
_bindings = (bindings ?? Array.Empty<EveInputBinding>()).Where(binding => binding != null).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<string> Press(string control, long timestampMs)
|
||||||
|
{
|
||||||
|
_pressed.Add(control ?? "");
|
||||||
|
var actions = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
foreach (var binding in _bindings)
|
||||||
|
{
|
||||||
|
var gesture = binding.Gesture ?? new EveInputGesture();
|
||||||
|
if (gesture.Kind == "direct" && gesture.Controls.FirstOrDefault() == control)
|
||||||
|
actions.Add(binding.ActionId);
|
||||||
|
else if (gesture.Kind == "chord" && gesture.Controls.Count > 0 && gesture.Controls.All(_pressed.Contains))
|
||||||
|
actions.Add(binding.ActionId);
|
||||||
|
else if (gesture.Kind == "sequence" && Advance(binding, control ?? "", timestampMs))
|
||||||
|
actions.Add(binding.ActionId);
|
||||||
|
}
|
||||||
|
return actions.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Release(string control) => _pressed.Remove(control ?? "");
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
_pressed.Clear();
|
||||||
|
_sequences.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool Advance(EveInputBinding binding, string control, long timestampMs)
|
||||||
|
{
|
||||||
|
var controls = binding.Gesture.Controls;
|
||||||
|
if (controls.Count == 0) return false;
|
||||||
|
_sequences.TryGetValue(binding.BindingId, out var previous);
|
||||||
|
var index = previous.Index > 0 && timestampMs - previous.TimestampMs <= Math.Max(50, binding.Gesture.MaxStepIntervalMs) ? previous.Index : 0;
|
||||||
|
var next = controls[index] == control ? index + 1 : controls[0] == control ? 1 : 0;
|
||||||
|
if (next == controls.Count)
|
||||||
|
{
|
||||||
|
_sequences.Remove(binding.BindingId);
|
||||||
|
return string.IsNullOrWhiteSpace(binding.Gesture.CompletionControl);
|
||||||
|
}
|
||||||
|
_sequences[binding.BindingId] = new SequenceState(next, timestampMs);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly struct SequenceState
|
||||||
|
{
|
||||||
|
public SequenceState(int index, long timestampMs) { Index = index; TimestampMs = timestampMs; }
|
||||||
|
public int Index { get; }
|
||||||
|
public long TimestampMs { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e3c2941be243a754092ac2543d1e6bf7
|
||||||
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using GameCult.Mesh;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace GameCult.Eve.Surface
|
||||||
|
{
|
||||||
|
public static class EveInventoryInteraction
|
||||||
|
{
|
||||||
|
public const string GridKind = "inventory.grid";
|
||||||
|
public const string ItemKind = "inventory.item";
|
||||||
|
public const string DragSessionKind = "inventory.drag_session";
|
||||||
|
|
||||||
|
public static bool TryCreateDropRequest(
|
||||||
|
EveSurfaceDocument document,
|
||||||
|
EveSurfaceComponent source,
|
||||||
|
EveSurfaceComponent target,
|
||||||
|
int destinationX,
|
||||||
|
int destinationY,
|
||||||
|
string clientId,
|
||||||
|
out EveSurfaceCommandRequest? request)
|
||||||
|
{
|
||||||
|
request = null;
|
||||||
|
if (document == null || source == null || target == null ||
|
||||||
|
!string.Equals(source.Kind, ItemKind, StringComparison.Ordinal) ||
|
||||||
|
!string.Equals(target.Kind, GridKind, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceKind = source.GetProp("sourceKind", source.GetProp("source"));
|
||||||
|
var command = target.GetProp($"dropCommand.{sourceKind}", target.GetProp("dropCommand"));
|
||||||
|
if (string.IsNullOrWhiteSpace(sourceKind) || string.IsNullOrWhiteSpace(command))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var sourceIndex = Int(source.GetProp("sourceIndex"), -1);
|
||||||
|
var targetIndex = Int(target.GetProp("targetIndex"), -1);
|
||||||
|
var targetKind = target.GetProp("targetKind");
|
||||||
|
var payload = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["sourceKind"] = sourceKind,
|
||||||
|
["originEntityKey"] = source.GetProp("sourceEntityKey", source.GetProp("entityKey")),
|
||||||
|
["originIndex"] = sourceIndex.ToString(CultureInfo.InvariantCulture),
|
||||||
|
["originCargoIndex"] = (string.Equals(sourceKind, "cargo", StringComparison.Ordinal) ? sourceIndex : -1)
|
||||||
|
.ToString(CultureInfo.InvariantCulture),
|
||||||
|
["itemKey"] = source.GetProp("itemKey"),
|
||||||
|
["quantity"] = Math.Max(1, Int(source.GetProp("quantity"), 1)).ToString(CultureInfo.InvariantCulture),
|
||||||
|
["sourceX"] = Int(source.GetProp("x"), int.MinValue).ToString(CultureInfo.InvariantCulture),
|
||||||
|
["sourceY"] = Int(source.GetProp("y"), int.MinValue).ToString(CultureInfo.InvariantCulture),
|
||||||
|
["destinationKind"] = targetKind,
|
||||||
|
["destinationEntityKey"] = target.GetProp("targetEntityKey", target.GetProp("entityKey")),
|
||||||
|
["destinationIndex"] = targetIndex.ToString(CultureInfo.InvariantCulture),
|
||||||
|
["destinationCargoIndex"] = (string.Equals(targetKind, "cargo", StringComparison.Ordinal) ? targetIndex : -1)
|
||||||
|
.ToString(CultureInfo.InvariantCulture),
|
||||||
|
["destinationX"] = destinationX.ToString(CultureInfo.InvariantCulture),
|
||||||
|
["destinationY"] = destinationY.ToString(CultureInfo.InvariantCulture),
|
||||||
|
["hasDestinationPosition"] = "true"
|
||||||
|
};
|
||||||
|
var template = document.Commands.FirstOrDefault(candidate =>
|
||||||
|
string.Equals(candidate.Command, command, StringComparison.Ordinal));
|
||||||
|
var operation = template == null
|
||||||
|
? CultMesh.OperationInvocation(command)
|
||||||
|
: CultMesh.OperationInvocation(template.Operation);
|
||||||
|
request = new EveSurfaceCommandRequest(
|
||||||
|
document.ProviderId,
|
||||||
|
document.Surface.Id,
|
||||||
|
operation,
|
||||||
|
CultMesh.OperationPayload(payload),
|
||||||
|
DateTimeOffset.UtcNow,
|
||||||
|
string.IsNullOrWhiteSpace(clientId) ? "unity-uitoolkit" : clientId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Int(string value, int fallback) =>
|
||||||
|
int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)
|
||||||
|
? parsed
|
||||||
|
: fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 17fd4173e6e44b4b9c503d343021ffcb
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,416 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using GameCult.Caching;
|
||||||
|
using MessagePack;
|
||||||
|
using MessagePack.Formatters;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace GameCult.Eve.Surface
|
||||||
|
{
|
||||||
|
[CultDocument("gamecult.eve.provider_advertisement", SchemaId)]
|
||||||
|
[MessagePackObject]
|
||||||
|
[MessagePackFormatter(typeof(EveProviderAdvertisementCompatibilityFormatter))]
|
||||||
|
public sealed class EveProviderAdvertisementDocument
|
||||||
|
{
|
||||||
|
public const string SchemaId = "gamecult.eve.provider_advertisement.v1";
|
||||||
|
|
||||||
|
public EveProviderAdvertisementDocument(
|
||||||
|
string providerId,
|
||||||
|
string serviceId,
|
||||||
|
string verseId,
|
||||||
|
string title,
|
||||||
|
string kind,
|
||||||
|
string cultMeshAddress,
|
||||||
|
string updatedAtUtc,
|
||||||
|
EveProviderFreshness freshness,
|
||||||
|
IReadOnlyList<string> schemas,
|
||||||
|
IReadOnlyList<EveProviderWitness> witnesses,
|
||||||
|
IReadOnlyList<EveAdvertisedSurface> surfaces,
|
||||||
|
IReadOnlyList<EveAdvertisedCommand> commands,
|
||||||
|
IReadOnlyList<string>? authorizedBodyProducerIds = null)
|
||||||
|
: this(SchemaId, providerId, serviceId, verseId, title, kind, cultMeshAddress, updatedAtUtc, freshness, schemas, witnesses, surfaces, commands, authorizedBodyProducerIds)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EveProviderAdvertisementDocument(
|
||||||
|
string schema,
|
||||||
|
string providerId,
|
||||||
|
string serviceId,
|
||||||
|
string verseId,
|
||||||
|
string title,
|
||||||
|
string kind,
|
||||||
|
string cultMeshAddress,
|
||||||
|
string updatedAtUtc,
|
||||||
|
EveProviderFreshness freshness,
|
||||||
|
IReadOnlyList<string> schemas,
|
||||||
|
IReadOnlyList<EveProviderWitness> witnesses,
|
||||||
|
IReadOnlyList<EveAdvertisedSurface> surfaces,
|
||||||
|
IReadOnlyList<EveAdvertisedCommand> commands,
|
||||||
|
IReadOnlyList<string>? authorizedBodyProducerIds = null)
|
||||||
|
{
|
||||||
|
Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema;
|
||||||
|
ProviderId = providerId ?? "";
|
||||||
|
ServiceId = serviceId ?? "";
|
||||||
|
VerseId = verseId ?? "";
|
||||||
|
Title = title ?? "";
|
||||||
|
Kind = kind ?? "";
|
||||||
|
CultMeshAddress = cultMeshAddress ?? "";
|
||||||
|
UpdatedAtUtc = updatedAtUtc ?? "";
|
||||||
|
Freshness = freshness ?? new EveProviderFreshness("unknown", "", 0);
|
||||||
|
Schemas = schemas ?? Array.Empty<string>();
|
||||||
|
Witnesses = witnesses ?? Array.Empty<EveProviderWitness>();
|
||||||
|
Surfaces = surfaces ?? Array.Empty<EveAdvertisedSurface>();
|
||||||
|
Commands = commands ?? Array.Empty<EveAdvertisedCommand>();
|
||||||
|
AuthorizedBodyProducerIds = authorizedBodyProducerIds ?? Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)] public string Schema { get; }
|
||||||
|
[Key(1)] public string ProviderId { get; }
|
||||||
|
[Key(2)] public string ServiceId { get; }
|
||||||
|
[Key(3)] public string VerseId { get; }
|
||||||
|
[Key(4)] public string Title { get; }
|
||||||
|
[Key(5)] public string Kind { get; }
|
||||||
|
[Key(6)] public string CultMeshAddress { get; }
|
||||||
|
[Key(7)] public string UpdatedAtUtc { get; }
|
||||||
|
[Key(8)] public EveProviderFreshness Freshness { get; }
|
||||||
|
[Key(9)] public IReadOnlyList<string> Schemas { get; }
|
||||||
|
[Key(10)] public IReadOnlyList<EveProviderWitness> Witnesses { get; }
|
||||||
|
[Key(11)] public IReadOnlyList<EveAdvertisedSurface> Surfaces { get; }
|
||||||
|
[Key(12)] public IReadOnlyList<EveAdvertisedCommand> Commands { get; }
|
||||||
|
[Key(13)] public IReadOnlyList<string> AuthorizedBodyProducerIds { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads canonical provider advertisements and the original Aetheria-expanded v1 layout.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class EveProviderAdvertisementCompatibilityFormatter :
|
||||||
|
IMessagePackFormatter<EveProviderAdvertisementDocument?>
|
||||||
|
{
|
||||||
|
private const int CurrentFieldCount = 14;
|
||||||
|
private const int LegacyFieldCount = 16;
|
||||||
|
|
||||||
|
public void Serialize(
|
||||||
|
ref MessagePackWriter writer,
|
||||||
|
EveProviderAdvertisementDocument? value,
|
||||||
|
MessagePackSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value == null) { writer.WriteNil(); return; }
|
||||||
|
writer.WriteArrayHeader(CurrentFieldCount);
|
||||||
|
writer.Write(value.Schema);
|
||||||
|
writer.Write(value.ProviderId);
|
||||||
|
writer.Write(value.ServiceId);
|
||||||
|
writer.Write(value.VerseId);
|
||||||
|
writer.Write(value.Title);
|
||||||
|
writer.Write(value.Kind);
|
||||||
|
writer.Write(value.CultMeshAddress);
|
||||||
|
writer.Write(value.UpdatedAtUtc);
|
||||||
|
Formatter<EveProviderFreshness>(options).Serialize(ref writer, value.Freshness, options);
|
||||||
|
Formatter<IReadOnlyList<string>>(options).Serialize(ref writer, value.Schemas, options);
|
||||||
|
Formatter<IReadOnlyList<EveProviderWitness>>(options).Serialize(ref writer, value.Witnesses, options);
|
||||||
|
Formatter<IReadOnlyList<EveAdvertisedSurface>>(options).Serialize(ref writer, value.Surfaces, options);
|
||||||
|
Formatter<IReadOnlyList<EveAdvertisedCommand>>(options).Serialize(ref writer, value.Commands, options);
|
||||||
|
Formatter<IReadOnlyList<string>>(options).Serialize(ref writer, value.AuthorizedBodyProducerIds, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveProviderAdvertisementDocument? Deserialize(
|
||||||
|
ref MessagePackReader reader,
|
||||||
|
MessagePackSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TryReadNil()) return null;
|
||||||
|
if (reader.NextMessagePackType != MessagePackType.Array)
|
||||||
|
throw new MessagePackSerializationException("Eve provider advertisement must be an array.");
|
||||||
|
|
||||||
|
options.Security.DepthStep(ref reader);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fields = reader.ReadArrayHeader();
|
||||||
|
if (fields == LegacyFieldCount)
|
||||||
|
return ReadLegacy(ref reader, options);
|
||||||
|
|
||||||
|
var schema = ReadString(ref reader, fields, 0);
|
||||||
|
var providerId = ReadString(ref reader, fields, 1);
|
||||||
|
var serviceId = ReadString(ref reader, fields, 2);
|
||||||
|
var verseId = ReadString(ref reader, fields, 3);
|
||||||
|
var title = ReadString(ref reader, fields, 4);
|
||||||
|
var kind = ReadString(ref reader, fields, 5);
|
||||||
|
var address = ReadString(ref reader, fields, 6);
|
||||||
|
var updatedAtUtc = ReadString(ref reader, fields, 7);
|
||||||
|
var freshness = fields > 8 ? Formatter<EveProviderFreshness>(options).Deserialize(ref reader, options) : null;
|
||||||
|
var schemas = fields > 9 ? Formatter<IReadOnlyList<string>>(options).Deserialize(ref reader, options) : null;
|
||||||
|
var witnesses = fields > 10 ? Formatter<IReadOnlyList<EveProviderWitness>>(options).Deserialize(ref reader, options) : null;
|
||||||
|
var surfaces = fields > 11 ? Formatter<IReadOnlyList<EveAdvertisedSurface>>(options).Deserialize(ref reader, options) : null;
|
||||||
|
var commands = fields > 12 ? Formatter<IReadOnlyList<EveAdvertisedCommand>>(options).Deserialize(ref reader, options) : null;
|
||||||
|
var authorizedBodyProducerIds = fields > 13 ? Formatter<IReadOnlyList<string>>(options).Deserialize(ref reader, options) : null;
|
||||||
|
for (var index = CurrentFieldCount; index < fields; index++) reader.Skip();
|
||||||
|
return new EveProviderAdvertisementDocument(
|
||||||
|
schema, providerId, serviceId, verseId, title, kind, address, updatedAtUtc,
|
||||||
|
freshness ?? new EveProviderFreshness("unknown", "", 0),
|
||||||
|
schemas ?? Array.Empty<string>(),
|
||||||
|
witnesses ?? Array.Empty<EveProviderWitness>(),
|
||||||
|
surfaces ?? Array.Empty<EveAdvertisedSurface>(),
|
||||||
|
commands ?? Array.Empty<EveAdvertisedCommand>(),
|
||||||
|
authorizedBodyProducerIds ?? Array.Empty<string>());
|
||||||
|
}
|
||||||
|
finally { reader.Depth--; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EveProviderAdvertisementDocument ReadLegacy(
|
||||||
|
ref MessagePackReader reader,
|
||||||
|
MessagePackSerializerOptions options)
|
||||||
|
{
|
||||||
|
var schema = ReadString(ref reader);
|
||||||
|
var providerId = ReadString(ref reader);
|
||||||
|
var serviceId = ReadString(ref reader);
|
||||||
|
var verseId = ReadString(ref reader);
|
||||||
|
reader.Skip(); // RootVerse remains provider-owned settings, not portable advertisement state.
|
||||||
|
reader.Skip(); // CanonicalService
|
||||||
|
reader.Skip(); // LocatedService
|
||||||
|
var address = ReadString(ref reader);
|
||||||
|
var title = ReadString(ref reader);
|
||||||
|
var kind = ReadString(ref reader);
|
||||||
|
var updatedAtUtc = ReadString(ref reader);
|
||||||
|
var freshness = Formatter<EveProviderFreshness>(options).Deserialize(ref reader, options)
|
||||||
|
?? new EveProviderFreshness("unknown", "", 0);
|
||||||
|
var schemas = Formatter<IReadOnlyList<string>>(options).Deserialize(ref reader, options)
|
||||||
|
?? Array.Empty<string>();
|
||||||
|
var witnesses = Formatter<IReadOnlyList<EveProviderWitness>>(options).Deserialize(ref reader, options)
|
||||||
|
?? Array.Empty<EveProviderWitness>();
|
||||||
|
var surfaces = ReadLegacySurfaces(ref reader);
|
||||||
|
var commands = ReadLegacyCommands(ref reader);
|
||||||
|
return new EveProviderAdvertisementDocument(
|
||||||
|
schema, providerId, serviceId, verseId, title, kind, address, updatedAtUtc,
|
||||||
|
freshness, schemas, witnesses, surfaces, commands);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EveAdvertisedSurface[] ReadLegacySurfaces(ref MessagePackReader reader)
|
||||||
|
{
|
||||||
|
var count = reader.ReadArrayHeader();
|
||||||
|
var values = new EveAdvertisedSurface[count];
|
||||||
|
for (var index = 0; index < count; index++)
|
||||||
|
{
|
||||||
|
var fields = reader.ReadArrayHeader();
|
||||||
|
var schema = ReadString(ref reader, fields, 0);
|
||||||
|
var surfaceId = ReadString(ref reader, fields, 1);
|
||||||
|
var recordRef = ReadString(ref reader, fields, 2);
|
||||||
|
var transport = ReadString(ref reader, fields, 3);
|
||||||
|
var status = ReadString(ref reader, fields, 4);
|
||||||
|
for (var field = 5; field < fields; field++) reader.Skip();
|
||||||
|
values[index] = new EveAdvertisedSurface(
|
||||||
|
surfaceId, schema, recordRef, transport, status, "", null);
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EveAdvertisedCommand[] ReadLegacyCommands(ref MessagePackReader reader)
|
||||||
|
{
|
||||||
|
var count = reader.ReadArrayHeader();
|
||||||
|
var values = new EveAdvertisedCommand[count];
|
||||||
|
for (var index = 0; index < count; index++)
|
||||||
|
{
|
||||||
|
var fields = reader.ReadArrayHeader();
|
||||||
|
var command = ReadString(ref reader, fields, 0);
|
||||||
|
var transport = ReadString(ref reader, fields, 1);
|
||||||
|
var summary = ReadString(ref reader, fields, 2);
|
||||||
|
for (var field = 3; field < fields; field++) reader.Skip();
|
||||||
|
values[index] = new EveAdvertisedCommand(command, "", transport, summary);
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IMessagePackFormatter<T> Formatter<T>(MessagePackSerializerOptions options) =>
|
||||||
|
options.Resolver.GetFormatter<T>()
|
||||||
|
?? throw new MessagePackSerializationException($"No formatter is registered for {typeof(T).FullName}.");
|
||||||
|
|
||||||
|
private static string ReadString(ref MessagePackReader reader) => reader.ReadString() ?? "";
|
||||||
|
private static string ReadString(ref MessagePackReader reader, int fields, int index) =>
|
||||||
|
index < fields ? ReadString(ref reader) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveProviderFreshness
|
||||||
|
{
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EveProviderFreshness(string state, string lastSeenAtUtc, long maxAgeMs)
|
||||||
|
{
|
||||||
|
State = state ?? "";
|
||||||
|
LastSeenAtUtc = lastSeenAtUtc ?? "";
|
||||||
|
MaxAgeMs = maxAgeMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)] public string State { get; }
|
||||||
|
[Key(1)] public string LastSeenAtUtc { get; }
|
||||||
|
[Key(2)] public long MaxAgeMs { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveProviderWitness
|
||||||
|
{
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EveProviderWitness(string kind, string reference, string summary)
|
||||||
|
{
|
||||||
|
Kind = kind ?? "";
|
||||||
|
Reference = reference ?? "";
|
||||||
|
Summary = summary ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)] public string Kind { get; }
|
||||||
|
[Key(1)] public string Reference { get; }
|
||||||
|
[Key(2)] public string Summary { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveAdvertisedSurface
|
||||||
|
{
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EveAdvertisedSurface(
|
||||||
|
string surfaceId,
|
||||||
|
string schema,
|
||||||
|
string recordRef,
|
||||||
|
string transport,
|
||||||
|
string status,
|
||||||
|
string surfaceKind,
|
||||||
|
EveWorldInteractionAdvertisement? worldInteraction = null)
|
||||||
|
{
|
||||||
|
SurfaceId = surfaceId ?? "";
|
||||||
|
Schema = schema ?? "";
|
||||||
|
RecordRef = recordRef ?? "";
|
||||||
|
Transport = transport ?? "";
|
||||||
|
Status = status ?? "";
|
||||||
|
SurfaceKind = surfaceKind ?? "";
|
||||||
|
WorldInteraction = worldInteraction;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)] public string SurfaceId { get; }
|
||||||
|
[Key(1)] public string Schema { get; }
|
||||||
|
[Key(2)] public string RecordRef { get; }
|
||||||
|
[Key(3)] public string Transport { get; }
|
||||||
|
[Key(4)] public string Status { get; }
|
||||||
|
[Key(5)] public string SurfaceKind { get; }
|
||||||
|
[Key(6)] public EveWorldInteractionAdvertisement? WorldInteraction { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveWorldInteractionAdvertisement
|
||||||
|
{
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EveWorldInteractionAdvertisement(
|
||||||
|
string projectionKind,
|
||||||
|
IReadOnlyList<string> stateSchemas,
|
||||||
|
string commandBoundary,
|
||||||
|
string commandRecordRef,
|
||||||
|
string receiptSchema,
|
||||||
|
string receiptRecordRef,
|
||||||
|
string assetManifestRecordRef,
|
||||||
|
IReadOnlyList<string> loweringTargets,
|
||||||
|
string ownership)
|
||||||
|
{
|
||||||
|
ProjectionKind = projectionKind ?? "";
|
||||||
|
StateSchemas = stateSchemas ?? Array.Empty<string>();
|
||||||
|
CommandBoundary = commandBoundary ?? "";
|
||||||
|
CommandRecordRef = commandRecordRef ?? "";
|
||||||
|
ReceiptSchema = receiptSchema ?? "";
|
||||||
|
ReceiptRecordRef = receiptRecordRef ?? "";
|
||||||
|
AssetManifestRecordRef = assetManifestRecordRef ?? "";
|
||||||
|
LoweringTargets = loweringTargets ?? Array.Empty<string>();
|
||||||
|
Ownership = ownership ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)] public string ProjectionKind { get; }
|
||||||
|
[Key(1)] public IReadOnlyList<string> StateSchemas { get; }
|
||||||
|
[Key(2)] public string CommandBoundary { get; }
|
||||||
|
[Key(3)] public string CommandRecordRef { get; }
|
||||||
|
[Key(4)] public string ReceiptSchema { get; }
|
||||||
|
[Key(5)] public string ReceiptRecordRef { get; }
|
||||||
|
[Key(6)] public string AssetManifestRecordRef { get; }
|
||||||
|
[Key(7)] public IReadOnlyList<string> LoweringTargets { get; }
|
||||||
|
[Key(8)] public string Ownership { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveAdvertisedCommand
|
||||||
|
{
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EveAdvertisedCommand(string command, string surfaceId, string transport, string summary)
|
||||||
|
{
|
||||||
|
Command = command ?? "";
|
||||||
|
SurfaceId = surfaceId ?? "";
|
||||||
|
Transport = transport ?? "";
|
||||||
|
Summary = summary ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)] public string Command { get; }
|
||||||
|
[Key(1)] public string SurfaceId { get; }
|
||||||
|
[Key(2)] public string Transport { get; }
|
||||||
|
[Key(3)] public string Summary { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[CultDocument("gamecult.eve.command_receipt", SchemaId)]
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveCommandReceiptDocument
|
||||||
|
{
|
||||||
|
public const string SchemaId = "gamecult.eve.command_receipt.v1";
|
||||||
|
|
||||||
|
public EveCommandReceiptDocument(
|
||||||
|
string receiptId,
|
||||||
|
string commandId,
|
||||||
|
string command,
|
||||||
|
string state,
|
||||||
|
string ownerRepo,
|
||||||
|
string authority,
|
||||||
|
string providerId,
|
||||||
|
string surfaceId,
|
||||||
|
string message,
|
||||||
|
string issuedAtUtc,
|
||||||
|
long sourceVersion)
|
||||||
|
: this(SchemaId, receiptId, commandId, command, state, ownerRepo, authority, providerId, surfaceId, message, issuedAtUtc, sourceVersion)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EveCommandReceiptDocument(
|
||||||
|
string schema,
|
||||||
|
string receiptId,
|
||||||
|
string commandId,
|
||||||
|
string command,
|
||||||
|
string state,
|
||||||
|
string ownerRepo,
|
||||||
|
string authority,
|
||||||
|
string providerId,
|
||||||
|
string surfaceId,
|
||||||
|
string message,
|
||||||
|
string issuedAtUtc,
|
||||||
|
long sourceVersion)
|
||||||
|
{
|
||||||
|
Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema;
|
||||||
|
ReceiptId = receiptId ?? "";
|
||||||
|
CommandId = commandId ?? "";
|
||||||
|
Command = command ?? "";
|
||||||
|
State = state ?? "";
|
||||||
|
OwnerRepo = ownerRepo ?? "";
|
||||||
|
Authority = authority ?? "";
|
||||||
|
ProviderId = providerId ?? "";
|
||||||
|
SurfaceId = surfaceId ?? "";
|
||||||
|
Message = message ?? "";
|
||||||
|
IssuedAtUtc = issuedAtUtc ?? "";
|
||||||
|
SourceVersion = sourceVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)] public string Schema { get; }
|
||||||
|
[Key(1)] public string ReceiptId { get; }
|
||||||
|
[Key(2)] public string CommandId { get; }
|
||||||
|
[Key(3)] public string Command { get; }
|
||||||
|
[Key(4)] public string State { get; }
|
||||||
|
[Key(5)] public string OwnerRepo { get; }
|
||||||
|
[Key(6)] public string Authority { get; }
|
||||||
|
[Key(7)] public string ProviderId { get; }
|
||||||
|
[Key(8)] public string SurfaceId { get; }
|
||||||
|
[Key(9)] public string Message { get; }
|
||||||
|
[Key(10)] public string IssuedAtUtc { get; }
|
||||||
|
[Key(11)] public long SourceVersion { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f7ea0cd07c4d5da4099b5acf8d44ca2e
|
||||||
|
|
||||||
876
packages/org.gamecult.eve.surface/Runtime/EveSurfaceBuilder.cs
Normal file
876
packages/org.gamecult.eve.surface/Runtime/EveSurfaceBuilder.cs
Normal file
|
|
@ -0,0 +1,876 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using GameCult.Mesh;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace GameCult.Eve.Surface
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Fluent CultUI/Eve surface builder. Eve owns the surface document; CultMesh only supplies typed binding descriptors.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class EveSurfaceBuilder
|
||||||
|
{
|
||||||
|
private readonly string _surfaceId;
|
||||||
|
private readonly List<EveSurfaceComponent> _children = new();
|
||||||
|
private readonly List<EveStyleToken> _styles = new();
|
||||||
|
private readonly List<EveCommandTemplate> _commands = new();
|
||||||
|
private EveSurfaceComponent? _root;
|
||||||
|
private long _version = 1;
|
||||||
|
private string _providerId = "gamecult";
|
||||||
|
private string _providerKind = "cultui.surface";
|
||||||
|
private string _title = "";
|
||||||
|
private string _updatedAtUtc = "";
|
||||||
|
|
||||||
|
public EveSurfaceBuilder(string surfaceId)
|
||||||
|
{
|
||||||
|
_surfaceId = RequireNonEmpty(surfaceId, nameof(surfaceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceBuilder Provider(string providerId, string providerKind)
|
||||||
|
{
|
||||||
|
_providerId = providerId ?? "";
|
||||||
|
_providerKind = providerKind ?? "";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceBuilder Title(string title)
|
||||||
|
{
|
||||||
|
_title = title ?? "";
|
||||||
|
_children.Add(Text($"{_surfaceId}.title", _title, "text.title"));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceBuilder TitleSubtitle(string title, string subtitle)
|
||||||
|
{
|
||||||
|
_title = string.IsNullOrWhiteSpace(subtitle) ? title ?? "" : $"{title} {subtitle}";
|
||||||
|
_children.Add(Text($"{_surfaceId}.title", title ?? "", "text.title"));
|
||||||
|
_children.Add(Text($"{_surfaceId}.subtitle", subtitle ?? "", "text.subtitle"));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceBuilder Text(string text, string? id = null)
|
||||||
|
{
|
||||||
|
_children.Add(Text(id ?? $"{_surfaceId}.text.{_children.Count}", text, "text"));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceBuilder Button(string label, string command)
|
||||||
|
{
|
||||||
|
var operation = CultMesh.OperationBinding(command, label);
|
||||||
|
_commands.Add(new EveCommandTemplate(operation));
|
||||||
|
_children.Add(ButtonComponent($"{_surfaceId}.button.{Slug(label)}", label, operation));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceBuilder Button(string label, CultMeshOperationBindingDescriptor operation)
|
||||||
|
{
|
||||||
|
if (operation == null) throw new ArgumentNullException(nameof(operation));
|
||||||
|
_commands.Add(new EveCommandTemplate(operation));
|
||||||
|
_children.Add(ButtonComponent($"{_surfaceId}.button.{Slug(label)}", label, operation));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceBuilder ButtonColumn(string id, Action<EveSurfaceGroupBuilder> build)
|
||||||
|
{
|
||||||
|
return Group(id, "column", build);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceBuilder ButtonRow(string id, Action<EveSurfaceGroupBuilder> build)
|
||||||
|
{
|
||||||
|
return Group(id, "row", build);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceBuilder Form(string id, Action<EveSurfaceFormBuilder> build)
|
||||||
|
{
|
||||||
|
if (build == null) throw new ArgumentNullException(nameof(build));
|
||||||
|
var form = new EveSurfaceFormBuilder(id, AddCommand);
|
||||||
|
build(form);
|
||||||
|
_children.Add(form.Build());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceBuilder RootColumn(string id, Action<EveSurfaceContainerBuilder> build)
|
||||||
|
{
|
||||||
|
_root = BuildContainer(id, "partition", Props(("split", "y")), build);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceBuilder RootRow(string id, Action<EveSurfaceContainerBuilder> build)
|
||||||
|
{
|
||||||
|
_root = BuildContainer(id, "partition", Props(("split", "x")), build);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceBuilder RootGrid(
|
||||||
|
string id,
|
||||||
|
int columns,
|
||||||
|
int rows,
|
||||||
|
Action<EveSurfaceContainerBuilder> build)
|
||||||
|
{
|
||||||
|
_root = BuildContainer(
|
||||||
|
id,
|
||||||
|
"grid",
|
||||||
|
Props(
|
||||||
|
("columns", columns.ToString(System.Globalization.CultureInfo.InvariantCulture)),
|
||||||
|
("rows", rows.ToString(System.Globalization.CultureInfo.InvariantCulture))),
|
||||||
|
build);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceBuilder Row(string id, Action<EveSurfaceContainerBuilder> build)
|
||||||
|
{
|
||||||
|
_children.Add(BuildContainer(id, "partition", Props(("split", "x")), build));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceBuilder Column(string id, Action<EveSurfaceContainerBuilder> build)
|
||||||
|
{
|
||||||
|
_children.Add(BuildContainer(id, "partition", Props(("split", "y")), build));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceBuilder Grid(string id, int columns, int rows, Action<EveSurfaceContainerBuilder> build)
|
||||||
|
{
|
||||||
|
_children.Add(BuildContainer(
|
||||||
|
id,
|
||||||
|
"grid",
|
||||||
|
Props(
|
||||||
|
("columns", columns.ToString(System.Globalization.CultureInfo.InvariantCulture)),
|
||||||
|
("rows", rows.ToString(System.Globalization.CultureInfo.InvariantCulture))),
|
||||||
|
build));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceBuilder Modal(string id, string title, Action<EveSurfaceContainerBuilder> build)
|
||||||
|
{
|
||||||
|
_children.Add(BuildContainer(id, "modal", Props(("title", title ?? "")), build));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceBuilder EmbeddedDocument(
|
||||||
|
string slotId,
|
||||||
|
string documentId,
|
||||||
|
string schemaId,
|
||||||
|
string presentationKind,
|
||||||
|
CultMeshRouteHint? routeHint = null)
|
||||||
|
{
|
||||||
|
_children.Add(new EveSurfaceComponent(
|
||||||
|
$"{_surfaceId}.slot.{Slug(slotId)}",
|
||||||
|
"surface.slot",
|
||||||
|
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["slotId"] = slotId ?? "",
|
||||||
|
["documentId"] = documentId ?? "",
|
||||||
|
["schemaId"] = schemaId ?? "",
|
||||||
|
["presentationKind"] = presentationKind ?? ""
|
||||||
|
},
|
||||||
|
Array.Empty<EveSurfaceComponent>(),
|
||||||
|
Array.Empty<CultMeshStateBindingDescriptor>(),
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new EveEmbeddedDocumentSlot(
|
||||||
|
slotId ?? "",
|
||||||
|
documentId ?? "",
|
||||||
|
schemaId ?? "",
|
||||||
|
presentationKind ?? "",
|
||||||
|
routeHint)
|
||||||
|
}));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceBuilder Style(string name, string value)
|
||||||
|
{
|
||||||
|
_styles.Add(new EveStyleToken(name, value));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceBuilder Version(long version)
|
||||||
|
{
|
||||||
|
_version = version;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceBuilder UpdatedAtUtc(string updatedAtUtc)
|
||||||
|
{
|
||||||
|
_updatedAtUtc = updatedAtUtc ?? "";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceDocument Build()
|
||||||
|
{
|
||||||
|
return new EveSurfaceDocument(
|
||||||
|
_providerId,
|
||||||
|
_providerKind,
|
||||||
|
_title,
|
||||||
|
_version,
|
||||||
|
string.IsNullOrWhiteSpace(_updatedAtUtc) ? DateTime.UtcNow.ToString("O") : _updatedAtUtc,
|
||||||
|
new EveSurfaceTree(
|
||||||
|
_surfaceId,
|
||||||
|
_root ?? new EveSurfaceComponent(
|
||||||
|
$"{_surfaceId}.root",
|
||||||
|
"surface",
|
||||||
|
EmptyProps(),
|
||||||
|
_children.ToArray()),
|
||||||
|
_styles.ToArray()),
|
||||||
|
_commands.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private EveSurfaceBuilder Group(string id, string kind, Action<EveSurfaceGroupBuilder> build)
|
||||||
|
{
|
||||||
|
if (build == null) throw new ArgumentNullException(nameof(build));
|
||||||
|
var group = new EveSurfaceGroupBuilder(id, kind, AddCommand);
|
||||||
|
build(group);
|
||||||
|
_children.Add(group.Build());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EveSurfaceComponent BuildContainer(
|
||||||
|
string id,
|
||||||
|
string kind,
|
||||||
|
IReadOnlyDictionary<string, string> props,
|
||||||
|
Action<EveSurfaceContainerBuilder> build)
|
||||||
|
{
|
||||||
|
if (build == null) throw new ArgumentNullException(nameof(build));
|
||||||
|
var container = new EveSurfaceContainerBuilder(id, kind, props, AddCommand);
|
||||||
|
build(container);
|
||||||
|
return container.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddCommand(EveCommandTemplate command)
|
||||||
|
{
|
||||||
|
if (command != null)
|
||||||
|
_commands.Add(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static EveSurfaceComponent Text(string id, string value, string kind)
|
||||||
|
{
|
||||||
|
return new EveSurfaceComponent(
|
||||||
|
id,
|
||||||
|
kind,
|
||||||
|
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["value"] = value ?? ""
|
||||||
|
},
|
||||||
|
Array.Empty<EveSurfaceComponent>());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static EveSurfaceComponent ButtonComponent(
|
||||||
|
string id,
|
||||||
|
string label,
|
||||||
|
CultMeshOperationBindingDescriptor operation)
|
||||||
|
{
|
||||||
|
return new EveSurfaceComponent(
|
||||||
|
id,
|
||||||
|
"control.button",
|
||||||
|
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["label"] = label ?? operation.Label,
|
||||||
|
["command"] = operation.OperationId,
|
||||||
|
["operationId"] = operation.OperationId,
|
||||||
|
["schemaId"] = operation.SchemaId
|
||||||
|
},
|
||||||
|
Array.Empty<EveSurfaceComponent>());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Dictionary<string, string> EmptyProps()
|
||||||
|
{
|
||||||
|
return new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Dictionary<string, string> Props(params (string Key, string Value)[] values)
|
||||||
|
{
|
||||||
|
return (values ?? Array.Empty<(string Key, string Value)>())
|
||||||
|
.ToDictionary(value => value.Key, value => value.Value ?? "", StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string Slug(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return "unnamed";
|
||||||
|
|
||||||
|
var chars = value
|
||||||
|
.Select(character => char.IsLetterOrDigit(character) ? char.ToLowerInvariant(character) : '.')
|
||||||
|
.ToArray();
|
||||||
|
var slug = new string(chars).Trim('.');
|
||||||
|
while (slug.Contains("..", StringComparison.Ordinal))
|
||||||
|
slug = slug.Replace("..", ".", StringComparison.Ordinal);
|
||||||
|
return string.IsNullOrWhiteSpace(slug) ? "unnamed" : slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RequireNonEmpty(string value, string paramName)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(value)
|
||||||
|
? throw new ArgumentException("Value must be non-empty.", paramName)
|
||||||
|
: value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class EveSurfaceContainerBuilder
|
||||||
|
{
|
||||||
|
private readonly string _id;
|
||||||
|
private readonly string _kind;
|
||||||
|
private readonly Dictionary<string, string> _props;
|
||||||
|
private readonly Action<EveCommandTemplate> _addCommand;
|
||||||
|
private readonly List<EveSurfaceComponent> _children = new();
|
||||||
|
private readonly List<CultMeshStateBindingDescriptor> _stateBindings = new();
|
||||||
|
private readonly List<EveEmbeddedDocumentSlot> _embeddedDocuments = new();
|
||||||
|
|
||||||
|
internal EveSurfaceContainerBuilder(
|
||||||
|
string id,
|
||||||
|
string kind,
|
||||||
|
IReadOnlyDictionary<string, string> props,
|
||||||
|
Action<EveCommandTemplate> addCommand)
|
||||||
|
{
|
||||||
|
_id = id ?? "";
|
||||||
|
_kind = kind ?? "partition";
|
||||||
|
_props = new Dictionary<string, string>(props ?? EveSurfaceBuilder.EmptyProps(), StringComparer.Ordinal);
|
||||||
|
_addCommand = addCommand ?? throw new ArgumentNullException(nameof(addCommand));
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Gap(int value) => Prop("gap", value);
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Padding(int value) => Prop("padding", value);
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Size(string value) => Prop("size", value);
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Min(string value) => Prop("min", value);
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Max(string value) => Prop("max", value);
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Align(string value) => Prop("align", value);
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Clip(string value = "hidden") => Prop("clip", value);
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Scroll(string value = "y") => Prop("scroll", value);
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Region(string value) => Prop("region", value);
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Bind(CultMeshStateBindingDescriptor binding)
|
||||||
|
{
|
||||||
|
if (binding != null)
|
||||||
|
_stateBindings.Add(binding);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Row(string id, Action<EveSurfaceContainerBuilder> build)
|
||||||
|
{
|
||||||
|
_children.Add(BuildContainer(id, "partition", EveSurfaceBuilder.Props(("split", "x")), build));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Column(string id, Action<EveSurfaceContainerBuilder> build)
|
||||||
|
{
|
||||||
|
_children.Add(BuildContainer(id, "partition", EveSurfaceBuilder.Props(("split", "y")), build));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Grid(
|
||||||
|
string id,
|
||||||
|
int columns,
|
||||||
|
int rows,
|
||||||
|
Action<EveSurfaceContainerBuilder> build)
|
||||||
|
{
|
||||||
|
_children.Add(BuildContainer(
|
||||||
|
id,
|
||||||
|
"grid",
|
||||||
|
EveSurfaceBuilder.Props(
|
||||||
|
("columns", columns.ToString(System.Globalization.CultureInfo.InvariantCulture)),
|
||||||
|
("rows", rows.ToString(System.Globalization.CultureInfo.InvariantCulture))),
|
||||||
|
build));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Pane(string id, string title, Action<EveSurfaceContainerBuilder> build)
|
||||||
|
{
|
||||||
|
_children.Add(BuildContainer(id, "pane", EveSurfaceBuilder.Props(("title", title ?? "")), build));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Card(string id, string title, Action<EveSurfaceContainerBuilder> build)
|
||||||
|
{
|
||||||
|
_children.Add(BuildContainer(id, "card", EveSurfaceBuilder.Props(("title", title ?? "")), build));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Modal(string id, string title, Action<EveSurfaceContainerBuilder> build)
|
||||||
|
{
|
||||||
|
_children.Add(BuildContainer(id, "modal", EveSurfaceBuilder.Props(("title", title ?? "")), build));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder FieldRow(
|
||||||
|
string id,
|
||||||
|
string label,
|
||||||
|
Action<EveSurfaceContainerBuilder> build)
|
||||||
|
{
|
||||||
|
return Row(id, row => row
|
||||||
|
.Gap(8)
|
||||||
|
.Label($"{id}.label", label, labelPart => labelPart.Size("12rem"))
|
||||||
|
.Column($"{id}.field", field =>
|
||||||
|
{
|
||||||
|
field.Size("1fr");
|
||||||
|
build(field);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Label(
|
||||||
|
string id,
|
||||||
|
string text,
|
||||||
|
Action<EveSurfaceContainerBuilder>? configure = null)
|
||||||
|
{
|
||||||
|
return Leaf(id, "label", EveSurfaceBuilder.Props(("value", text ?? "")), null, null, configure);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Text(string id, string text)
|
||||||
|
{
|
||||||
|
return Leaf(id, "text", EveSurfaceBuilder.Props(("value", text ?? "")));
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Title(string id, string text)
|
||||||
|
{
|
||||||
|
return Leaf(id, "text.title", EveSurfaceBuilder.Props(("value", text ?? "")));
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Metric(
|
||||||
|
string id,
|
||||||
|
string label,
|
||||||
|
string value,
|
||||||
|
CultMeshStateBindingDescriptor? binding = null)
|
||||||
|
{
|
||||||
|
return Leaf(
|
||||||
|
id,
|
||||||
|
"metric",
|
||||||
|
EveSurfaceBuilder.Props(("label", label ?? ""), ("value", value ?? "")),
|
||||||
|
binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Progress(
|
||||||
|
string id,
|
||||||
|
string label,
|
||||||
|
string value,
|
||||||
|
string min,
|
||||||
|
string max,
|
||||||
|
CultMeshStateBindingDescriptor? binding = null)
|
||||||
|
{
|
||||||
|
return Leaf(
|
||||||
|
id,
|
||||||
|
"progress",
|
||||||
|
EveSurfaceBuilder.Props(
|
||||||
|
("label", label ?? ""),
|
||||||
|
("value", value ?? ""),
|
||||||
|
("min", min ?? ""),
|
||||||
|
("max", max ?? "")),
|
||||||
|
binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Button(string id, string label, string command)
|
||||||
|
{
|
||||||
|
return Button(id, label, CultMesh.OperationBinding(command, label));
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Button(
|
||||||
|
string id,
|
||||||
|
string label,
|
||||||
|
CultMeshOperationBindingDescriptor operation)
|
||||||
|
{
|
||||||
|
if (operation == null) throw new ArgumentNullException(nameof(operation));
|
||||||
|
_addCommand(new EveCommandTemplate(operation));
|
||||||
|
_children.Add(EveSurfaceBuilder.ButtonComponent(id, label, operation));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder PopupButton(
|
||||||
|
string id,
|
||||||
|
string label,
|
||||||
|
string title,
|
||||||
|
Action<EveSurfaceContainerBuilder> build,
|
||||||
|
string anchor = "viewport",
|
||||||
|
string placement = "center",
|
||||||
|
int offsetX = 0,
|
||||||
|
int offsetY = 0)
|
||||||
|
{
|
||||||
|
if (build == null) throw new ArgumentNullException(nameof(build));
|
||||||
|
var popup = BuildContainer(
|
||||||
|
$"{id}.popup",
|
||||||
|
"modal",
|
||||||
|
EveSurfaceBuilder.Props(
|
||||||
|
("title", title ?? label ?? ""),
|
||||||
|
("anchor", anchor ?? "viewport"),
|
||||||
|
("placement", placement ?? "center"),
|
||||||
|
("offsetX", offsetX.ToString(System.Globalization.CultureInfo.InvariantCulture)),
|
||||||
|
("offsetY", offsetY.ToString(System.Globalization.CultureInfo.InvariantCulture)),
|
||||||
|
("presentation", "popover")),
|
||||||
|
build);
|
||||||
|
_children.Add(new EveSurfaceComponent(
|
||||||
|
id ?? "",
|
||||||
|
"control.popup",
|
||||||
|
EveSurfaceBuilder.Props(
|
||||||
|
("label", label ?? ""),
|
||||||
|
("title", title ?? label ?? ""),
|
||||||
|
("anchor", anchor ?? "viewport"),
|
||||||
|
("placement", placement ?? "center"),
|
||||||
|
("offsetX", offsetX.ToString(System.Globalization.CultureInfo.InvariantCulture)),
|
||||||
|
("offsetY", offsetY.ToString(System.Globalization.CultureInfo.InvariantCulture))),
|
||||||
|
new[] { popup }));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder EnumField(
|
||||||
|
string id,
|
||||||
|
string label,
|
||||||
|
string value,
|
||||||
|
CultMeshStateBindingDescriptor optionsBinding,
|
||||||
|
CultMeshOperationBindingDescriptor setValue,
|
||||||
|
string anchor = "trigger",
|
||||||
|
string placement = "right-down")
|
||||||
|
{
|
||||||
|
return FieldRow(id, label, field => field.PopupButton(
|
||||||
|
$"{id}.dropdown",
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
popup => popup.OptionList($"{id}.options", label, optionsBinding, setValue),
|
||||||
|
anchor,
|
||||||
|
placement));
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder OptionList(
|
||||||
|
string id,
|
||||||
|
string label,
|
||||||
|
CultMeshStateBindingDescriptor itemsBinding,
|
||||||
|
CultMeshOperationBindingDescriptor operation)
|
||||||
|
{
|
||||||
|
if (itemsBinding == null) throw new ArgumentNullException(nameof(itemsBinding));
|
||||||
|
if (operation == null) throw new ArgumentNullException(nameof(operation));
|
||||||
|
_addCommand(new EveCommandTemplate(operation));
|
||||||
|
_children.Add(new EveSurfaceComponent(
|
||||||
|
id ?? "",
|
||||||
|
"options",
|
||||||
|
EveSurfaceBuilder.Props(
|
||||||
|
("label", label ?? ""),
|
||||||
|
("command", operation.OperationId),
|
||||||
|
("operationId", operation.OperationId),
|
||||||
|
("schemaId", operation.SchemaId)),
|
||||||
|
Array.Empty<EveSurfaceComponent>(),
|
||||||
|
new[] { itemsBinding }));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder TextInput(
|
||||||
|
string id,
|
||||||
|
string label,
|
||||||
|
string value,
|
||||||
|
CultMeshOperationBindingDescriptor operation,
|
||||||
|
CultMeshStateBindingDescriptor? binding = null)
|
||||||
|
{
|
||||||
|
return Control("control.input.text", id, label, value, operation, binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Toggle(
|
||||||
|
string id,
|
||||||
|
string label,
|
||||||
|
bool value,
|
||||||
|
CultMeshOperationBindingDescriptor operation,
|
||||||
|
CultMeshStateBindingDescriptor? binding = null)
|
||||||
|
{
|
||||||
|
return Control("control.toggle", id, label, value ? "true" : "false", operation, binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Slider(
|
||||||
|
string id,
|
||||||
|
string label,
|
||||||
|
string value,
|
||||||
|
string min,
|
||||||
|
string max,
|
||||||
|
string step,
|
||||||
|
CultMeshOperationBindingDescriptor operation,
|
||||||
|
CultMeshStateBindingDescriptor? binding = null)
|
||||||
|
{
|
||||||
|
return Control(
|
||||||
|
"control.slider",
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
operation,
|
||||||
|
binding,
|
||||||
|
("min", min ?? ""),
|
||||||
|
("max", max ?? ""),
|
||||||
|
("step", step ?? ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder Select(
|
||||||
|
string id,
|
||||||
|
string label,
|
||||||
|
string value,
|
||||||
|
CultMeshOperationBindingDescriptor operation,
|
||||||
|
CultMeshStateBindingDescriptor? binding = null)
|
||||||
|
{
|
||||||
|
return Control("control.select", id, label, value, operation, binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceContainerBuilder EmbeddedDocument(
|
||||||
|
string id,
|
||||||
|
string slotId,
|
||||||
|
string documentId,
|
||||||
|
string schemaId,
|
||||||
|
string presentationKind,
|
||||||
|
CultMeshRouteHint? routeHint = null)
|
||||||
|
{
|
||||||
|
_children.Add(new EveSurfaceComponent(
|
||||||
|
id ?? "",
|
||||||
|
"surface.slot",
|
||||||
|
EveSurfaceBuilder.Props(
|
||||||
|
("slotId", slotId ?? ""),
|
||||||
|
("documentId", documentId ?? ""),
|
||||||
|
("schemaId", schemaId ?? ""),
|
||||||
|
("presentationKind", presentationKind ?? "")),
|
||||||
|
Array.Empty<EveSurfaceComponent>(),
|
||||||
|
Array.Empty<CultMeshStateBindingDescriptor>(),
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new EveEmbeddedDocumentSlot(
|
||||||
|
slotId ?? "",
|
||||||
|
documentId ?? "",
|
||||||
|
schemaId ?? "",
|
||||||
|
presentationKind ?? "",
|
||||||
|
routeHint)
|
||||||
|
}));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal EveSurfaceComponent Build()
|
||||||
|
{
|
||||||
|
return new EveSurfaceComponent(
|
||||||
|
_id,
|
||||||
|
_kind,
|
||||||
|
_props,
|
||||||
|
_children.ToArray(),
|
||||||
|
_stateBindings.ToArray(),
|
||||||
|
_embeddedDocuments.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private EveSurfaceContainerBuilder Prop(string key, int value)
|
||||||
|
{
|
||||||
|
_props[key] = value.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EveSurfaceContainerBuilder Prop(string key, string value)
|
||||||
|
{
|
||||||
|
_props[key] = value ?? "";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EveSurfaceContainerBuilder Leaf(
|
||||||
|
string id,
|
||||||
|
string kind,
|
||||||
|
IReadOnlyDictionary<string, string> props,
|
||||||
|
CultMeshStateBindingDescriptor? binding = null,
|
||||||
|
IReadOnlyList<EveSurfaceComponent>? children = null,
|
||||||
|
Action<EveSurfaceContainerBuilder>? configure = null)
|
||||||
|
{
|
||||||
|
var builder = new EveSurfaceContainerBuilder(id, kind, props, _addCommand);
|
||||||
|
configure?.Invoke(builder);
|
||||||
|
var component = new EveSurfaceComponent(
|
||||||
|
id ?? "",
|
||||||
|
kind ?? "",
|
||||||
|
new Dictionary<string, string>(props ?? EveSurfaceBuilder.EmptyProps(), StringComparer.Ordinal),
|
||||||
|
children ?? Array.Empty<EveSurfaceComponent>(),
|
||||||
|
binding == null ? Array.Empty<CultMeshStateBindingDescriptor>() : new[] { binding });
|
||||||
|
if (builder._props.Count != (props?.Count ?? 0))
|
||||||
|
{
|
||||||
|
component = new EveSurfaceComponent(
|
||||||
|
id ?? "",
|
||||||
|
kind ?? "",
|
||||||
|
new Dictionary<string, string>(builder._props, StringComparer.Ordinal),
|
||||||
|
children ?? Array.Empty<EveSurfaceComponent>(),
|
||||||
|
binding == null ? Array.Empty<CultMeshStateBindingDescriptor>() : new[] { binding });
|
||||||
|
}
|
||||||
|
_children.Add(component);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EveSurfaceContainerBuilder Control(
|
||||||
|
string kind,
|
||||||
|
string id,
|
||||||
|
string label,
|
||||||
|
string value,
|
||||||
|
CultMeshOperationBindingDescriptor operation,
|
||||||
|
CultMeshStateBindingDescriptor? binding,
|
||||||
|
params (string Key, string Value)[] extraProps)
|
||||||
|
{
|
||||||
|
if (operation == null) throw new ArgumentNullException(nameof(operation));
|
||||||
|
_addCommand(new EveCommandTemplate(operation));
|
||||||
|
var props = EveSurfaceBuilder.Props(
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
("label", label ?? ""),
|
||||||
|
("value", value ?? ""),
|
||||||
|
("command", operation.OperationId),
|
||||||
|
("operationId", operation.OperationId),
|
||||||
|
("schemaId", operation.SchemaId)
|
||||||
|
}.Concat(extraProps ?? Array.Empty<(string Key, string Value)>()).ToArray());
|
||||||
|
return Leaf(kind: kind, id: id, props: props, binding: binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
private EveSurfaceComponent BuildContainer(
|
||||||
|
string id,
|
||||||
|
string kind,
|
||||||
|
IReadOnlyDictionary<string, string> props,
|
||||||
|
Action<EveSurfaceContainerBuilder> build)
|
||||||
|
{
|
||||||
|
if (build == null) throw new ArgumentNullException(nameof(build));
|
||||||
|
var container = new EveSurfaceContainerBuilder(id, kind, props, _addCommand);
|
||||||
|
build(container);
|
||||||
|
return container.Build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class EveSurfaceGroupBuilder
|
||||||
|
{
|
||||||
|
private readonly string _id;
|
||||||
|
private readonly string _kind;
|
||||||
|
private readonly Action<EveCommandTemplate> _addCommand;
|
||||||
|
private readonly List<EveSurfaceComponent> _children = new();
|
||||||
|
|
||||||
|
internal EveSurfaceGroupBuilder(
|
||||||
|
string id,
|
||||||
|
string kind,
|
||||||
|
Action<EveCommandTemplate> addCommand)
|
||||||
|
{
|
||||||
|
_id = id ?? "";
|
||||||
|
_kind = kind ?? "column";
|
||||||
|
_addCommand = addCommand ?? throw new ArgumentNullException(nameof(addCommand));
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceGroupBuilder Button(string label, string command)
|
||||||
|
{
|
||||||
|
var operation = CultMesh.OperationBinding(command, label);
|
||||||
|
_addCommand(new EveCommandTemplate(operation));
|
||||||
|
_children.Add(EveSurfaceBuilder.ButtonComponent($"{_id}.button.{EveSurfaceBuilder.Slug(label)}", label, operation));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceGroupBuilder Button(string label, CultMeshOperationBindingDescriptor operation)
|
||||||
|
{
|
||||||
|
if (operation == null) throw new ArgumentNullException(nameof(operation));
|
||||||
|
_addCommand(new EveCommandTemplate(operation));
|
||||||
|
_children.Add(EveSurfaceBuilder.ButtonComponent($"{_id}.button.{EveSurfaceBuilder.Slug(label)}", label, operation));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal EveSurfaceComponent Build()
|
||||||
|
{
|
||||||
|
return new EveSurfaceComponent(_id, _kind, EveSurfaceBuilder.EmptyProps(), _children.ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class EveSurfaceFormBuilder
|
||||||
|
{
|
||||||
|
private readonly string _id;
|
||||||
|
private readonly Action<EveCommandTemplate> _addCommand;
|
||||||
|
private readonly List<EveSurfaceComponent> _children = new();
|
||||||
|
|
||||||
|
internal EveSurfaceFormBuilder(string id, Action<EveCommandTemplate> addCommand)
|
||||||
|
{
|
||||||
|
_id = id ?? "";
|
||||||
|
_addCommand = addCommand ?? throw new ArgumentNullException(nameof(addCommand));
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceFormBuilder Text(
|
||||||
|
string label,
|
||||||
|
string value,
|
||||||
|
CultMeshOperationBindingDescriptor operation,
|
||||||
|
CultMeshStateBindingDescriptor? binding = null)
|
||||||
|
{
|
||||||
|
if (operation == null) throw new ArgumentNullException(nameof(operation));
|
||||||
|
_addCommand(new EveCommandTemplate(operation));
|
||||||
|
_children.Add(Control(
|
||||||
|
"control.text",
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
operation,
|
||||||
|
binding));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceFormBuilder Toggle(
|
||||||
|
string label,
|
||||||
|
bool value,
|
||||||
|
CultMeshOperationBindingDescriptor operation,
|
||||||
|
CultMeshStateBindingDescriptor? binding = null)
|
||||||
|
{
|
||||||
|
if (operation == null) throw new ArgumentNullException(nameof(operation));
|
||||||
|
_addCommand(new EveCommandTemplate(operation));
|
||||||
|
_children.Add(Control(
|
||||||
|
"control.toggle",
|
||||||
|
label,
|
||||||
|
value ? "true" : "false",
|
||||||
|
operation,
|
||||||
|
binding));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceFormBuilder Metric(
|
||||||
|
string label,
|
||||||
|
string value,
|
||||||
|
CultMeshStateBindingDescriptor? binding = null)
|
||||||
|
{
|
||||||
|
var bindings = binding == null
|
||||||
|
? Array.Empty<CultMeshStateBindingDescriptor>()
|
||||||
|
: new[] { binding };
|
||||||
|
_children.Add(new EveSurfaceComponent(
|
||||||
|
$"{_id}.metric.{EveSurfaceBuilder.Slug(label)}",
|
||||||
|
"metric",
|
||||||
|
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["label"] = label ?? "",
|
||||||
|
["value"] = value ?? ""
|
||||||
|
},
|
||||||
|
Array.Empty<EveSurfaceComponent>(),
|
||||||
|
bindings));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal EveSurfaceComponent Build()
|
||||||
|
{
|
||||||
|
return new EveSurfaceComponent(_id, "form", EveSurfaceBuilder.EmptyProps(), _children.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private EveSurfaceComponent Control(
|
||||||
|
string kind,
|
||||||
|
string label,
|
||||||
|
string value,
|
||||||
|
CultMeshOperationBindingDescriptor operation,
|
||||||
|
CultMeshStateBindingDescriptor? binding)
|
||||||
|
{
|
||||||
|
var bindings = binding == null
|
||||||
|
? Array.Empty<CultMeshStateBindingDescriptor>()
|
||||||
|
: new[] { binding };
|
||||||
|
return new EveSurfaceComponent(
|
||||||
|
$"{_id}.{EveSurfaceBuilder.Slug(label)}",
|
||||||
|
kind,
|
||||||
|
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["label"] = label ?? "",
|
||||||
|
["value"] = value ?? "",
|
||||||
|
["command"] = operation.OperationId,
|
||||||
|
["operationId"] = operation.OperationId,
|
||||||
|
["schemaId"] = operation.SchemaId
|
||||||
|
},
|
||||||
|
Array.Empty<EveSurfaceComponent>(),
|
||||||
|
bindings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class EveSurface
|
||||||
|
{
|
||||||
|
public static EveSurfaceBuilder Create(string surfaceId)
|
||||||
|
{
|
||||||
|
return new EveSurfaceBuilder(surfaceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 97ac7f757692aef4db64d75d0df6fe51
|
||||||
|
|
||||||
540
packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs
Normal file
540
packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs
Normal file
|
|
@ -0,0 +1,540 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using GameCult.Caching;
|
||||||
|
using GameCult.Mesh;
|
||||||
|
using MessagePack;
|
||||||
|
using MessagePack.Formatters;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace GameCult.Eve.Surface
|
||||||
|
{
|
||||||
|
[CultDocument("gamecult.eve.surface", "gamecult.eve.surface.v1")]
|
||||||
|
[MessagePackObject]
|
||||||
|
[MessagePackFormatter(typeof(EveSurfaceDocumentCompatibilityFormatter))]
|
||||||
|
public sealed class EveSurfaceDocument
|
||||||
|
{
|
||||||
|
public const string DefaultType = "surface-state";
|
||||||
|
public const string SchemaId = "gamecult.eve.surface.v1";
|
||||||
|
|
||||||
|
public EveSurfaceDocument(
|
||||||
|
string providerId,
|
||||||
|
string providerKind,
|
||||||
|
string title,
|
||||||
|
long version,
|
||||||
|
string updatedAtUtc,
|
||||||
|
EveSurfaceTree surface,
|
||||||
|
IReadOnlyList<EveCommandTemplate> commands)
|
||||||
|
: this(DefaultType, SchemaId, providerId, providerKind, title, version, updatedAtUtc, surface, commands)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EveSurfaceDocument(
|
||||||
|
string type,
|
||||||
|
string schema,
|
||||||
|
string providerId,
|
||||||
|
string providerKind,
|
||||||
|
string title,
|
||||||
|
long version,
|
||||||
|
string updatedAtUtc,
|
||||||
|
EveSurfaceTree surface,
|
||||||
|
IReadOnlyList<EveCommandTemplate> commands)
|
||||||
|
{
|
||||||
|
Type = string.IsNullOrWhiteSpace(type) ? DefaultType : type;
|
||||||
|
Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema;
|
||||||
|
ProviderId = providerId ?? "";
|
||||||
|
ProviderKind = providerKind ?? "";
|
||||||
|
Title = title ?? "";
|
||||||
|
Version = version;
|
||||||
|
UpdatedAtUtc = updatedAtUtc ?? "";
|
||||||
|
Surface = surface ?? throw new ArgumentNullException(nameof(surface));
|
||||||
|
Commands = commands ?? Array.Empty<EveCommandTemplate>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)]
|
||||||
|
public string Type { get; }
|
||||||
|
|
||||||
|
[Key(1)]
|
||||||
|
public string Schema { get; }
|
||||||
|
|
||||||
|
[Key(2)]
|
||||||
|
public string ProviderId { get; }
|
||||||
|
|
||||||
|
[Key(3)]
|
||||||
|
public string ProviderKind { get; }
|
||||||
|
|
||||||
|
[Key(4)]
|
||||||
|
public string Title { get; }
|
||||||
|
|
||||||
|
[Key(5)]
|
||||||
|
public long Version { get; }
|
||||||
|
|
||||||
|
[Key(6)]
|
||||||
|
public string UpdatedAtUtc { get; }
|
||||||
|
|
||||||
|
[Key(7)]
|
||||||
|
public EveSurfaceTree Surface { get; }
|
||||||
|
|
||||||
|
[Key(8)]
|
||||||
|
public IReadOnlyList<EveCommandTemplate> Commands { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveSurfaceTree
|
||||||
|
{
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EveSurfaceTree(string id, EveSurfaceComponent root, IReadOnlyList<EveStyleToken> styles)
|
||||||
|
{
|
||||||
|
Id = id ?? "";
|
||||||
|
Root = root ?? throw new ArgumentNullException(nameof(root));
|
||||||
|
Styles = styles ?? Array.Empty<EveStyleToken>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)]
|
||||||
|
public string Id { get; }
|
||||||
|
|
||||||
|
[Key(1)]
|
||||||
|
public EveSurfaceComponent Root { get; }
|
||||||
|
|
||||||
|
[Key(2)]
|
||||||
|
public IReadOnlyList<EveStyleToken> Styles { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveSurfaceComponent
|
||||||
|
{
|
||||||
|
public EveSurfaceComponent(
|
||||||
|
string id,
|
||||||
|
string kind,
|
||||||
|
IReadOnlyDictionary<string, string> props,
|
||||||
|
IReadOnlyList<EveSurfaceComponent> children)
|
||||||
|
: this(id, kind, props, children, StateBindingsFromProps(props))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceComponent(
|
||||||
|
string id,
|
||||||
|
string kind,
|
||||||
|
IReadOnlyDictionary<string, string> props,
|
||||||
|
IReadOnlyList<EveSurfaceComponent> children,
|
||||||
|
IReadOnlyList<CultMeshStateBindingDescriptor> stateBindings)
|
||||||
|
: this(id, kind, props, children, stateBindings, Array.Empty<EveEmbeddedDocumentSlot>())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceComponent(
|
||||||
|
string id,
|
||||||
|
string kind,
|
||||||
|
IReadOnlyDictionary<string, string> props,
|
||||||
|
IReadOnlyList<EveSurfaceComponent> children,
|
||||||
|
IReadOnlyList<CultMeshStateBindingDescriptor> stateBindings,
|
||||||
|
IReadOnlyList<EveEmbeddedDocumentSlot> embeddedDocuments,
|
||||||
|
IReadOnlyDictionary<string, string>? layout = null,
|
||||||
|
IReadOnlyDictionary<string, string>? style = null)
|
||||||
|
: this(
|
||||||
|
id,
|
||||||
|
kind,
|
||||||
|
props,
|
||||||
|
children,
|
||||||
|
(stateBindings ?? Array.Empty<CultMeshStateBindingDescriptor>())
|
||||||
|
.Select(CultMeshStateBindingRecord.FromBinding)
|
||||||
|
.ToArray(),
|
||||||
|
embeddedDocuments,
|
||||||
|
layout,
|
||||||
|
style)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EveSurfaceComponent(
|
||||||
|
string id,
|
||||||
|
string kind,
|
||||||
|
IReadOnlyDictionary<string, string> props,
|
||||||
|
IReadOnlyList<EveSurfaceComponent> children,
|
||||||
|
CultMeshStateBindingRecord[] stateBindingRecords,
|
||||||
|
IReadOnlyList<EveEmbeddedDocumentSlot> embeddedDocuments,
|
||||||
|
IReadOnlyDictionary<string, string>? layout = null,
|
||||||
|
IReadOnlyDictionary<string, string>? style = null)
|
||||||
|
{
|
||||||
|
Id = id ?? "";
|
||||||
|
Kind = kind ?? "";
|
||||||
|
Props = props ?? new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
Children = children ?? Array.Empty<EveSurfaceComponent>();
|
||||||
|
StateBindingRecords = stateBindingRecords ?? Array.Empty<CultMeshStateBindingRecord>();
|
||||||
|
EmbeddedDocuments = embeddedDocuments ?? Array.Empty<EveEmbeddedDocumentSlot>();
|
||||||
|
Layout = layout ?? new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
Style = style ?? new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)]
|
||||||
|
public string Id { get; }
|
||||||
|
|
||||||
|
[Key(1)]
|
||||||
|
public string Kind { get; }
|
||||||
|
|
||||||
|
[Key(2)]
|
||||||
|
public IReadOnlyDictionary<string, string> Props { get; }
|
||||||
|
|
||||||
|
[Key(3)]
|
||||||
|
public IReadOnlyList<EveSurfaceComponent> Children { get; }
|
||||||
|
|
||||||
|
[Key(4)]
|
||||||
|
public CultMeshStateBindingRecord[] StateBindingRecords { get; }
|
||||||
|
|
||||||
|
[IgnoreMember]
|
||||||
|
public IReadOnlyList<CultMeshStateBindingDescriptor> StateBindings =>
|
||||||
|
StateBindingRecords.Select(record => record.ToBinding()).ToArray();
|
||||||
|
|
||||||
|
[Key(5)]
|
||||||
|
public IReadOnlyList<EveEmbeddedDocumentSlot> EmbeddedDocuments { get; }
|
||||||
|
|
||||||
|
[Key(6)]
|
||||||
|
public IReadOnlyDictionary<string, string> Layout { get; }
|
||||||
|
|
||||||
|
[Key(7)]
|
||||||
|
public IReadOnlyDictionary<string, string> Style { get; }
|
||||||
|
|
||||||
|
public string GetProp(string key, string fallback = "")
|
||||||
|
{
|
||||||
|
return Props.TryGetValue(key, out var value) ? value : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<CultMeshStateBindingDescriptor> StateBindingsFromProps(
|
||||||
|
IReadOnlyDictionary<string, string>? props)
|
||||||
|
{
|
||||||
|
if (props == null || props.Count == 0)
|
||||||
|
return Array.Empty<CultMeshStateBindingDescriptor>();
|
||||||
|
|
||||||
|
var bindings = new List<CultMeshStateBindingDescriptor>();
|
||||||
|
foreach (var prop in props)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(prop.Value) ||
|
||||||
|
!prop.Key.EndsWith("PointerId", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetProp = prop.Key.Substring(0, prop.Key.Length - "PointerId".Length);
|
||||||
|
if (targetProp.Length == 0)
|
||||||
|
targetProp = "value";
|
||||||
|
bindings.Add(new CultMeshStateBindingDescriptor(targetProp, prop.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return bindings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveEmbeddedDocumentSlot
|
||||||
|
{
|
||||||
|
public EveEmbeddedDocumentSlot(
|
||||||
|
string slotId,
|
||||||
|
string documentId,
|
||||||
|
string schemaId,
|
||||||
|
string presentationKind,
|
||||||
|
CultMeshRouteHint? routeHint = null)
|
||||||
|
: this(slotId, documentId, schemaId, presentationKind, CultMeshRouteRecord.FromRoute(routeHint))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EveEmbeddedDocumentSlot(
|
||||||
|
string slotId,
|
||||||
|
string documentId,
|
||||||
|
string schemaId,
|
||||||
|
string presentationKind,
|
||||||
|
CultMeshRouteRecord route)
|
||||||
|
{
|
||||||
|
SlotId = slotId ?? "";
|
||||||
|
DocumentId = documentId ?? "";
|
||||||
|
SchemaId = schemaId ?? "";
|
||||||
|
PresentationKind = presentationKind ?? "";
|
||||||
|
Route = route ?? CultMeshRouteRecord.FromRoute(CultMeshRouteHint.Automatic);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)]
|
||||||
|
public string SlotId { get; }
|
||||||
|
|
||||||
|
[Key(1)]
|
||||||
|
public string DocumentId { get; }
|
||||||
|
|
||||||
|
[Key(2)]
|
||||||
|
public string SchemaId { get; }
|
||||||
|
|
||||||
|
[Key(3)]
|
||||||
|
public string PresentationKind { get; }
|
||||||
|
|
||||||
|
[Key(4)]
|
||||||
|
public CultMeshRouteRecord Route { get; }
|
||||||
|
|
||||||
|
[IgnoreMember]
|
||||||
|
public CultMeshRouteHint RouteHint => Route.ToRoute();
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveStyleToken
|
||||||
|
{
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EveStyleToken(string name, string value)
|
||||||
|
{
|
||||||
|
Name = name ?? "";
|
||||||
|
Value = value ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)]
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
[Key(1)]
|
||||||
|
public string Value { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
[MessagePackFormatter(typeof(EveCommandTemplateCompatibilityFormatter))]
|
||||||
|
public sealed class EveCommandTemplate
|
||||||
|
{
|
||||||
|
public EveCommandTemplate(CultMeshOperationBindingDescriptor operation)
|
||||||
|
: this(CultMeshOperationBindingRecord.FromBinding(operation))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EveCommandTemplate(CultMeshOperationBindingRecord operationRecord)
|
||||||
|
{
|
||||||
|
OperationRecord = operationRecord ?? throw new ArgumentNullException(nameof(operationRecord));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)]
|
||||||
|
public CultMeshOperationBindingRecord OperationRecord { get; }
|
||||||
|
|
||||||
|
[IgnoreMember]
|
||||||
|
public CultMeshOperationBindingDescriptor Operation => OperationRecord.ToBinding();
|
||||||
|
|
||||||
|
[IgnoreMember]
|
||||||
|
public string Command => Operation.OperationId;
|
||||||
|
|
||||||
|
[IgnoreMember]
|
||||||
|
public string Label => Operation.Label;
|
||||||
|
|
||||||
|
[IgnoreMember]
|
||||||
|
public string Transport => Operation.RouteHint.Description ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
[CultDocument("gamecult.eve.command_invocation", SchemaId)]
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveSurfaceCommandRequest
|
||||||
|
{
|
||||||
|
public const string SchemaId = "gamecult.eve.command_invocation.v1";
|
||||||
|
|
||||||
|
public EveSurfaceCommandRequest(
|
||||||
|
string providerId,
|
||||||
|
string surfaceId,
|
||||||
|
CultMeshOperationInvocationDescriptor operation,
|
||||||
|
CultMeshOperationPayload payload,
|
||||||
|
DateTimeOffset issuedAt,
|
||||||
|
string clientId,
|
||||||
|
string commandBoundary = "",
|
||||||
|
string receiptSchema = "")
|
||||||
|
: this(
|
||||||
|
SchemaId,
|
||||||
|
providerId,
|
||||||
|
surfaceId,
|
||||||
|
CultMeshOperationInvocationRecord.FromInvocation(operation),
|
||||||
|
payload?.ToDictionary(entry => entry.Key, entry => entry.Value, StringComparer.Ordinal)
|
||||||
|
?? new Dictionary<string, string>(StringComparer.Ordinal),
|
||||||
|
issuedAt,
|
||||||
|
clientId,
|
||||||
|
commandBoundary,
|
||||||
|
receiptSchema)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EveSurfaceCommandRequest(
|
||||||
|
string schema,
|
||||||
|
string providerId,
|
||||||
|
string surfaceId,
|
||||||
|
CultMeshOperationInvocationRecord operation,
|
||||||
|
Dictionary<string, string> payload,
|
||||||
|
DateTimeOffset issuedAt,
|
||||||
|
string clientId,
|
||||||
|
string commandBoundary,
|
||||||
|
string receiptSchema)
|
||||||
|
{
|
||||||
|
Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema;
|
||||||
|
ProviderId = providerId;
|
||||||
|
SurfaceId = surfaceId;
|
||||||
|
OperationRecord = operation ?? throw new ArgumentNullException(nameof(operation));
|
||||||
|
PayloadFields = payload == null
|
||||||
|
? new Dictionary<string, string>(StringComparer.Ordinal)
|
||||||
|
: new Dictionary<string, string>(payload, StringComparer.Ordinal);
|
||||||
|
IssuedAt = issuedAt;
|
||||||
|
ClientId = clientId;
|
||||||
|
CommandBoundary = commandBoundary ?? "";
|
||||||
|
ReceiptSchema = receiptSchema ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)]
|
||||||
|
public string Schema { get; }
|
||||||
|
|
||||||
|
[Key(1)]
|
||||||
|
public string ProviderId { get; }
|
||||||
|
|
||||||
|
[Key(2)]
|
||||||
|
public string SurfaceId { get; }
|
||||||
|
|
||||||
|
[Key(3)]
|
||||||
|
public CultMeshOperationInvocationRecord OperationRecord { get; }
|
||||||
|
|
||||||
|
[IgnoreMember]
|
||||||
|
public CultMeshOperationInvocationDescriptor Operation => OperationRecord.ToInvocation();
|
||||||
|
|
||||||
|
[IgnoreMember]
|
||||||
|
public string Command => Operation.OperationId;
|
||||||
|
|
||||||
|
[Key(4)]
|
||||||
|
public Dictionary<string, string> PayloadFields { get; }
|
||||||
|
|
||||||
|
[IgnoreMember]
|
||||||
|
public CultMeshOperationPayload Payload => new CultMeshOperationPayload(PayloadFields);
|
||||||
|
|
||||||
|
[Key(5)]
|
||||||
|
public DateTimeOffset IssuedAt { get; }
|
||||||
|
|
||||||
|
[Key(6)]
|
||||||
|
public string ClientId { get; }
|
||||||
|
|
||||||
|
[Key(7)]
|
||||||
|
public string CommandBoundary { get; }
|
||||||
|
|
||||||
|
[Key(8)]
|
||||||
|
public string ReceiptSchema { get; }
|
||||||
|
|
||||||
|
[IgnoreMember]
|
||||||
|
public string CommandId => Operation.IdempotencyKey ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the current Eve surface envelope and the original seven-field v1 envelope.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class EveSurfaceDocumentCompatibilityFormatter : IMessagePackFormatter<EveSurfaceDocument?>
|
||||||
|
{
|
||||||
|
private const int LegacyFieldCount = 7;
|
||||||
|
private const int CurrentFieldCount = 9;
|
||||||
|
|
||||||
|
public void Serialize(ref MessagePackWriter writer, EveSurfaceDocument? value, MessagePackSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value == null) { writer.WriteNil(); return; }
|
||||||
|
writer.WriteArrayHeader(CurrentFieldCount);
|
||||||
|
writer.Write(value.Type);
|
||||||
|
writer.Write(value.Schema);
|
||||||
|
writer.Write(value.ProviderId);
|
||||||
|
writer.Write(value.ProviderKind);
|
||||||
|
writer.Write(value.Title);
|
||||||
|
writer.Write(value.Version);
|
||||||
|
writer.Write(value.UpdatedAtUtc);
|
||||||
|
Formatter<EveSurfaceTree>(options).Serialize(ref writer, value.Surface, options);
|
||||||
|
Formatter<IReadOnlyList<EveCommandTemplate>>(options).Serialize(ref writer, value.Commands, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveSurfaceDocument? Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TryReadNil()) return null;
|
||||||
|
if (reader.NextMessagePackType != MessagePackType.Array)
|
||||||
|
throw new MessagePackSerializationException("Eve surface document must be an array.");
|
||||||
|
|
||||||
|
options.Security.DepthStep(ref reader);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fields = reader.ReadArrayHeader();
|
||||||
|
if (fields == LegacyFieldCount)
|
||||||
|
{
|
||||||
|
return new EveSurfaceDocument(
|
||||||
|
ReadString(ref reader), ReadString(ref reader), ReadString(ref reader),
|
||||||
|
reader.ReadInt64(), ReadString(ref reader),
|
||||||
|
Formatter<EveSurfaceTree>(options).Deserialize(ref reader, options)!,
|
||||||
|
Formatter<IReadOnlyList<EveCommandTemplate>>(options).Deserialize(ref reader, options)
|
||||||
|
?? Array.Empty<EveCommandTemplate>());
|
||||||
|
}
|
||||||
|
|
||||||
|
var type = ReadString(ref reader, fields, 0);
|
||||||
|
var schema = ReadString(ref reader, fields, 1);
|
||||||
|
var providerId = ReadString(ref reader, fields, 2);
|
||||||
|
var providerKind = ReadString(ref reader, fields, 3);
|
||||||
|
var title = ReadString(ref reader, fields, 4);
|
||||||
|
var version = fields > 5 ? reader.ReadInt64() : 0;
|
||||||
|
var updatedAtUtc = ReadString(ref reader, fields, 6);
|
||||||
|
var surface = fields > 7 ? Formatter<EveSurfaceTree>(options).Deserialize(ref reader, options) : null;
|
||||||
|
var commands = fields > 8 ? Formatter<IReadOnlyList<EveCommandTemplate>>(options).Deserialize(ref reader, options) : null;
|
||||||
|
for (var index = CurrentFieldCount; index < fields; index++) reader.Skip();
|
||||||
|
return new EveSurfaceDocument(
|
||||||
|
type, schema, providerId, providerKind, title, version, updatedAtUtc,
|
||||||
|
surface ?? throw new MessagePackSerializationException("Eve surface tree is required."),
|
||||||
|
commands ?? Array.Empty<EveCommandTemplate>());
|
||||||
|
}
|
||||||
|
finally { reader.Depth--; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IMessagePackFormatter<T> Formatter<T>(MessagePackSerializerOptions options) =>
|
||||||
|
options.Resolver.GetFormatter<T>()
|
||||||
|
?? throw new MessagePackSerializationException($"No formatter is registered for {typeof(T).FullName}.");
|
||||||
|
|
||||||
|
private static string ReadString(ref MessagePackReader reader) => reader.ReadString() ?? "";
|
||||||
|
private static string ReadString(ref MessagePackReader reader, int fields, int index) =>
|
||||||
|
index < fields ? ReadString(ref reader) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads both direct legacy command fields and the current wrapped binding record.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class EveCommandTemplateCompatibilityFormatter : IMessagePackFormatter<EveCommandTemplate?>
|
||||||
|
{
|
||||||
|
public void Serialize(ref MessagePackWriter writer, EveCommandTemplate? value, MessagePackSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value == null) { writer.WriteNil(); return; }
|
||||||
|
writer.WriteArrayHeader(1);
|
||||||
|
BindingFormatter(options).Serialize(ref writer, value.OperationRecord, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveCommandTemplate? Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TryReadNil()) return null;
|
||||||
|
if (reader.NextMessagePackType != MessagePackType.Array)
|
||||||
|
throw new MessagePackSerializationException("Eve command template must be an array.");
|
||||||
|
|
||||||
|
options.Security.DepthStep(ref reader);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fields = reader.ReadArrayHeader();
|
||||||
|
if (fields == 1 && reader.NextMessagePackType == MessagePackType.Array)
|
||||||
|
{
|
||||||
|
return new EveCommandTemplate(
|
||||||
|
BindingFormatter(options).Deserialize(ref reader, options)
|
||||||
|
?? new CultMeshOperationBindingRecord());
|
||||||
|
}
|
||||||
|
|
||||||
|
var operationId = ReadString(ref reader, fields, 0);
|
||||||
|
var label = ReadString(ref reader, fields, 1);
|
||||||
|
var schemaId = ReadString(ref reader, fields, 2);
|
||||||
|
var routeKind = ReadString(ref reader, fields, 3);
|
||||||
|
var routeDescription = ReadString(ref reader, fields, 4);
|
||||||
|
for (var index = 5; index < fields; index++) reader.Skip();
|
||||||
|
return new EveCommandTemplate(new CultMeshOperationBindingRecord(
|
||||||
|
operationId, label, schemaId, routeKind, routeDescription));
|
||||||
|
}
|
||||||
|
finally { reader.Depth--; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IMessagePackFormatter<CultMeshOperationBindingRecord> BindingFormatter(
|
||||||
|
MessagePackSerializerOptions options) =>
|
||||||
|
options.Resolver.GetFormatter<CultMeshOperationBindingRecord>()
|
||||||
|
?? throw new MessagePackSerializationException("No CultMesh operation binding formatter is registered.");
|
||||||
|
|
||||||
|
private static string ReadString(ref MessagePackReader reader, int fields, int index) =>
|
||||||
|
index < fields ? reader.ReadString() ?? "" : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6516c42104b235f42b15efeb4639bb73
|
||||||
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"name": "GameCult.Eve.Surface",
|
||||||
|
"rootNamespace": "GameCult.Eve.Surface",
|
||||||
|
"references": [
|
||||||
|
"GameCult.Mesh"
|
||||||
|
],
|
||||||
|
"includePlatforms": [],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"overrideReferences": true,
|
||||||
|
"precompiledReferences": [
|
||||||
|
"GameCult.Caching.dll",
|
||||||
|
"GameCult.Mesh.dll",
|
||||||
|
"MessagePack.Annotations.dll",
|
||||||
|
"MessagePack.dll"
|
||||||
|
],
|
||||||
|
"autoReferenced": true,
|
||||||
|
"defineConstraints": [],
|
||||||
|
"versionDefines": [],
|
||||||
|
"noEngineReferences": true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a9004278c14816345941af2f2ce673ca
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
||||||
|
|
||||||
29
packages/org.gamecult.eve.surface/package.json
Normal file
29
packages/org.gamecult.eve.surface/package.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "org.gamecult.eve.surface",
|
||||||
|
"displayName": "Eve Surface Contract",
|
||||||
|
"version": "0.3.0",
|
||||||
|
"unity": "2021.3",
|
||||||
|
"description": "Renderer-neutral C# contracts and builders for gamecult.eve.surface.v1 documents.",
|
||||||
|
"dependencies": {
|
||||||
|
"org.gamecult.cultlib": "1.0.43"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"Runtime",
|
||||||
|
"Runtime.meta",
|
||||||
|
"README.md",
|
||||||
|
"README.md.meta",
|
||||||
|
"Directory.Build.props",
|
||||||
|
"GameCult.Eve.Surface.csproj",
|
||||||
|
"GameCult.Eve.Surface.csproj.meta",
|
||||||
|
"package.json.meta"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"Eve",
|
||||||
|
"CultUI",
|
||||||
|
"CultMesh",
|
||||||
|
"Surface"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"category": "Scripting"
|
||||||
|
}
|
||||||
|
|
||||||
9
packages/org.gamecult.eve.surface/package.json.meta
Normal file
9
packages/org.gamecult.eve.surface/package.json.meta
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 49f1738bd94075e489c82190cf660b1d
|
||||||
|
PackageManifestImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
||||||
|
|
||||||
3
tests/GameCult.Eve.Surface.Serialization.Tests/.gitignore
vendored
Normal file
3
tests/GameCult.Eve.Surface.Serialization.Tests/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
!GameCult.Eve.Surface.Serialization.Tests.csproj
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,268 @@
|
||||||
|
using System.Buffers;
|
||||||
|
using GameCult.Eve.Surface;
|
||||||
|
using GameCult.Mesh;
|
||||||
|
using MessagePack;
|
||||||
|
using NUnit.Framework;
|
||||||
|
|
||||||
|
namespace GameCult.Eve.Surface.Tests;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public sealed class EveSurfaceSerializationCompatibilityTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void EntitySoaV2SerializesLogicalLayoutWithoutTransportFields()
|
||||||
|
{
|
||||||
|
var document = new EveEntitySoaViewDocument
|
||||||
|
{
|
||||||
|
ProviderId = "provider",
|
||||||
|
ViewId = "entities",
|
||||||
|
BodySchemaId = "entity.slab.v1",
|
||||||
|
LayoutVersion = 2,
|
||||||
|
ProducerEpoch = 3,
|
||||||
|
Sequence = 4,
|
||||||
|
Capacity = 8,
|
||||||
|
FrameId = 91,
|
||||||
|
Buffers = new[] { new EveEntitySoaBuffer { BufferId = "entity-hot", ByteLength = 64 } },
|
||||||
|
Identities = new[]
|
||||||
|
{
|
||||||
|
new EveEntityIdentity
|
||||||
|
{
|
||||||
|
Index = 0,
|
||||||
|
EntityId = "player",
|
||||||
|
EntityKind = "ship",
|
||||||
|
Label = "Vanguard",
|
||||||
|
Faction = "alliance",
|
||||||
|
Selectable = true,
|
||||||
|
Controllable = true,
|
||||||
|
AssetRef = "cultmesh://assets/player"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var bytes = MessagePackSerializer.Serialize(document, Options);
|
||||||
|
var reader = new MessagePackReader(bytes);
|
||||||
|
var restored = MessagePackSerializer.Deserialize<EveEntitySoaViewDocument>(bytes, Options);
|
||||||
|
|
||||||
|
Assert.That(reader.ReadArrayHeader(), Is.EqualTo(15));
|
||||||
|
Assert.That(restored.Schema, Is.EqualTo(EveEntitySoaViewDocument.SchemaId));
|
||||||
|
Assert.That(restored.Buffers[0].BufferId, Is.EqualTo("entity-hot"));
|
||||||
|
Assert.That(restored.FrameId, Is.EqualTo(91));
|
||||||
|
Assert.That(restored.Identities[0].Label, Is.EqualTo("Vanguard"));
|
||||||
|
Assert.That(restored.Identities[0].Faction, Is.EqualTo("alliance"));
|
||||||
|
Assert.That(restored.Identities[0].Selectable, Is.True);
|
||||||
|
Assert.That(restored.Identities[0].Controllable, Is.True);
|
||||||
|
Assert.That(restored.Identities[0].AssetRef, Is.EqualTo("cultmesh://assets/player"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void EntitySoaContractsExposeNoTransportAuthorityFields()
|
||||||
|
{
|
||||||
|
var forbidden = new[] { "Backend", "Location", "CapabilityToken", "Synchronization", "SynchronizationMode", "Descriptor" };
|
||||||
|
var exposed = new[] { typeof(EveEntitySoaViewDocument), typeof(EveEntitySoaBuffer), typeof(EveEntitySoaColumn) }
|
||||||
|
.SelectMany(type => type.GetProperties())
|
||||||
|
.Select(property => property.Name);
|
||||||
|
|
||||||
|
Assert.That(exposed, Has.None.Matches<string>(name => forbidden.Contains(name, StringComparer.Ordinal)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard;
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void LegacySevenFieldSurfaceDocumentDeserializes()
|
||||||
|
{
|
||||||
|
var bytes = WriteLegacySurfaceDocument();
|
||||||
|
|
||||||
|
var document = MessagePackSerializer.Deserialize<EveSurfaceDocument>(bytes, Options);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(document.Type, Is.EqualTo(EveSurfaceDocument.DefaultType));
|
||||||
|
Assert.That(document.Schema, Is.EqualTo(EveSurfaceDocument.SchemaId));
|
||||||
|
Assert.That(document.ProviderId, Is.EqualTo("legacy-provider"));
|
||||||
|
Assert.That(document.ProviderKind, Is.EqualTo("legacy-kind"));
|
||||||
|
Assert.That(document.Title, Is.EqualTo("Legacy surface"));
|
||||||
|
Assert.That(document.Version, Is.EqualTo(17));
|
||||||
|
Assert.That(document.UpdatedAtUtc, Is.EqualTo("2026-07-13T12:00:00Z"));
|
||||||
|
Assert.That(document.Surface.Id, Is.EqualTo("legacy-surface"));
|
||||||
|
Assert.That(document.Commands, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(document.Commands[0].Command, Is.EqualTo("legacy.execute"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void LegacyDirectFiveStringCommandTemplateDeserializes()
|
||||||
|
{
|
||||||
|
var bytes = WriteLegacyCommandTemplate();
|
||||||
|
|
||||||
|
var command = MessagePackSerializer.Deserialize<EveCommandTemplate>(bytes, Options);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(command.Command, Is.EqualTo("legacy.execute"));
|
||||||
|
Assert.That(command.Label, Is.EqualTo("Execute legacy command"));
|
||||||
|
Assert.That(command.Operation.SchemaId, Is.EqualTo("gamecult.legacy.command.v1"));
|
||||||
|
Assert.That(command.OperationRecord.RouteKind, Is.EqualTo("cultmesh"));
|
||||||
|
Assert.That(command.OperationRecord.RouteDescription, Is.EqualTo("legacy.commands"));
|
||||||
|
Assert.That(command.Transport, Is.EqualTo("legacy.commands"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CurrentSerializationWritesNineFieldSurfaceAndWrappedStructuredCommandBinding()
|
||||||
|
{
|
||||||
|
var document = CreateCurrentDocument();
|
||||||
|
|
||||||
|
var bytes = MessagePackSerializer.Serialize(document, Options);
|
||||||
|
var reader = new MessagePackReader(bytes);
|
||||||
|
|
||||||
|
Assert.That(reader.ReadArrayHeader(), Is.EqualTo(9));
|
||||||
|
for (var field = 0; field < 8; field++)
|
||||||
|
reader.Skip();
|
||||||
|
Assert.That(reader.ReadArrayHeader(), Is.EqualTo(1), "commands collection");
|
||||||
|
Assert.That(reader.ReadArrayHeader(), Is.EqualTo(1), "command template wrapper");
|
||||||
|
Assert.That(reader.NextMessagePackType, Is.EqualTo(MessagePackType.Array), "structured operation binding");
|
||||||
|
|
||||||
|
var roundTrip = MessagePackSerializer.Deserialize<EveSurfaceDocument>(bytes, Options);
|
||||||
|
Assert.That(roundTrip.Commands[0].Command, Is.EqualTo("current.execute"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void LegacyExpandedProviderAdvertisementDeserializesAsCanonicalContract()
|
||||||
|
{
|
||||||
|
var document = MessagePackSerializer.Deserialize<EveProviderAdvertisementDocument>(
|
||||||
|
WriteLegacyProviderAdvertisement(),
|
||||||
|
Options);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(document.ProviderId, Is.EqualTo("aetheria"));
|
||||||
|
Assert.That(document.ServiceId, Is.EqualTo("aetheria-daemon"));
|
||||||
|
Assert.That(document.VerseId, Is.EqualTo("aetheria.local"));
|
||||||
|
Assert.That(document.Title, Is.EqualTo("Aetheria"));
|
||||||
|
Assert.That(document.CultMeshAddress, Is.EqualTo("cultmesh://aetheria.local/aetheria"));
|
||||||
|
Assert.That(document.Surfaces[0].SurfaceId, Is.EqualTo("aetheria.pilot"));
|
||||||
|
Assert.That(document.Surfaces[0].Schema, Is.EqualTo(EveSurfaceDocument.SchemaId));
|
||||||
|
Assert.That(document.Commands[0].Command, Is.EqualTo("aetheria.pilot.move"));
|
||||||
|
Assert.That(document.Commands[0].SurfaceId, Is.Empty);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CurrentProviderAdvertisementSerializationWritesCanonicalFourteenFields()
|
||||||
|
{
|
||||||
|
var document = new EveProviderAdvertisementDocument(
|
||||||
|
"provider", "service", "verse", "Title", "game.runtime", "cultmesh://verse/provider",
|
||||||
|
"2026-07-13T14:00:00Z", new EveProviderFreshness("fresh", "2026-07-13T14:00:00Z", 15000),
|
||||||
|
new[] { EveSurfaceDocument.SchemaId }, Array.Empty<EveProviderWitness>(),
|
||||||
|
Array.Empty<EveAdvertisedSurface>(), Array.Empty<EveAdvertisedCommand>(),
|
||||||
|
new[] { "body-producer" });
|
||||||
|
|
||||||
|
var bytes = MessagePackSerializer.Serialize(document, Options);
|
||||||
|
var reader = new MessagePackReader(bytes);
|
||||||
|
|
||||||
|
Assert.That(reader.ReadArrayHeader(), Is.EqualTo(14));
|
||||||
|
var roundTrip = MessagePackSerializer.Deserialize<EveProviderAdvertisementDocument>(bytes, Options);
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(roundTrip.Title, Is.EqualTo("Title"));
|
||||||
|
Assert.That(roundTrip.AuthorizedBodyProducerIds, Is.EqualTo(new[] { "body-producer" }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EveSurfaceDocument CreateCurrentDocument() => new(
|
||||||
|
"current-provider",
|
||||||
|
"current-kind",
|
||||||
|
"Current surface",
|
||||||
|
23,
|
||||||
|
"2026-07-13T13:00:00Z",
|
||||||
|
CreateSurfaceTree("current-surface"),
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new EveCommandTemplate(new CultMeshOperationBindingRecord(
|
||||||
|
"current.execute",
|
||||||
|
"Execute current command",
|
||||||
|
"gamecult.current.command.v1",
|
||||||
|
"cultmesh",
|
||||||
|
"current.commands"))
|
||||||
|
});
|
||||||
|
|
||||||
|
private static EveSurfaceTree CreateSurfaceTree(string id) => new(
|
||||||
|
id,
|
||||||
|
new EveSurfaceComponent(
|
||||||
|
"root",
|
||||||
|
"column",
|
||||||
|
new Dictionary<string, string>(),
|
||||||
|
Array.Empty<EveSurfaceComponent>()),
|
||||||
|
Array.Empty<EveStyleToken>());
|
||||||
|
|
||||||
|
private static byte[] WriteLegacySurfaceDocument()
|
||||||
|
{
|
||||||
|
var buffer = new ArrayBufferWriter<byte>();
|
||||||
|
var writer = new MessagePackWriter(buffer);
|
||||||
|
writer.WriteArrayHeader(7);
|
||||||
|
writer.Write("legacy-provider");
|
||||||
|
writer.Write("legacy-kind");
|
||||||
|
writer.Write("Legacy surface");
|
||||||
|
writer.Write(17L);
|
||||||
|
writer.Write("2026-07-13T12:00:00Z");
|
||||||
|
writer.WriteRaw(MessagePackSerializer.Serialize(CreateSurfaceTree("legacy-surface"), Options));
|
||||||
|
writer.WriteArrayHeader(1);
|
||||||
|
writer.WriteRaw(WriteLegacyCommandTemplate());
|
||||||
|
writer.Flush();
|
||||||
|
return buffer.WrittenSpan.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] WriteLegacyCommandTemplate()
|
||||||
|
{
|
||||||
|
var buffer = new ArrayBufferWriter<byte>();
|
||||||
|
var writer = new MessagePackWriter(buffer);
|
||||||
|
writer.WriteArrayHeader(5);
|
||||||
|
writer.Write("legacy.execute");
|
||||||
|
writer.Write("Execute legacy command");
|
||||||
|
writer.Write("gamecult.legacy.command.v1");
|
||||||
|
writer.Write("cultmesh");
|
||||||
|
writer.Write("legacy.commands");
|
||||||
|
writer.Flush();
|
||||||
|
return buffer.WrittenSpan.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] WriteLegacyProviderAdvertisement()
|
||||||
|
{
|
||||||
|
var buffer = new ArrayBufferWriter<byte>();
|
||||||
|
var writer = new MessagePackWriter(buffer);
|
||||||
|
writer.WriteArrayHeader(16);
|
||||||
|
writer.Write(EveProviderAdvertisementDocument.SchemaId);
|
||||||
|
writer.Write("aetheria");
|
||||||
|
writer.Write("aetheria-daemon");
|
||||||
|
writer.Write("aetheria.local");
|
||||||
|
writer.Write("asgard");
|
||||||
|
writer.Write("aetheria");
|
||||||
|
writer.Write("aetheria.local/aetheria");
|
||||||
|
writer.Write("cultmesh://aetheria.local/aetheria");
|
||||||
|
writer.Write("Aetheria");
|
||||||
|
writer.Write("game.runtime");
|
||||||
|
writer.Write("2026-07-13T14:00:00Z");
|
||||||
|
writer.WriteArrayHeader(3);
|
||||||
|
writer.Write("fresh");
|
||||||
|
writer.Write("2026-07-13T14:00:00Z");
|
||||||
|
writer.Write(15000);
|
||||||
|
writer.WriteArrayHeader(1);
|
||||||
|
writer.Write(EveSurfaceDocument.SchemaId);
|
||||||
|
writer.WriteArrayHeader(0);
|
||||||
|
writer.WriteArrayHeader(1);
|
||||||
|
writer.WriteArrayHeader(5);
|
||||||
|
writer.Write(EveSurfaceDocument.SchemaId);
|
||||||
|
writer.Write("aetheria.pilot");
|
||||||
|
writer.Write("eve:surface:aetheria.pilot");
|
||||||
|
writer.Write("cultmesh");
|
||||||
|
writer.Write("available");
|
||||||
|
writer.WriteArrayHeader(1);
|
||||||
|
writer.WriteArrayHeader(3);
|
||||||
|
writer.Write("aetheria.pilot.move");
|
||||||
|
writer.Write("cultmesh");
|
||||||
|
writer.Write("Move the pilot ship");
|
||||||
|
writer.Flush();
|
||||||
|
return buffer.WrittenSpan.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
<BaseOutputPath>obj\bin\</BaseOutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
|
<PackageReference Include="NUnit" Version="4.2.2" />
|
||||||
|
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\packages\org.gamecult.eve.surface\GameCult.Eve.Surface.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in a new issue