Restore the stable legacy Unity project
This commit is contained in:
parent
56a156ee87
commit
7006a6b0dd
2070 changed files with 180490 additions and 169303 deletions
|
|
@ -2,31 +2,22 @@
|
|||
"dependencies": {
|
||||
"com.unity.2d.sprite": "1.0.0",
|
||||
"com.unity.2d.tilemap": "1.0.0",
|
||||
"com.unity.ai.navigation": "2.0.12",
|
||||
"com.unity.burst": "1.8.29",
|
||||
"com.unity.cinemachine": "2.10.7",
|
||||
"com.unity.entities.graphics": "1.4.20",
|
||||
"com.unity.ide.rider": "3.0.39",
|
||||
"com.unity.ide.visualstudio": "2.0.27",
|
||||
"com.unity.inputsystem": "1.19.0",
|
||||
"com.unity.mathematics": "1.3.3",
|
||||
"com.unity.multiplayer.center": "1.0.1",
|
||||
"com.unity.recorder": "5.1.5",
|
||||
"com.unity.render-pipelines.universal": "17.4.0",
|
||||
"com.unity.test-framework": "1.6.0",
|
||||
"com.unity.timeline": "1.8.12",
|
||||
"com.unity.ai.navigation": "2.0.8",
|
||||
"com.unity.burst": "1.8.23",
|
||||
"com.unity.cinemachine": "2.10.4",
|
||||
"com.unity.ide.rider": "3.0.36",
|
||||
"com.unity.ide.visualstudio": "2.0.23",
|
||||
"com.unity.inputsystem": "1.14.0",
|
||||
"com.unity.localization": "1.5.4",
|
||||
"com.unity.mathematics": "1.3.2",
|
||||
"com.unity.multiplayer.center": "1.0.0",
|
||||
"com.unity.postprocessing": "3.5.0",
|
||||
"com.unity.recorder": "5.1.2",
|
||||
"com.unity.test-framework": "1.5.1",
|
||||
"com.unity.timeline": "1.8.7",
|
||||
"com.unity.ugui": "2.0.0",
|
||||
"com.unity.xr.legacyinputhelpers": "3.0.1",
|
||||
"org.gamecult.aetheria.eve-runtime": "file:org.gamecult.aetheria.eve-runtime",
|
||||
"org.gamecult.aetheria.state": "file:org.gamecult.aetheria.state",
|
||||
"org.gamecult.cultmath": "https://github.com/GameCult/CultMath.git?path=/unity/org.gamecult.cultmath#cultmath-unity-v0.1.2",
|
||||
"org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.43",
|
||||
"org.gamecult.eve.surface": "file:../../EveUnity/packages/org.gamecult.eve.surface",
|
||||
"org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3",
|
||||
"org.gamecult.eve.unity-scene": "file:../../EveUnity/packages/org.gamecult.eve.unity-scene",
|
||||
"org.gamecult.eve.unity-uitoolkit": "file:../../EveUnity/packages/org.gamecult.eve.unity-uitoolkit",
|
||||
"com.unity.xr.legacyinputhelpers": "2.1.12",
|
||||
"com.unity.modules.accessibility": "1.0.0",
|
||||
"com.unity.modules.adaptiveperformance": "1.0.0",
|
||||
"com.unity.modules.ai": "1.0.0",
|
||||
"com.unity.modules.androidjni": "1.0.0",
|
||||
"com.unity.modules.animation": "1.0.0",
|
||||
|
|
@ -53,7 +44,6 @@
|
|||
"com.unity.modules.unitywebrequestaudio": "1.0.0",
|
||||
"com.unity.modules.unitywebrequesttexture": "1.0.0",
|
||||
"com.unity.modules.unitywebrequestwww": "1.0.0",
|
||||
"com.unity.modules.vectorgraphics": "1.0.0",
|
||||
"com.unity.modules.vehicles": "1.0.0",
|
||||
"com.unity.modules.video": "1.0.0",
|
||||
"com.unity.modules.vr": "1.0.0",
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
# Aetheria Eve Runtime
|
||||
|
||||
This package mounts provider-owned Eve surfaces from Aetheria's typed
|
||||
CultCache state into Unity UI Toolkit.
|
||||
|
||||
`AetheriaEveUnitySceneProviderBridge` is the local-replica adapter. It reads a
|
||||
provider-owned `.cc` replica in the Unity process; it is not evidence of a
|
||||
network connection to the daemon. Remote clients open
|
||||
`AetheriaRuntimeVerseClient.OpenRemoteAsync`, synchronize published documents
|
||||
through CultMesh, and forward writes to the daemon's primary shard. The generic
|
||||
EveUnity PlayMode client must compose that remote path before this package can
|
||||
claim live daemon playability.
|
||||
|
||||
Remote command completion is derived only from provider-owned
|
||||
`gamecult.aetheria.committed_command_fact.v1` documents. The adapter correlates
|
||||
`CommandId` and waits until the synchronized game surface version reaches the
|
||||
fact's `SourceFrameId` before emitting a terminal EveUnity receipt. Submission
|
||||
success is never treated as command acceptance.
|
||||
|
||||
`AetheriaEveSurfacePresenter` is a typed surface document lowerer:
|
||||
|
||||
- it resolves the active client target from `GameData/aetheria-client.cc`;
|
||||
- it reads `gamecult.eve.surface.v1` from the selected typed state source, which
|
||||
is currently still a local `.cc` file transport;
|
||||
- it lowers the retained tree through `org.gamecult.eve.unity-uitoolkit`;
|
||||
- it submits renderer-emitted commands as typed `gamecult.eve.command.v1` state
|
||||
records for Aetheria's provider-owned command bridge.
|
||||
|
||||
`AetheriaEveRuntimeBootstrap` mounts the first runtime surface after scene load.
|
||||
By default it creates a `UIDocument` host for `aetheria.game`, the daemon-owned
|
||||
game surface, so Unity starts as a lowering surface for the daemon Verse member
|
||||
without a hand-wired scene object. Set `AETHERIA_EVE_SURFACE_ID` to mount a
|
||||
different daemon or provider surface such as `aetheria.game.tui`,
|
||||
`aetheria.daemon.editor`, or `aetheria.daemon.editor.tui`; set
|
||||
`AETHERIA_STATE_PATH` to override the selected local `.cc` state file, or disable the automatic mount with
|
||||
`AETHERIA_DISABLE_EVE_RUNTIME_BOOTSTRAP=true` or
|
||||
`--aetheria-disable-eve-runtime-bootstrap`. Batchmode disables the bootstrap so
|
||||
compile and smoke gates do not accidentally create renderer state.
|
||||
|
||||
Nested CultUI regions are expressed as `embeddedDocuments` / `surface.slot`
|
||||
entries on the retained surface component. Aetheria mirrors Eve's
|
||||
`EveEmbeddedDocumentSlot` as `AetheriaRuntimeEmbeddedDocumentSlot`, and the
|
||||
Unity host resolves those child documents through the same managed CultMesh
|
||||
state handles as the parent surface. Use this for daemon-owned composite UI
|
||||
such as inventory panels with dropdown child surfaces; do not rebuild the child
|
||||
state as a Unity-only model, projector, or drag/drop workaround.
|
||||
|
||||
Discovery starts from Eve's `web/fixtures/cultui-embedded-surface.json` fixture
|
||||
and `docs/parity-testing-harness.md` runtime matrix. Keep Unity covered by the
|
||||
Aetheria state verifier and `verify-stage7d-unity-parity.ps1`; keep the shared
|
||||
CultUI contract covered by Eve's browser DSL test, parity harness, and Flutter
|
||||
parity smoke.
|
||||
|
||||
Commands are surfaced as `gamecult.eve.command.v1` requests. The presenter does
|
||||
not accept or apply them locally; provider acceptance still belongs to the
|
||||
CultMesh command bridge. Renderer-local command effects would be the wrong
|
||||
organ wearing a clean shirt.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e1c93ed85db7ef24abd1a24ca16abe4e
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2860def02295c5045af9ba820ea8e696
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,348 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Runtime.Serialization.Json;
|
||||
using System.Text;
|
||||
using GameCult.Eve.Surface;
|
||||
using GameCult.Mesh;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.EveRuntime
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(UIDocument))]
|
||||
public sealed class AetheriaEveFileSurfacePresenter : MonoBehaviour
|
||||
{
|
||||
public const string DebugSurfacePathEnvironmentVariable = "AETHERIA_CULTUI_DEBUG_SURFACE_PATH";
|
||||
public const string DefaultDebugSurfaceRelativePath = "GameData/cultui-debug-surface.cultui";
|
||||
|
||||
[SerializeField]
|
||||
private string surfaceFilePath = "";
|
||||
|
||||
[SerializeField]
|
||||
private bool mountOnEnable = true;
|
||||
|
||||
[SerializeField]
|
||||
private bool refreshInUpdate = true;
|
||||
|
||||
[SerializeField]
|
||||
private float refreshIntervalSeconds = 0.1f;
|
||||
|
||||
private UIDocument? _document;
|
||||
private string _mountedPath = "";
|
||||
private DateTime _mountedWriteTimeUtc = DateTime.MinValue;
|
||||
private long _mountedLength = -1;
|
||||
private float _nextRefreshTime;
|
||||
|
||||
public string SurfaceFilePath
|
||||
{
|
||||
get => surfaceFilePath;
|
||||
set => surfaceFilePath = value ?? "";
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (mountOnEnable)
|
||||
Mount();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!refreshInUpdate || Time.unscaledTime < _nextRefreshTime)
|
||||
return;
|
||||
|
||||
_nextRefreshTime = Time.unscaledTime + Mathf.Max(0.01f, refreshIntervalSeconds);
|
||||
RefreshIfChanged();
|
||||
}
|
||||
|
||||
public void Mount()
|
||||
{
|
||||
var path = ResolveSurfacePath(surfaceFilePath);
|
||||
var document = ResolveDocument();
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
MountError(document, $"CultUI debug surface file not found: {path}");
|
||||
_mountedPath = "";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var surface = AetheriaEveFileSurfaceDocuments.Read(path);
|
||||
AetheriaEveUnitySurfaceHost.Render(
|
||||
transform,
|
||||
document,
|
||||
"Aetheria CultUI File Surface",
|
||||
surface,
|
||||
request => Debug.Log(
|
||||
$"CultUI debug file surface command preview: {request.ProviderId}/{request.SurfaceId}/{request.Operation.OperationId}"),
|
||||
CreateChrome());
|
||||
CaptureMountedState(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MountError(document, $"Failed to read CultUI debug surface file: {ex.Message}");
|
||||
_mountedPath = "";
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshIfChanged()
|
||||
{
|
||||
var path = ResolveSurfacePath(surfaceFilePath);
|
||||
if (!File.Exists(path))
|
||||
return;
|
||||
|
||||
var info = new FileInfo(path);
|
||||
if (string.Equals(_mountedPath, path, StringComparison.Ordinal) &&
|
||||
_mountedWriteTimeUtc == info.LastWriteTimeUtc &&
|
||||
_mountedLength == info.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Mount();
|
||||
}
|
||||
|
||||
private UIDocument ResolveDocument()
|
||||
{
|
||||
if (_document == null)
|
||||
_document = GetComponent<UIDocument>();
|
||||
return _document;
|
||||
}
|
||||
|
||||
private void CaptureMountedState(string path)
|
||||
{
|
||||
var info = new FileInfo(path);
|
||||
_mountedPath = path;
|
||||
_mountedWriteTimeUtc = info.LastWriteTimeUtc;
|
||||
_mountedLength = info.Length;
|
||||
}
|
||||
|
||||
private static void MountError(UIDocument document, string message)
|
||||
{
|
||||
var root = document.rootVisualElement;
|
||||
root.Clear();
|
||||
var label = new Label(message);
|
||||
label.AddToClassList("aetheria-eve-runtime-error-label");
|
||||
root.Add(label);
|
||||
Debug.LogWarning(message);
|
||||
}
|
||||
|
||||
public static string ResolveSurfacePath(string configuredPath)
|
||||
{
|
||||
var environmentPath = Environment.GetEnvironmentVariable(DebugSurfacePathEnvironmentVariable);
|
||||
var path = string.IsNullOrWhiteSpace(configuredPath) ? environmentPath : configuredPath;
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
path = Path.Combine(ProjectRoot(), DefaultDebugSurfaceRelativePath);
|
||||
if (Path.IsPathRooted(path))
|
||||
return Path.GetFullPath(path);
|
||||
return Path.GetFullPath(Path.Combine(ProjectRoot(), path));
|
||||
}
|
||||
|
||||
private static string ProjectRoot()
|
||||
{
|
||||
return Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
|
||||
}
|
||||
|
||||
private static AetheriaEveUnitySurfaceChrome CreateChrome()
|
||||
{
|
||||
return new AetheriaEveUnitySurfaceChrome
|
||||
{
|
||||
UseShell = true,
|
||||
RootAlignItems = Align.FlexEnd,
|
||||
RootJustifyContent = Justify.FlexStart,
|
||||
RootPaddingTop = 72f,
|
||||
RootPaddingRight = 24f,
|
||||
MaxWidth = 420f,
|
||||
MinWidth = 360f,
|
||||
BackgroundColor = new Color(0.05f, 0.07f, 0.09f, 0.94f),
|
||||
BorderColor = new Color(0.35f, 0.64f, 0.74f, 0.82f)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class AetheriaEveFileSurfaceDocuments
|
||||
{
|
||||
public static EveSurfaceDocument Read(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new ArgumentException("CultUI surface path is required.", nameof(path));
|
||||
|
||||
using var stream = File.OpenRead(path);
|
||||
var serializer = new DataContractJsonSerializer(
|
||||
typeof(FileSurfaceDocument),
|
||||
new DataContractJsonSerializerSettings
|
||||
{
|
||||
UseSimpleDictionaryFormat = true
|
||||
});
|
||||
var document = (FileSurfaceDocument?)serializer.ReadObject(stream)
|
||||
?? throw new InvalidDataException("CultUI surface file is empty.");
|
||||
|
||||
return ToEveSurfaceDocument(document, path);
|
||||
}
|
||||
|
||||
private static EveSurfaceDocument ToEveSurfaceDocument(FileSurfaceDocument document, string path)
|
||||
{
|
||||
var surface = document.Surface;
|
||||
var surfaceId = string.IsNullOrWhiteSpace(surface?.Id)
|
||||
? "aetheria.debug.file_surface"
|
||||
: surface!.Id!;
|
||||
var root = document.Surface?.Root == null
|
||||
? new EveSurfaceComponent(
|
||||
surfaceId + ".root",
|
||||
"surface",
|
||||
new Dictionary<string, string>(StringComparer.Ordinal),
|
||||
Array.Empty<EveSurfaceComponent>())
|
||||
: ToComponent(document.Surface.Root);
|
||||
var writeTime = File.GetLastWriteTimeUtc(path);
|
||||
var providerId = string.IsNullOrWhiteSpace(document.ProviderId) ? "aetheria.debug" : document.ProviderId!;
|
||||
var providerKind = string.IsNullOrWhiteSpace(document.ProviderKind) ? "debug.file_surface" : document.ProviderKind!;
|
||||
var updatedAtUtc = string.IsNullOrWhiteSpace(document.UpdatedAtUtc) ? writeTime.ToString("O") : document.UpdatedAtUtc!;
|
||||
return new EveSurfaceDocument(
|
||||
providerId,
|
||||
providerKind,
|
||||
document.Title ?? "",
|
||||
document.Version <= 0 ? writeTime.Ticks : document.Version,
|
||||
updatedAtUtc,
|
||||
new EveSurfaceTree(
|
||||
surfaceId,
|
||||
root,
|
||||
(document.Surface?.Styles ?? Array.Empty<FileStyleToken>())
|
||||
.Select(style => new EveStyleToken(style.Name ?? "", style.Value ?? ""))
|
||||
.ToArray()),
|
||||
(document.Commands ?? Array.Empty<FileCommand>())
|
||||
.Where(command => !string.IsNullOrWhiteSpace(command.Command))
|
||||
.Select(command => new EveCommandTemplate(CultMesh.OperationBindingRecord(
|
||||
command.Command ?? "",
|
||||
command.Label ?? command.Command ?? "",
|
||||
"",
|
||||
nameof(CultMeshLocalityKind.Automatic),
|
||||
command.Transport ?? "debug-log").ToBinding()))
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
private static EveSurfaceComponent ToComponent(FileSurfaceComponent component)
|
||||
{
|
||||
return new EveSurfaceComponent(
|
||||
component.Id ?? "",
|
||||
component.Kind ?? "",
|
||||
ToProps(component.Props),
|
||||
(component.Children ?? Array.Empty<FileSurfaceComponent>())
|
||||
.Select(ToComponent)
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> ToProps(IReadOnlyList<FileProp>? props)
|
||||
{
|
||||
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
if (props == null)
|
||||
return result;
|
||||
|
||||
foreach (var prop in props)
|
||||
{
|
||||
var key = prop.Key;
|
||||
if (!string.IsNullOrWhiteSpace(key))
|
||||
result[key!] = prop.Value ?? "";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#pragma warning disable 0649
|
||||
[DataContract]
|
||||
private sealed class FileSurfaceDocument
|
||||
{
|
||||
[DataMember(Name = "schema")]
|
||||
public string? Schema;
|
||||
|
||||
[DataMember(Name = "providerId")]
|
||||
public string? ProviderId;
|
||||
|
||||
[DataMember(Name = "providerKind")]
|
||||
public string? ProviderKind;
|
||||
|
||||
[DataMember(Name = "title")]
|
||||
public string? Title;
|
||||
|
||||
[DataMember(Name = "version")]
|
||||
public long Version;
|
||||
|
||||
[DataMember(Name = "updatedAtUtc")]
|
||||
public string? UpdatedAtUtc;
|
||||
|
||||
[DataMember(Name = "surface")]
|
||||
public FileSurfaceTree? Surface;
|
||||
|
||||
[DataMember(Name = "commands")]
|
||||
public FileCommand[]? Commands;
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
private sealed class FileSurfaceTree
|
||||
{
|
||||
[DataMember(Name = "id")]
|
||||
public string? Id;
|
||||
|
||||
[DataMember(Name = "root")]
|
||||
public FileSurfaceComponent? Root;
|
||||
|
||||
[DataMember(Name = "styles")]
|
||||
public FileStyleToken[]? Styles;
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
private sealed class FileSurfaceComponent
|
||||
{
|
||||
[DataMember(Name = "id")]
|
||||
public string? Id;
|
||||
|
||||
[DataMember(Name = "kind")]
|
||||
public string? Kind;
|
||||
|
||||
[DataMember(Name = "props")]
|
||||
public FileProp[]? Props;
|
||||
|
||||
[DataMember(Name = "children")]
|
||||
public FileSurfaceComponent[]? Children;
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
private sealed class FileProp
|
||||
{
|
||||
[DataMember(Name = "key")]
|
||||
public string? Key;
|
||||
|
||||
[DataMember(Name = "value")]
|
||||
public string? Value;
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
private sealed class FileStyleToken
|
||||
{
|
||||
[DataMember(Name = "name")]
|
||||
public string? Name;
|
||||
|
||||
[DataMember(Name = "value")]
|
||||
public string? Value;
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
private sealed class FileCommand
|
||||
{
|
||||
[DataMember(Name = "command")]
|
||||
public string? Command;
|
||||
|
||||
[DataMember(Name = "label")]
|
||||
public string? Label;
|
||||
|
||||
[DataMember(Name = "transport")]
|
||||
public string? Transport;
|
||||
}
|
||||
#pragma warning restore 0649
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 55fd8c7b47c94f2c9407eb5d59d8e17f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using GameCult.Aetheria.State.Verse;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.EveRuntime
|
||||
{
|
||||
public static class AetheriaEveRuntimeBootstrap
|
||||
{
|
||||
public const string DefaultSurfaceId = AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId;
|
||||
public const string DisableEnvironmentVariable = "AETHERIA_DISABLE_EVE_RUNTIME_BOOTSTRAP";
|
||||
public const string SurfaceEnvironmentVariable = "AETHERIA_EVE_SURFACE_ID";
|
||||
public const string FileSurfaceEnvironmentVariable = "AETHERIA_EVE_FILE_SURFACE_PATH";
|
||||
public const string DisableCommandLineSwitch = "--aetheria-disable-eve-runtime-bootstrap";
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||
private static void MountDefaultSurface()
|
||||
{
|
||||
if (IsDisabled() ||
|
||||
UnityEngine.Object.FindAnyObjectByType<AetheriaEveSurfacePresenter>() != null ||
|
||||
UnityEngine.Object.FindAnyObjectByType<AetheriaEveFileSurfacePresenter>() != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var host = new GameObject("Aetheria Eve Runtime Surface");
|
||||
UnityEngine.Object.DontDestroyOnLoad(host);
|
||||
|
||||
host.AddComponent<UIDocument>();
|
||||
var fileSurfacePath = FileSurfacePath();
|
||||
if (!string.IsNullOrWhiteSpace(fileSurfacePath))
|
||||
{
|
||||
var presenter = host.AddComponent<AetheriaEveFileSurfacePresenter>();
|
||||
presenter.SurfaceFilePath = fileSurfacePath;
|
||||
return;
|
||||
}
|
||||
|
||||
var statePresenter = host.AddComponent<AetheriaEveSurfacePresenter>();
|
||||
statePresenter.SurfaceId = SurfaceId();
|
||||
}
|
||||
|
||||
private static bool IsDisabled()
|
||||
{
|
||||
if (Application.isBatchMode)
|
||||
return true;
|
||||
|
||||
var environment = Environment.GetEnvironmentVariable(DisableEnvironmentVariable);
|
||||
if (string.Equals(environment, "1", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(environment, "true", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return Environment.GetCommandLineArgs()
|
||||
.Any(argument => string.Equals(argument, DisableCommandLineSwitch, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static string SurfaceId()
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable(SurfaceEnvironmentVariable);
|
||||
return string.IsNullOrWhiteSpace(configured) ? DefaultSurfaceId : configured;
|
||||
}
|
||||
|
||||
private static string FileSurfacePath()
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable(FileSurfaceEnvironmentVariable);
|
||||
return string.IsNullOrWhiteSpace(configured) ? "" : configured;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: dfb126623705ee04c9747f63b945c5f1
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using GameCult.Aetheria.State.Verse;
|
||||
using GameCult.Mesh;
|
||||
using UnityEngine;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.EveRuntime
|
||||
{
|
||||
public static class AetheriaEveRuntimeUnityClientCache
|
||||
{
|
||||
private static readonly Dictionary<string, AetheriaClient> RuntimeClients =
|
||||
new Dictionary<string, AetheriaClient>(StringComparer.Ordinal);
|
||||
private static DirectoryInfo? s_gameDataDirectory;
|
||||
private static string? s_runtimeStateFilePath;
|
||||
|
||||
public static DirectoryInfo GameDataDirectory =>
|
||||
s_gameDataDirectory ??= new DirectoryInfo(Application.dataPath).Parent.CreateSubdirectory("GameData");
|
||||
|
||||
public static string RuntimeStateFilePath =>
|
||||
s_runtimeStateFilePath ??= AetheriaRuntimeStateBoot.Inspect(GameDataDirectory).StateFilePath;
|
||||
|
||||
public static AetheriaClientState RuntimeState(
|
||||
AetheriaRuntimeStateBootReport stateBoot,
|
||||
string runtimeId = "")
|
||||
{
|
||||
return RuntimeClient(stateBoot, runtimeId).State;
|
||||
}
|
||||
|
||||
public static AetheriaControl Control(
|
||||
AetheriaRuntimeStateBootReport stateBoot,
|
||||
string runtimeId = "")
|
||||
{
|
||||
return RuntimeClient(stateBoot, runtimeId).Control;
|
||||
}
|
||||
|
||||
public static AetheriaUi Ui(
|
||||
AetheriaRuntimeStateBootReport stateBoot,
|
||||
string runtimeId = "")
|
||||
{
|
||||
return RuntimeClient(stateBoot, runtimeId).Ui;
|
||||
}
|
||||
|
||||
public static CultMeshStateRefResolver EveSurfaceCultMeshStateRefResolver(
|
||||
AetheriaRuntimeStateBootReport stateBoot,
|
||||
string runtimeId = "")
|
||||
{
|
||||
return RuntimeState(stateBoot, runtimeId).CreateEveSurfaceCultMeshStateRefResolver();
|
||||
}
|
||||
|
||||
public static void ClearRuntimeStateFilePathCache()
|
||||
{
|
||||
s_runtimeStateFilePath = null;
|
||||
}
|
||||
|
||||
public static void Dispose()
|
||||
{
|
||||
foreach (var client in RuntimeClients.Values)
|
||||
client.Dispose();
|
||||
|
||||
RuntimeClients.Clear();
|
||||
s_runtimeStateFilePath = null;
|
||||
}
|
||||
|
||||
private static AetheriaClient RuntimeClient(
|
||||
AetheriaRuntimeStateBootReport stateBoot,
|
||||
string runtimeId = "")
|
||||
{
|
||||
return ResolveClient(
|
||||
stateBoot.StateFilePath,
|
||||
string.IsNullOrWhiteSpace(runtimeId) ? stateBoot.RuntimeId : runtimeId);
|
||||
}
|
||||
|
||||
private static AetheriaClient ResolveClient(string stateFilePath, string runtimeId)
|
||||
{
|
||||
var effectiveStateFilePath = stateFilePath ?? "";
|
||||
var effectiveRuntimeId = string.IsNullOrWhiteSpace(runtimeId)
|
||||
? AetheriaRuntimeStateBoundary.DefaultClientRuntimeId
|
||||
: runtimeId;
|
||||
var cacheKey = effectiveStateFilePath + "\n" + effectiveRuntimeId;
|
||||
if (RuntimeClients.TryGetValue(cacheKey, out var runtimeClient))
|
||||
return runtimeClient;
|
||||
|
||||
runtimeClient = AetheriaClient
|
||||
.OpenAsync(
|
||||
effectiveStateFilePath,
|
||||
effectiveRuntimeId,
|
||||
"local",
|
||||
startServer: false,
|
||||
pullOnOpen: true)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
RuntimeClients[cacheKey] = runtimeClient;
|
||||
return runtimeClient;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2a2c3bf5e2954d1f8aeab60ee92f7f0a
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
using GameCult.Aetheria.State.Verse;
|
||||
using UnityEngine;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.EveRuntime
|
||||
{
|
||||
public static class AetheriaEveRuntimeUnityHookInstaller
|
||||
{
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
public static void Install()
|
||||
{
|
||||
AetheriaEveRuntimeUnityHooks.ResolveStateBoot =
|
||||
overridePath => AetheriaRuntimeStateBoot.Inspect(
|
||||
AetheriaEveRuntimeUnityClientCache.GameDataDirectory,
|
||||
overridePath ?? "");
|
||||
AetheriaEveRuntimeUnityHooks.RuntimeState =
|
||||
(stateBoot, runtimeId) => AetheriaEveRuntimeUnityClientCache.RuntimeState(stateBoot, runtimeId);
|
||||
AetheriaEveRuntimeUnityHooks.Control =
|
||||
(stateBoot, runtimeId) => AetheriaEveRuntimeUnityClientCache.Control(stateBoot, runtimeId);
|
||||
AetheriaEveRuntimeUnityHooks.Ui =
|
||||
(stateBoot, runtimeId) => AetheriaEveRuntimeUnityClientCache.Ui(stateBoot, runtimeId);
|
||||
AetheriaEveRuntimeUnityHooks.StateRefResolver =
|
||||
(stateBoot, runtimeId) =>
|
||||
AetheriaEveRuntimeUnityClientCache.EveSurfaceCultMeshStateRefResolver(stateBoot, runtimeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d60d5ddc6b3240438128f85f90464b42
|
||||
|
|
@ -1,290 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GameCult.Aetheria.State.Verse;
|
||||
using GameCult.Eve.Surface;
|
||||
using GameCult.Mesh;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using EveSurfaceDocument = GameCult.Eve.Surface.EveSurfaceDocument;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.EveRuntime
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(UIDocument))]
|
||||
public sealed class AetheriaEveSurfacePresenter : MonoBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
private string surfaceId = AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId;
|
||||
|
||||
[SerializeField]
|
||||
private string stateFilePathOverride = "";
|
||||
|
||||
[SerializeField]
|
||||
private bool mountOnEnable = true;
|
||||
|
||||
[SerializeField]
|
||||
private bool refreshInUpdate = true;
|
||||
|
||||
[SerializeField]
|
||||
private float refreshIntervalSeconds = 0.1f;
|
||||
|
||||
private UIDocument? _document;
|
||||
private float _nextRefreshTime;
|
||||
private string _mountedStatePath = "";
|
||||
private string _mountedSurfaceId = "";
|
||||
private long _mountedSurfaceVersion = -1;
|
||||
private string _mountedSurfaceUpdatedAtUtc = "";
|
||||
private CultMeshReactiveDocument<EveSurfaceDocument>? _reactiveSurfaceState;
|
||||
private string _reactiveSurfaceStatePath = "";
|
||||
private string _reactiveSurfaceId = "";
|
||||
private static readonly AetheriaEveUnitySurfaceChrome RootOnlyChrome = new AetheriaEveUnitySurfaceChrome
|
||||
{
|
||||
UseShell = false,
|
||||
RootPaddingTop = 0f,
|
||||
RootAlignItems = Align.Stretch,
|
||||
RootJustifyContent = Justify.FlexStart,
|
||||
RootPickingMode = PickingMode.Position
|
||||
};
|
||||
|
||||
public string SurfaceId
|
||||
{
|
||||
get => surfaceId;
|
||||
set => surfaceId = value ?? "";
|
||||
}
|
||||
|
||||
public string StateFilePathOverride
|
||||
{
|
||||
get => stateFilePathOverride;
|
||||
set => stateFilePathOverride = value ?? "";
|
||||
}
|
||||
|
||||
public bool RefreshInUpdate
|
||||
{
|
||||
get => refreshInUpdate;
|
||||
set => refreshInUpdate = value;
|
||||
}
|
||||
|
||||
public void Mount()
|
||||
{
|
||||
var document = ResolveDocument();
|
||||
var root = document.rootVisualElement;
|
||||
root.Clear();
|
||||
|
||||
var stateBoot = ResolveStateBoot();
|
||||
if (!stateBoot.SupportsLocalStateFileRead)
|
||||
{
|
||||
root.Add(BuildError(stateBoot.FailureMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
var statePath = stateBoot.StateFilePath;
|
||||
if (!stateBoot.StateFileExists)
|
||||
{
|
||||
root.Add(BuildError($"Aetheria state file not found: {statePath}"));
|
||||
return;
|
||||
}
|
||||
|
||||
var surface = ReadDaemonSurface(stateBoot);
|
||||
if (surface == null)
|
||||
{
|
||||
ClearMountedSurface();
|
||||
return;
|
||||
}
|
||||
|
||||
MountSurface(stateBoot, surface);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (mountOnEnable)
|
||||
Mount();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
DisposeReactiveSurfaceState();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!refreshInUpdate || Time.unscaledTime < _nextRefreshTime)
|
||||
return;
|
||||
|
||||
_nextRefreshTime = Time.unscaledTime + Mathf.Max(0.01f, refreshIntervalSeconds);
|
||||
RefreshIfChanged();
|
||||
}
|
||||
|
||||
private void RefreshIfChanged()
|
||||
{
|
||||
var stateBoot = ResolveStateBoot();
|
||||
if (!stateBoot.SupportsLocalStateFileRead || !stateBoot.StateFileExists)
|
||||
return;
|
||||
|
||||
var surface = ReadDaemonSurface(stateBoot);
|
||||
if (surface == null || !ShouldMountSurface(stateBoot.StateFilePath, surface))
|
||||
return;
|
||||
|
||||
MountSurface(stateBoot, surface);
|
||||
}
|
||||
|
||||
private EveSurfaceDocument? ReadDaemonSurface(AetheriaRuntimeStateBootReport stateBoot)
|
||||
{
|
||||
var surface = ReadDaemonSurfaceState(stateBoot);
|
||||
return surface == null
|
||||
? null
|
||||
: AetheriaRuntimeSurfaceDocuments.ToEveSurfaceDocument(
|
||||
surface,
|
||||
AetheriaEveRuntimeUnityHooks.TryCreateStateRefResolver(
|
||||
stateBoot,
|
||||
"unity-eve-surface-presenter"));
|
||||
}
|
||||
|
||||
private EveSurfaceDocument? ReadDaemonSurfaceState(
|
||||
AetheriaRuntimeStateBootReport stateBoot)
|
||||
{
|
||||
var reactive = ResolveReactiveDaemonSurfaceState(stateBoot);
|
||||
return reactive?.Current;
|
||||
}
|
||||
|
||||
private CultMeshReactiveDocument<EveSurfaceDocument>? ResolveReactiveDaemonSurfaceState(
|
||||
AetheriaRuntimeStateBootReport stateBoot)
|
||||
{
|
||||
if (_reactiveSurfaceState != null &&
|
||||
string.Equals(_reactiveSurfaceStatePath, stateBoot.StateFilePath, StringComparison.Ordinal) &&
|
||||
string.Equals(_reactiveSurfaceId, surfaceId, StringComparison.Ordinal))
|
||||
{
|
||||
return _reactiveSurfaceState;
|
||||
}
|
||||
|
||||
DisposeReactiveSurfaceState();
|
||||
_reactiveSurfaceState = CreateReactiveDaemonSurfaceState(stateBoot);
|
||||
if (_reactiveSurfaceState != null)
|
||||
{
|
||||
_reactiveSurfaceStatePath = stateBoot.StateFilePath;
|
||||
_reactiveSurfaceId = surfaceId ?? "";
|
||||
}
|
||||
|
||||
return _reactiveSurfaceState;
|
||||
}
|
||||
|
||||
private CultMeshReactiveDocument<EveSurfaceDocument>? CreateReactiveDaemonSurfaceState(
|
||||
AetheriaRuntimeStateBootReport stateBoot)
|
||||
{
|
||||
try
|
||||
{
|
||||
return AetheriaEveRuntimeUnityHooks
|
||||
.RequireRuntimeState(stateBoot, "unity-eve-surface-presenter")
|
||||
.EveSurfaceDocument(surfaceId)
|
||||
?.Reactive();
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private UIDocument ResolveDocument()
|
||||
{
|
||||
if (_document == null)
|
||||
_document = GetComponent<UIDocument>();
|
||||
return _document;
|
||||
}
|
||||
|
||||
private AetheriaRuntimeStateBootReport ResolveStateBoot()
|
||||
{
|
||||
return AetheriaEveRuntimeUnityHooks.RequireStateBoot(stateFilePathOverride);
|
||||
}
|
||||
|
||||
private static VisualElement BuildError(string message)
|
||||
{
|
||||
var container = new VisualElement();
|
||||
container.AddToClassList("aetheria-eve-runtime-error");
|
||||
var label = new Label(message);
|
||||
label.AddToClassList("aetheria-eve-runtime-error-label");
|
||||
container.Add(label);
|
||||
return container;
|
||||
}
|
||||
|
||||
private bool ShouldMountSurface(string statePath, EveSurfaceDocument surface)
|
||||
{
|
||||
return !string.Equals(_mountedStatePath, statePath, StringComparison.Ordinal) ||
|
||||
!string.Equals(_mountedSurfaceId, surface.Surface.Id, StringComparison.Ordinal) ||
|
||||
_mountedSurfaceVersion != surface.Version ||
|
||||
!string.Equals(_mountedSurfaceUpdatedAtUtc, surface.UpdatedAtUtc, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private void ClearMountedSurface()
|
||||
{
|
||||
var document = ResolveDocument();
|
||||
document.rootVisualElement.Clear();
|
||||
_mountedStatePath = "";
|
||||
_mountedSurfaceId = "";
|
||||
_mountedSurfaceVersion = -1;
|
||||
_mountedSurfaceUpdatedAtUtc = "";
|
||||
}
|
||||
|
||||
private void MountSurface(AetheriaRuntimeStateBootReport stateBoot, EveSurfaceDocument surface)
|
||||
{
|
||||
var statePath = stateBoot.StateFilePath;
|
||||
AetheriaEveUnitySurfaceHost.Render(
|
||||
transform,
|
||||
ResolveDocument(),
|
||||
"Aetheria Eve Surface",
|
||||
surface,
|
||||
request => EmitCommand(stateBoot, request),
|
||||
RootOnlyChrome,
|
||||
AetheriaEveRuntimeUnityHooks.TryCreateStateRefResolver(
|
||||
stateBoot,
|
||||
"unity-eve-surface-presenter"));
|
||||
_mountedStatePath = statePath;
|
||||
_mountedSurfaceId = surface.Surface.Id;
|
||||
_mountedSurfaceVersion = surface.Version;
|
||||
_mountedSurfaceUpdatedAtUtc = surface.UpdatedAtUtc ?? "";
|
||||
}
|
||||
|
||||
private void EmitCommand(AetheriaRuntimeStateBootReport stateBoot, EveSurfaceCommandRequest request)
|
||||
{
|
||||
var clientId = string.IsNullOrWhiteSpace(request.ClientId)
|
||||
? "unity-eve-surface-presenter"
|
||||
: request.ClientId;
|
||||
var control = AetheriaEveRuntimeUnityHooks.RequireControl(
|
||||
stateBoot,
|
||||
clientId);
|
||||
if (control.TrySubmitSurfaceCommand(request, out var daemonEnvelope))
|
||||
{
|
||||
Debug.Log(
|
||||
$"Submitted Aetheria daemon operation from Eve surface: {daemonEnvelope!.Kind} {daemonEnvelope.CommandId}");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var envelope = AetheriaEveRuntimeUnityHooks
|
||||
.RequireUi(stateBoot, "unity-eve-surface-presenter")
|
||||
.SurfaceCommandAsync(
|
||||
request,
|
||||
string.IsNullOrWhiteSpace(request.ClientId) ? "unity-uitoolkit" : request.ClientId)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
|
||||
Debug.Log(
|
||||
$"Submitted Eve operation for CultMesh bridge: {envelope.OperationId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"Ignored or failed Aetheria Eve command: {request.ProviderId}/{request.SurfaceId}/{request.Operation?.OperationId}: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeReactiveSurfaceState()
|
||||
{
|
||||
_reactiveSurfaceState?.Dispose();
|
||||
_reactiveSurfaceState = null;
|
||||
_reactiveSurfaceStatePath = "";
|
||||
_reactiveSurfaceId = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f538568d3b6341c43adeb7e023573756
|
||||
|
|
@ -1,464 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using GameCult.Aetheria.State.Verse;
|
||||
using GameCult.Eve.Surface;
|
||||
using GameCult.Eve.UnityScene;
|
||||
using GameCult.Mesh;
|
||||
using UnityEngine;
|
||||
using EveSurfaceDocument = GameCult.Eve.Surface.EveSurfaceDocument;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.EveRuntime
|
||||
{
|
||||
public sealed class AetheriaEveUnitySceneProviderBridge :
|
||||
IEveUnitySceneLiveProviderTransport,
|
||||
IEveUnitySceneProviderSurfaceDocumentSource,
|
||||
IEveUnityPlayableWorldAssetManifestDocumentSource,
|
||||
IEveUnitySceneCommandSink,
|
||||
IEveUnitySceneCommandReceiptSource,
|
||||
IEveUnityProviderRefreshSource,
|
||||
IDisposable
|
||||
{
|
||||
private readonly string _stateFilePathOverride;
|
||||
private readonly string _surfaceId;
|
||||
private readonly string _runtimeId;
|
||||
private readonly string _cultMeshEndpoint;
|
||||
private AetheriaRuntimeStateBootReport? _stateBoot;
|
||||
private AetheriaClientState? _runtimeState;
|
||||
private AetheriaClient? _remoteClient;
|
||||
private readonly AetheriaEveUnityRemoteReceiptTracker _remoteReceipts =
|
||||
new AetheriaEveUnityRemoteReceiptTracker();
|
||||
private event Action<EveUnityPlayableWorldAssetManifestDocument>? AssetManifestDocumentAvailable;
|
||||
|
||||
public AetheriaEveUnitySceneProviderBridge(
|
||||
string stateFilePathOverride = "",
|
||||
string surfaceId = AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId,
|
||||
string runtimeId = "unity-scene",
|
||||
string cultMeshEndpoint = "")
|
||||
{
|
||||
_stateFilePathOverride = stateFilePathOverride ?? "";
|
||||
_surfaceId = string.IsNullOrWhiteSpace(surfaceId)
|
||||
? AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId
|
||||
: surfaceId;
|
||||
_runtimeId = string.IsNullOrWhiteSpace(runtimeId) ? "unity-scene" : runtimeId;
|
||||
_cultMeshEndpoint = cultMeshEndpoint?.Trim() ?? "";
|
||||
CurrentDocument = CreateSurfaceDocument(AetheriaRuntimeSurfaceDocuments.EmptySurface(_surfaceId), 0);
|
||||
CurrentDocumentManifest = new EveUnityPlayableWorldAssetManifestDocument(
|
||||
AetheriaRuntimeVerseRecordKeys.DaemonAssetManifest.ToString(),
|
||||
Array.Empty<EveUnityPlayableWorldAssetManifestDocumentEntry>(),
|
||||
"aetheria.daemon");
|
||||
}
|
||||
|
||||
public string TransportKind => IsRemote
|
||||
? "aetheria-remote-cultmesh-replica"
|
||||
: "aetheria-local-cultmesh-replica";
|
||||
|
||||
public string SurfacePointer => CurrentDocument.SourcePointer;
|
||||
|
||||
public string AssetManifestPointer => CurrentDocumentManifest.ManifestRef;
|
||||
|
||||
public string SinkKind => "aetheria-daemon-command-boundary";
|
||||
|
||||
public string ManifestRef => CurrentDocumentManifest.ManifestRef;
|
||||
|
||||
public EveUnitySceneProviderSurfaceDocument CurrentDocument { get; private set; }
|
||||
|
||||
EveUnitySceneProviderSurfaceDocument IEveUnitySceneLiveProviderTransport.CurrentSurfaceDocument =>
|
||||
CurrentDocument;
|
||||
|
||||
EveUnityPlayableWorldAssetManifestDocument IEveUnityPlayableWorldAssetManifestDocumentSource.CurrentDocument =>
|
||||
CurrentDocumentManifest;
|
||||
|
||||
EveUnityPlayableWorldAssetManifestDocument IEveUnitySceneLiveProviderTransport.CurrentAssetManifestDocument =>
|
||||
CurrentDocumentManifest;
|
||||
|
||||
public EveUnityPlayableWorldAssetManifestDocument CurrentDocumentManifest { get; private set; }
|
||||
|
||||
public event Action<EveUnitySceneProviderSurfaceDocument>? DocumentAvailable;
|
||||
|
||||
public event Action<EveUnitySceneCommandReceipt>? ReceiptAvailable;
|
||||
|
||||
event Action<EveUnitySceneProviderSurfaceDocument> IEveUnitySceneLiveProviderTransport.SurfaceDocumentAvailable
|
||||
{
|
||||
add => DocumentAvailable += value;
|
||||
remove => DocumentAvailable -= value;
|
||||
}
|
||||
|
||||
event Action<EveUnityPlayableWorldAssetManifestDocument> IEveUnityPlayableWorldAssetManifestDocumentSource.DocumentAvailable
|
||||
{
|
||||
add => AssetManifestDocumentAvailable += value;
|
||||
remove => AssetManifestDocumentAvailable -= value;
|
||||
}
|
||||
|
||||
event Action<EveUnityPlayableWorldAssetManifestDocument> IEveUnitySceneLiveProviderTransport.AssetManifestDocumentAvailable
|
||||
{
|
||||
add => AssetManifestDocumentAvailable += value;
|
||||
remove => AssetManifestDocumentAvailable -= value;
|
||||
}
|
||||
|
||||
event Action<EveUnitySceneCommandReceipt> IEveUnitySceneLiveProviderTransport.CommandReceiptAvailable
|
||||
{
|
||||
add => ReceiptAvailable += value;
|
||||
remove => ReceiptAvailable -= value;
|
||||
}
|
||||
|
||||
public void Connect()
|
||||
{
|
||||
Refresh();
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
if (IsRemote)
|
||||
{
|
||||
var remote = ResolveRemoteClient();
|
||||
remote.RefreshRemoteAsync().GetAwaiter().GetResult();
|
||||
RefreshFromRuntimeState(remote.State, null);
|
||||
return;
|
||||
}
|
||||
|
||||
var stateBoot = ResolveStateBoot();
|
||||
if (!stateBoot.SupportsLocalStateFileRead || !stateBoot.StateFileExists)
|
||||
return;
|
||||
|
||||
RefreshFromRuntimeState(ResolveRuntimeState(stateBoot), stateBoot);
|
||||
}
|
||||
|
||||
private void RefreshFromRuntimeState(
|
||||
AetheriaClientState runtimeState,
|
||||
AetheriaRuntimeStateBootReport? stateBoot)
|
||||
{
|
||||
var surface = ReadSurface(runtimeState, stateBoot);
|
||||
if (surface != null)
|
||||
{
|
||||
CurrentDocument = CreateSurfaceDocument(
|
||||
surface,
|
||||
surface.Version,
|
||||
ReadAdvertisedSurface(runtimeState));
|
||||
DocumentAvailable?.Invoke(CurrentDocument);
|
||||
}
|
||||
|
||||
CurrentDocumentManifest = ReadAssetManifest(runtimeState);
|
||||
AssetManifestDocumentAvailable?.Invoke(CurrentDocumentManifest);
|
||||
ReconcileRemoteReceipts();
|
||||
}
|
||||
|
||||
public void Submit(EveSurfaceCommandRequest request)
|
||||
{
|
||||
if (request == null) throw new ArgumentNullException(nameof(request));
|
||||
var clientId = string.IsNullOrWhiteSpace(request.ClientId) ? _runtimeId : request.ClientId;
|
||||
if (IsRemote)
|
||||
{
|
||||
var remote = ResolveRemoteClient();
|
||||
if (!remote.Control.TrySubmitSurfaceCommand(request, out var submitted))
|
||||
throw new InvalidOperationException($"Aetheria daemon did not advertise command {request.Operation?.OperationId ?? "unknown"}.");
|
||||
|
||||
Debug.Log(
|
||||
$"Forwarded Aetheria operation to remote CultMesh authority: {submitted!.Kind} {submitted.CommandId}");
|
||||
_remoteReceipts.Track(submitted.CommandId, request);
|
||||
return;
|
||||
}
|
||||
|
||||
var stateBoot = ResolveStateBoot();
|
||||
var control = AetheriaEveRuntimeUnityHooks.RequireControl(stateBoot, clientId);
|
||||
if (control.TrySubmitSurfaceCommand(request, out var daemonEnvelope))
|
||||
{
|
||||
Debug.Log(
|
||||
$"Submitted Aetheria daemon operation from Eve Unity scene: {daemonEnvelope!.Kind} {daemonEnvelope.CommandId}");
|
||||
ReceiptAvailable?.Invoke(ToReceipt(request, daemonEnvelope));
|
||||
return;
|
||||
}
|
||||
|
||||
var envelope = AetheriaEveRuntimeUnityHooks
|
||||
.RequireUi(stateBoot, _runtimeId)
|
||||
.SurfaceCommandAsync(request, clientId)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
Debug.Log($"Submitted Eve Unity scene operation for CultMesh bridge: {envelope.OperationId}");
|
||||
ReceiptAvailable?.Invoke(ToReceipt(request, envelope));
|
||||
}
|
||||
|
||||
public void SubmitCommand(EveSurfaceCommandRequest request)
|
||||
{
|
||||
Submit(request);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_remoteClient?.Dispose();
|
||||
_remoteClient = null;
|
||||
_remoteReceipts.Clear();
|
||||
}
|
||||
|
||||
private bool IsRemote => !string.IsNullOrWhiteSpace(_cultMeshEndpoint);
|
||||
|
||||
private AetheriaClient ResolveRemoteClient()
|
||||
{
|
||||
if (_remoteClient != null)
|
||||
return _remoteClient;
|
||||
|
||||
var replicaPath = string.IsNullOrWhiteSpace(_stateFilePathOverride)
|
||||
? System.IO.Path.Combine(Application.persistentDataPath, "aetheria-remote-replica.cc")
|
||||
: _stateFilePathOverride;
|
||||
_remoteClient = AetheriaClient
|
||||
.OpenRemoteAsync(
|
||||
replicaPath,
|
||||
_cultMeshEndpoint,
|
||||
_runtimeId,
|
||||
sessionId: "eve-unity",
|
||||
synchronizeOnOpen: false)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
return _remoteClient;
|
||||
}
|
||||
|
||||
private void ReconcileRemoteReceipts()
|
||||
{
|
||||
if (!IsRemote || !_remoteReceipts.HasPending || _remoteClient == null)
|
||||
return;
|
||||
|
||||
foreach (var receipt in _remoteReceipts.Reconcile(
|
||||
_remoteClient.CommittedCommandFacts(),
|
||||
CurrentDocument.Version))
|
||||
ReceiptAvailable?.Invoke(receipt);
|
||||
}
|
||||
|
||||
private AetheriaRuntimeStateBootReport ResolveStateBoot()
|
||||
{
|
||||
var stateBoot = AetheriaEveRuntimeUnityHooks.RequireStateBoot(_stateFilePathOverride);
|
||||
_stateBoot = stateBoot;
|
||||
return stateBoot;
|
||||
}
|
||||
|
||||
private AetheriaClientState ResolveRuntimeState(AetheriaRuntimeStateBootReport stateBoot)
|
||||
{
|
||||
var runtimeState = AetheriaEveRuntimeUnityHooks.RequireRuntimeState(stateBoot, _runtimeId);
|
||||
_runtimeState = runtimeState;
|
||||
return runtimeState;
|
||||
}
|
||||
|
||||
private EveSurfaceDocument? ReadSurface(
|
||||
AetheriaClientState runtimeState,
|
||||
AetheriaRuntimeStateBootReport? stateBoot)
|
||||
{
|
||||
try
|
||||
{
|
||||
var handle = runtimeState.EveSurfaceDocument(_surfaceId);
|
||||
if (handle == null)
|
||||
return null;
|
||||
|
||||
var resolver = stateBoot == null
|
||||
? null
|
||||
: AetheriaEveRuntimeUnityHooks.TryCreateStateRefResolver(stateBoot.Value, _runtimeId);
|
||||
return AetheriaRuntimeSurfaceDocuments.ToEveSurfaceDocument(handle.Latest(), resolver);
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private EveUnityPlayableWorldAssetManifestDocument ReadAssetManifest(AetheriaClientState runtimeState)
|
||||
{
|
||||
var manifest = runtimeState.AssetManifest.Latest();
|
||||
return ToUnityPlayableWorldAssetManifest(manifest);
|
||||
}
|
||||
|
||||
private AetheriaRuntimeEveSurfaceAdvertisement? ReadAdvertisedSurface(AetheriaClientState runtimeState)
|
||||
{
|
||||
var provider = runtimeState.ProviderAdvertisement.Latest();
|
||||
return (provider.EveSurfaces ?? Array.Empty<AetheriaRuntimeEveSurfaceAdvertisement>())
|
||||
.FirstOrDefault(surface => surface != null && string.Equals(surface.SurfaceId, _surfaceId, StringComparison.Ordinal))
|
||||
?? AetheriaRuntimeEveSurfaceCatalog.Find(_surfaceId);
|
||||
}
|
||||
|
||||
private EveUnitySceneProviderSurfaceDocument CreateSurfaceDocument(
|
||||
EveSurfaceDocument surface,
|
||||
long version,
|
||||
AetheriaRuntimeEveSurfaceAdvertisement? advertisedSurface = null)
|
||||
{
|
||||
advertisedSurface ??= AetheriaRuntimeEveSurfaceCatalog.Find(_surfaceId);
|
||||
var worldInteraction = advertisedSurface?.WorldInteraction;
|
||||
return new EveUnitySceneProviderSurfaceDocument(
|
||||
surface,
|
||||
new EveUnitySceneProviderSurfaceAdvertisement(
|
||||
_surfaceId,
|
||||
advertisedSurface?.SurfaceKind ?? "",
|
||||
new EveUnitySceneWorldInteraction(
|
||||
worldInteraction?.ProjectionKind ?? "",
|
||||
worldInteraction?.CommandBoundary ?? "",
|
||||
worldInteraction?.ReceiptSchema ?? "",
|
||||
worldInteraction?.Ownership ?? "")),
|
||||
SourcePointer(advertisedSurface),
|
||||
version);
|
||||
}
|
||||
|
||||
private string SourcePointer(AetheriaRuntimeEveSurfaceAdvertisement? advertisedSurface)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(advertisedSurface?.RecordRef))
|
||||
return advertisedSurface.RecordRef;
|
||||
|
||||
var path = _stateBoot?.StateFilePath ?? _stateFilePathOverride;
|
||||
return string.IsNullOrWhiteSpace(path)
|
||||
? AetheriaRuntimeVerseRecordKeys.DaemonGameSurface.ToString()
|
||||
: $"{path}#{_surfaceId}";
|
||||
}
|
||||
|
||||
private static EveUnitySceneCommandReceipt ToReceipt(
|
||||
EveSurfaceCommandRequest request,
|
||||
AetheriaRuntimeDaemonCommandEnvelope envelope)
|
||||
{
|
||||
if (request == null) throw new ArgumentNullException(nameof(request));
|
||||
if (envelope == null) throw new ArgumentNullException(nameof(envelope));
|
||||
|
||||
return new EveUnitySceneCommandReceipt(
|
||||
string.IsNullOrWhiteSpace(envelope.CommandId) ? envelope.OperationId : envelope.CommandId,
|
||||
envelope.OperationId,
|
||||
envelope.CommandId,
|
||||
envelope.Accepted ? "accepted" : "denied",
|
||||
"Aetheria",
|
||||
"aetheria-daemon-command-boundary",
|
||||
request.ReceiptSchema,
|
||||
request.ProviderId,
|
||||
request.SurfaceId,
|
||||
envelope.Diagnostic ?? "",
|
||||
ParseIssuedAt(envelope.IssuedAtUtc, request.IssuedAt));
|
||||
}
|
||||
|
||||
private static EveUnitySceneCommandReceipt ToReceipt(
|
||||
EveSurfaceCommandRequest request,
|
||||
CultMeshOperationReceipt receipt)
|
||||
{
|
||||
if (request == null) throw new ArgumentNullException(nameof(request));
|
||||
if (receipt == null) throw new ArgumentNullException(nameof(receipt));
|
||||
|
||||
var commandId = request.Operation?.IdempotencyKey ?? "";
|
||||
return new EveUnitySceneCommandReceipt(
|
||||
string.IsNullOrWhiteSpace(commandId) ? receipt.OperationId : commandId,
|
||||
receipt.OperationId,
|
||||
commandId,
|
||||
receipt.Accepted ? "accepted" : "denied",
|
||||
"Aetheria",
|
||||
"aetheria-eve-command-boundary",
|
||||
request.ReceiptSchema,
|
||||
request.ProviderId,
|
||||
request.SurfaceId,
|
||||
receipt.Diagnostic ?? "",
|
||||
request.IssuedAt);
|
||||
}
|
||||
|
||||
private static DateTimeOffset ParseIssuedAt(string value, DateTimeOffset fallback)
|
||||
{
|
||||
return DateTimeOffset.TryParse(value, out var parsed) ? parsed : fallback;
|
||||
}
|
||||
|
||||
private static EveUnityPlayableWorldAssetManifestDocument ToUnityPlayableWorldAssetManifest(
|
||||
AetheriaRuntimeAssetManifestDocument manifest)
|
||||
{
|
||||
var entries = (manifest.Assets ?? Array.Empty<AetheriaRuntimeAssetManifestEntry>())
|
||||
.Where(entry => entry?.Ref != null && !string.IsNullOrWhiteSpace(entry.Ref.AssetKey))
|
||||
.Select(entry => new EveUnityPlayableWorldAssetManifestDocumentEntry(
|
||||
entry.Ref.AssetKey,
|
||||
EntityKind(entry),
|
||||
ResourcePath(entry.Ref),
|
||||
entry.Ref.AssetKey,
|
||||
"provider-asset-ref"))
|
||||
.ToArray();
|
||||
|
||||
return new EveUnityPlayableWorldAssetManifestDocument(
|
||||
AetheriaRuntimeVerseRecordKeys.DaemonAssetManifest.ToString(),
|
||||
entries,
|
||||
"aetheria.daemon");
|
||||
}
|
||||
|
||||
private static string EntityKind(AetheriaRuntimeAssetManifestEntry entry)
|
||||
{
|
||||
if (entry.Ref.Metadata != null &&
|
||||
entry.Ref.Metadata.TryGetValue("presentationRole", out var role) &&
|
||||
!string.IsNullOrWhiteSpace(role))
|
||||
return role;
|
||||
|
||||
return (entry.Tags ?? Array.Empty<string>())
|
||||
.FirstOrDefault(tag => !string.IsNullOrWhiteSpace(tag) && !string.Equals(tag, "map", StringComparison.OrdinalIgnoreCase) && !string.Equals(tag, "icon", StringComparison.OrdinalIgnoreCase))
|
||||
?? "";
|
||||
}
|
||||
|
||||
private static string ResourcePath(AetheriaRuntimeAssetRef asset)
|
||||
{
|
||||
if (asset.Metadata != null && asset.Metadata.TryGetValue("resourcesPath", out var resourcesPath))
|
||||
return resourcesPath ?? "";
|
||||
|
||||
return string.IsNullOrWhiteSpace(asset.AssetKey)
|
||||
? ""
|
||||
: asset.AssetKey.Replace('.', '/');
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AetheriaEveUnityRemoteReceiptTracker
|
||||
{
|
||||
private readonly Dictionary<string, EveSurfaceCommandRequest> _pending =
|
||||
new Dictionary<string, EveSurfaceCommandRequest>(StringComparer.Ordinal);
|
||||
|
||||
public bool HasPending => _pending.Count > 0;
|
||||
|
||||
public void Track(string commandId, EveSurfaceCommandRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(commandId))
|
||||
throw new ArgumentException("Remote command id must be non-empty.", nameof(commandId));
|
||||
_pending[commandId] = request ?? throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
|
||||
public IReadOnlyList<EveUnitySceneCommandReceipt> Reconcile(
|
||||
IReadOnlyList<AetheriaRuntimeCommittedCommandFactDocument> facts,
|
||||
long observedSurfaceVersion)
|
||||
{
|
||||
if (facts == null) throw new ArgumentNullException(nameof(facts));
|
||||
var receipts = new List<EveUnitySceneCommandReceipt>();
|
||||
var byCommandId = facts
|
||||
.Where(fact => fact != null && !string.IsNullOrWhiteSpace(fact.CommandId))
|
||||
.GroupBy(fact => fact.CommandId, StringComparer.Ordinal)
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
group => group.OrderByDescending(fact => fact.SourceFrameId).First(),
|
||||
StringComparer.Ordinal);
|
||||
foreach (var commandId in _pending.Keys.ToArray())
|
||||
{
|
||||
if (!byCommandId.TryGetValue(commandId, out var fact) ||
|
||||
observedSurfaceVersion < fact.SourceFrameId)
|
||||
continue;
|
||||
|
||||
var request = _pending[commandId];
|
||||
_pending.Remove(commandId);
|
||||
var accepted = string.Equals(
|
||||
fact.Outcome,
|
||||
AetheriaRuntimeCommandFactOutcomes.Applied,
|
||||
StringComparison.Ordinal);
|
||||
receipts.Add(new EveUnitySceneCommandReceipt(
|
||||
fact.FactId,
|
||||
request.Operation?.OperationId ?? "",
|
||||
commandId,
|
||||
accepted ? "accepted" : "denied",
|
||||
"Aetheria",
|
||||
fact.SourceDaemonId,
|
||||
AetheriaRuntimeDaemonSchemas.CommittedCommandFact,
|
||||
request.ProviderId,
|
||||
request.SurfaceId,
|
||||
accepted ? "Command applied by authoritative daemon." : "Command rejected by authoritative daemon.",
|
||||
DateTimeOffset.TryParse(fact.CommittedAtUtc, out var committedAt) ? committedAt : request.IssuedAt,
|
||||
fact.SourceFrameId));
|
||||
}
|
||||
return receipts;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_pending.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3e2e1c6c7bb24b44b89529166d1676a2
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
using System;
|
||||
using GameCult.Aetheria.State.Verse;
|
||||
using GameCult.Eve.Surface;
|
||||
using GameCult.Eve.UnityScene;
|
||||
using UnityEngine;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.EveRuntime
|
||||
{
|
||||
public sealed class AetheriaEveUnitySceneProviderComponent :
|
||||
EveUnitySceneLiveProviderTransportBehaviour,
|
||||
IEveUnitySceneProviderSurfaceDocumentSource,
|
||||
IEveUnityPlayableWorldAssetManifestDocumentSource,
|
||||
IEveUnitySceneCommandSink,
|
||||
IEveUnitySceneCommandReceiptSource,
|
||||
IEveUnityProviderRefreshSource,
|
||||
IDisposable
|
||||
{
|
||||
[SerializeField] private string stateFilePathOverride = "";
|
||||
[SerializeField] private string cultMeshEndpoint = "";
|
||||
[SerializeField] private string surfaceId = AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId;
|
||||
[SerializeField] private string runtimeId = "unity-scene";
|
||||
[SerializeField] private bool refreshOnEnable = true;
|
||||
|
||||
private AetheriaEveUnitySceneProviderBridge? _bridge;
|
||||
|
||||
public override string TransportKind => Bridge.TransportKind;
|
||||
|
||||
public override string SurfacePointer => Bridge.SurfacePointer;
|
||||
|
||||
public override string AssetManifestPointer => Bridge.AssetManifestPointer;
|
||||
|
||||
public override EveUnitySceneProviderSurfaceDocument CurrentSurfaceDocument =>
|
||||
((IEveUnitySceneLiveProviderTransport)Bridge).CurrentSurfaceDocument;
|
||||
|
||||
public override EveUnityPlayableWorldAssetManifestDocument CurrentAssetManifestDocument =>
|
||||
((IEveUnitySceneLiveProviderTransport)Bridge).CurrentAssetManifestDocument;
|
||||
|
||||
public string SinkKind => Bridge.SinkKind;
|
||||
|
||||
public string ManifestRef => ((IEveUnityPlayableWorldAssetManifestDocumentSource)Bridge).ManifestRef;
|
||||
|
||||
public EveUnitySceneProviderSurfaceDocument CurrentDocument => Bridge.CurrentDocument;
|
||||
|
||||
EveUnityPlayableWorldAssetManifestDocument IEveUnityPlayableWorldAssetManifestDocumentSource.CurrentDocument =>
|
||||
((IEveUnityPlayableWorldAssetManifestDocumentSource)Bridge).CurrentDocument;
|
||||
|
||||
public event Action<EveUnitySceneProviderSurfaceDocument>? DocumentAvailable
|
||||
{
|
||||
add => Bridge.DocumentAvailable += value;
|
||||
remove => Bridge.DocumentAvailable -= value;
|
||||
}
|
||||
|
||||
public override event Action<EveUnitySceneProviderSurfaceDocument> SurfaceDocumentAvailable
|
||||
{
|
||||
add => ((IEveUnitySceneLiveProviderTransport)Bridge).SurfaceDocumentAvailable += value;
|
||||
remove => ((IEveUnitySceneLiveProviderTransport)Bridge).SurfaceDocumentAvailable -= value;
|
||||
}
|
||||
|
||||
public event Action<EveUnitySceneCommandReceipt>? ReceiptAvailable
|
||||
{
|
||||
add => Bridge.ReceiptAvailable += value;
|
||||
remove => Bridge.ReceiptAvailable -= value;
|
||||
}
|
||||
|
||||
public override event Action<EveUnitySceneCommandReceipt> CommandReceiptAvailable
|
||||
{
|
||||
add => ((IEveUnitySceneLiveProviderTransport)Bridge).CommandReceiptAvailable += value;
|
||||
remove => ((IEveUnitySceneLiveProviderTransport)Bridge).CommandReceiptAvailable -= value;
|
||||
}
|
||||
|
||||
event Action<EveUnityPlayableWorldAssetManifestDocument> IEveUnityPlayableWorldAssetManifestDocumentSource.DocumentAvailable
|
||||
{
|
||||
add => ((IEveUnityPlayableWorldAssetManifestDocumentSource)Bridge).DocumentAvailable += value;
|
||||
remove => ((IEveUnityPlayableWorldAssetManifestDocumentSource)Bridge).DocumentAvailable -= value;
|
||||
}
|
||||
|
||||
public override event Action<EveUnityPlayableWorldAssetManifestDocument> AssetManifestDocumentAvailable
|
||||
{
|
||||
add => ((IEveUnitySceneLiveProviderTransport)Bridge).AssetManifestDocumentAvailable += value;
|
||||
remove => ((IEveUnitySceneLiveProviderTransport)Bridge).AssetManifestDocumentAvailable -= value;
|
||||
}
|
||||
|
||||
public void Configure(
|
||||
string stateFilePath,
|
||||
string surfaceId = AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId,
|
||||
string runtimeId = "unity-scene",
|
||||
string endpoint = "")
|
||||
{
|
||||
Dispose();
|
||||
stateFilePathOverride = stateFilePath ?? "";
|
||||
this.surfaceId = string.IsNullOrWhiteSpace(surfaceId)
|
||||
? AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId
|
||||
: surfaceId;
|
||||
this.runtimeId = string.IsNullOrWhiteSpace(runtimeId)
|
||||
? "unity-scene"
|
||||
: runtimeId;
|
||||
cultMeshEndpoint = endpoint ?? "";
|
||||
}
|
||||
|
||||
public override void Connect()
|
||||
{
|
||||
Bridge.Connect();
|
||||
}
|
||||
|
||||
public override void Disconnect()
|
||||
{
|
||||
Bridge.Disconnect();
|
||||
}
|
||||
|
||||
public override void Refresh()
|
||||
{
|
||||
Bridge.Refresh();
|
||||
}
|
||||
|
||||
public void Submit(EveSurfaceCommandRequest request)
|
||||
{
|
||||
Bridge.Submit(request);
|
||||
}
|
||||
|
||||
public override void SubmitCommand(EveSurfaceCommandRequest request)
|
||||
{
|
||||
Bridge.SubmitCommand(request);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_bridge?.Dispose();
|
||||
_bridge = null;
|
||||
}
|
||||
|
||||
private AetheriaEveUnitySceneProviderBridge Bridge
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_bridge == null)
|
||||
{
|
||||
_bridge = new AetheriaEveUnitySceneProviderBridge(
|
||||
stateFilePathOverride,
|
||||
surfaceId,
|
||||
runtimeId,
|
||||
cultMeshEndpoint);
|
||||
}
|
||||
|
||||
return _bridge;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (refreshOnEnable)
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 456b6126c7330234799fc1609e117b9a
|
||||
|
|
@ -1,411 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using GameCult.Aetheria.State.Verse;
|
||||
using GameCult.Eve.Surface;
|
||||
using GameCult.Eve.UnityUIToolkit;
|
||||
using GameCult.Mesh;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TextCore.LowLevel;
|
||||
using UnityEngine.UIElements;
|
||||
using TextCoreFontAsset = UnityEngine.TextCore.Text.FontAsset;
|
||||
|
||||
namespace GameCult.Aetheria.EveRuntime
|
||||
{
|
||||
public sealed class AetheriaEveUnitySurfaceChrome
|
||||
{
|
||||
public float RootPaddingTop = 16f;
|
||||
public Align RootAlignItems = Align.Center;
|
||||
public Justify RootJustifyContent = Justify.FlexStart;
|
||||
public PickingMode RootPickingMode = PickingMode.Ignore;
|
||||
public float RootPaddingLeft = 0f;
|
||||
public float RootPaddingRight = 0f;
|
||||
public float RootPaddingBottom = 0f;
|
||||
public Color RootBackgroundColor = new Color(0f, 0f, 0f, 0f);
|
||||
|
||||
public float Width = 0f;
|
||||
public float MinWidth = 420f;
|
||||
public float MaxWidth = 760f;
|
||||
public float MaxHeight = 0f;
|
||||
public float FlexGrow = 0f;
|
||||
public FlexDirection FlexDirection = FlexDirection.Column;
|
||||
public float PaddingLeft = 16f;
|
||||
public float PaddingRight = 16f;
|
||||
public float PaddingTop = 12f;
|
||||
public float PaddingBottom = 12f;
|
||||
public float BorderRadius = 8f;
|
||||
public float BorderWidth = 1f;
|
||||
public Color BackgroundColor = new Color(0.08f, 0.1f, 0.14f, 0.94f);
|
||||
public Color BorderColor = new Color(0.3f, 0.47f, 0.71f, 0.8f);
|
||||
public PickingMode ShellPickingMode = PickingMode.Position;
|
||||
public bool UseShell = true;
|
||||
}
|
||||
|
||||
public static class AetheriaEveUnitySurfaceHost
|
||||
{
|
||||
private static PanelSettings _runtimePanelSettings;
|
||||
private static readonly Dictionary<string, TextCoreFontAsset> FontCache = new Dictionary<string, TextCoreFontAsset>(StringComparer.Ordinal);
|
||||
|
||||
public static UIDocument Render(
|
||||
Transform owner,
|
||||
UIDocument document,
|
||||
string hostName,
|
||||
EveSurfaceDocument surface,
|
||||
Action<EveSurfaceCommandRequest> commandHandler,
|
||||
AetheriaEveUnitySurfaceChrome chrome,
|
||||
CultMeshStateRefResolver stateRefResolver = null,
|
||||
Func<EveEmbeddedDocumentSlot, EveSurfaceDocument> embeddedDocumentResolver = null,
|
||||
int sortingOrder = 1000)
|
||||
{
|
||||
if (owner == null)
|
||||
throw new ArgumentNullException(nameof(owner));
|
||||
if (surface == null)
|
||||
throw new ArgumentNullException(nameof(surface));
|
||||
if (commandHandler == null)
|
||||
throw new ArgumentNullException(nameof(commandHandler));
|
||||
|
||||
document = ResolveDocument(owner, document, hostName, sortingOrder);
|
||||
document.gameObject.SetActive(true);
|
||||
|
||||
var root = document.rootVisualElement;
|
||||
ConfigureRoot(root, chrome);
|
||||
|
||||
var effectiveStateRefResolver = stateRefResolver ?? (
|
||||
ContainsStateRefs(surface)
|
||||
? CreateDefaultStateRefResolver()
|
||||
: null);
|
||||
surface = AetheriaRuntimeSurfaceDocuments.ResolveStateRefs(surface, effectiveStateRefResolver);
|
||||
var lowerer = new EveUiToolkitSurfaceLowerer(new EveUiToolkitSurfaceOptions(embeddedDocumentResolver));
|
||||
VisualElement lowered;
|
||||
if (chrome.UseShell)
|
||||
{
|
||||
var shell = CreateShell(chrome);
|
||||
root.Add(shell);
|
||||
lowered = lowerer.Lower(surface, commandHandler);
|
||||
ApplyTheme(lowered, surface);
|
||||
shell.Add(lowered);
|
||||
}
|
||||
else
|
||||
{
|
||||
lowered = lowerer.Lower(surface, commandHandler);
|
||||
ApplyTheme(lowered, surface);
|
||||
root.Add(lowered);
|
||||
}
|
||||
|
||||
Debug.Log(
|
||||
$"Rendered Eve surface '{surface.Surface.Id}' into UIDocument '{document.name}' with panel '{document.panelSettings?.name ?? "none"}'.");
|
||||
return document;
|
||||
}
|
||||
|
||||
public static UIDocument RenderRuntime(
|
||||
Transform owner,
|
||||
UIDocument document,
|
||||
string hostName,
|
||||
AetheriaRuntimeSurfaceDocument surface,
|
||||
Action<EveSurfaceCommandRequest> commandHandler,
|
||||
AetheriaEveUnitySurfaceChrome chrome,
|
||||
CultMeshStateRefResolver stateRefResolver = null,
|
||||
Func<EveEmbeddedDocumentSlot, AetheriaRuntimeSurfaceDocument> embeddedDocumentResolver = null,
|
||||
int sortingOrder = 1000)
|
||||
{
|
||||
if (surface == null)
|
||||
throw new ArgumentNullException(nameof(surface));
|
||||
|
||||
return Render(
|
||||
owner,
|
||||
document,
|
||||
hostName,
|
||||
AetheriaRuntimeSurfaceDocuments.ToEveSurfaceDocument(surface),
|
||||
commandHandler,
|
||||
chrome,
|
||||
stateRefResolver,
|
||||
embeddedDocumentResolver == null
|
||||
? null
|
||||
: slot =>
|
||||
{
|
||||
var nested = embeddedDocumentResolver(slot);
|
||||
return nested == null ? null : AetheriaRuntimeSurfaceDocuments.ToEveSurfaceDocument(nested);
|
||||
},
|
||||
sortingOrder);
|
||||
}
|
||||
|
||||
public static void Hide(UIDocument document)
|
||||
{
|
||||
if (document == null)
|
||||
return;
|
||||
|
||||
document.rootVisualElement.Clear();
|
||||
document.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
public static void DestroyDocument(UIDocument document)
|
||||
{
|
||||
if (document == null)
|
||||
return;
|
||||
|
||||
UnityEngine.Object.Destroy(document.gameObject);
|
||||
}
|
||||
|
||||
private static UIDocument ResolveDocument(
|
||||
Transform owner,
|
||||
UIDocument document,
|
||||
string hostName,
|
||||
int sortingOrder)
|
||||
{
|
||||
if (document != null)
|
||||
{
|
||||
if (document.panelSettings == null)
|
||||
document.panelSettings = ResolveRuntimePanelSettings();
|
||||
return document;
|
||||
}
|
||||
|
||||
var host = new GameObject(string.IsNullOrWhiteSpace(hostName) ? "Aetheria Eve Surface" : hostName);
|
||||
host.SetActive(false);
|
||||
host.transform.SetParent(owner, false);
|
||||
host.layer = owner.gameObject.layer;
|
||||
document = host.AddComponent<UIDocument>();
|
||||
document.sortingOrder = sortingOrder;
|
||||
document.panelSettings = ResolveRuntimePanelSettings();
|
||||
return document;
|
||||
}
|
||||
|
||||
private static PanelSettings ResolveRuntimePanelSettings()
|
||||
{
|
||||
if (_runtimePanelSettings != null)
|
||||
return _runtimePanelSettings;
|
||||
|
||||
_runtimePanelSettings = ScriptableObject.CreateInstance<PanelSettings>();
|
||||
_runtimePanelSettings.name = "Aetheria Runtime Eve Panel Settings";
|
||||
_runtimePanelSettings.scaleMode = PanelScaleMode.ScaleWithScreenSize;
|
||||
_runtimePanelSettings.referenceResolution = new Vector2Int(1920, 1080);
|
||||
_runtimePanelSettings.screenMatchMode = PanelScreenMatchMode.MatchWidthOrHeight;
|
||||
_runtimePanelSettings.match = 0.5f;
|
||||
_runtimePanelSettings.themeStyleSheet = Resources.Load<ThemeStyleSheet>("UnityDefaultRuntimeTheme");
|
||||
if (_runtimePanelSettings.themeStyleSheet == null)
|
||||
Debug.LogWarning("Aetheria runtime Eve panel could not load UnityDefaultRuntimeTheme.tss.");
|
||||
return _runtimePanelSettings;
|
||||
}
|
||||
|
||||
private static void ConfigureRoot(VisualElement root, AetheriaEveUnitySurfaceChrome chrome)
|
||||
{
|
||||
root.Clear();
|
||||
root.style.flexGrow = 1;
|
||||
root.style.position = Position.Absolute;
|
||||
root.style.left = 0;
|
||||
root.style.top = 0;
|
||||
root.style.right = 0;
|
||||
root.style.bottom = 0;
|
||||
root.style.alignItems = chrome.RootAlignItems;
|
||||
root.style.justifyContent = chrome.RootJustifyContent;
|
||||
root.style.paddingLeft = chrome.RootPaddingLeft;
|
||||
root.style.paddingTop = chrome.RootPaddingTop;
|
||||
root.style.paddingRight = chrome.RootPaddingRight;
|
||||
root.style.paddingBottom = chrome.RootPaddingBottom;
|
||||
if (chrome.RootBackgroundColor.a > 0f)
|
||||
root.style.backgroundColor = chrome.RootBackgroundColor;
|
||||
root.pickingMode = chrome.RootPickingMode;
|
||||
}
|
||||
|
||||
private static VisualElement CreateShell(AetheriaEveUnitySurfaceChrome chrome)
|
||||
{
|
||||
var shell = new VisualElement();
|
||||
shell.style.flexDirection = chrome.FlexDirection;
|
||||
shell.style.flexGrow = chrome.FlexGrow;
|
||||
if (chrome.Width > 0f)
|
||||
shell.style.width = chrome.Width;
|
||||
shell.style.minWidth = chrome.MinWidth;
|
||||
shell.style.maxWidth = chrome.MaxWidth;
|
||||
if (chrome.MaxHeight > 0f)
|
||||
shell.style.maxHeight = chrome.MaxHeight;
|
||||
shell.style.backgroundColor = chrome.BackgroundColor;
|
||||
shell.style.borderTopLeftRadius = chrome.BorderRadius;
|
||||
shell.style.borderTopRightRadius = chrome.BorderRadius;
|
||||
shell.style.borderBottomLeftRadius = chrome.BorderRadius;
|
||||
shell.style.borderBottomRightRadius = chrome.BorderRadius;
|
||||
shell.style.paddingLeft = chrome.PaddingLeft;
|
||||
shell.style.paddingRight = chrome.PaddingRight;
|
||||
shell.style.paddingTop = chrome.PaddingTop;
|
||||
shell.style.paddingBottom = chrome.PaddingBottom;
|
||||
shell.style.borderLeftWidth = chrome.BorderWidth;
|
||||
shell.style.borderRightWidth = chrome.BorderWidth;
|
||||
shell.style.borderTopWidth = chrome.BorderWidth;
|
||||
shell.style.borderBottomWidth = chrome.BorderWidth;
|
||||
shell.style.borderLeftColor = chrome.BorderColor;
|
||||
shell.style.borderRightColor = chrome.BorderColor;
|
||||
shell.style.borderTopColor = chrome.BorderColor;
|
||||
shell.style.borderBottomColor = chrome.BorderColor;
|
||||
shell.pickingMode = chrome.ShellPickingMode;
|
||||
return shell;
|
||||
}
|
||||
|
||||
private static void ApplyTheme(VisualElement root, EveSurfaceDocument surface)
|
||||
{
|
||||
if (root == null || surface?.Surface?.Styles == null)
|
||||
return;
|
||||
|
||||
var tokens = surface.Surface.Styles
|
||||
.Where(token => !string.IsNullOrWhiteSpace(token?.Name))
|
||||
.GroupBy(token => token.Name, StringComparer.Ordinal)
|
||||
.ToDictionary(group => group.Key, group => group.Last().Value ?? "", StringComparer.Ordinal);
|
||||
var bodyFont = ResolveFont(tokens, "body");
|
||||
var titleFont = ResolveFont(tokens, "title") ?? bodyFont;
|
||||
|
||||
if (bodyFont != null)
|
||||
{
|
||||
root.Query<TextElement>().ForEach(element =>
|
||||
element.style.unityFontDefinition = new StyleFontDefinition(bodyFont));
|
||||
}
|
||||
|
||||
if (titleFont != null)
|
||||
{
|
||||
root.Query<TextElement>(className: "eve-kind-text-title").ForEach(element =>
|
||||
element.style.unityFontDefinition = new StyleFontDefinition(titleFont));
|
||||
}
|
||||
}
|
||||
|
||||
private static TextCoreFontAsset ResolveFont(IReadOnlyDictionary<string, string> tokens, string role)
|
||||
{
|
||||
if (tokens == null || string.IsNullOrWhiteSpace(role))
|
||||
return null;
|
||||
|
||||
if (!tokens.TryGetValue($"font.{role}.family", out var family) || string.IsNullOrWhiteSpace(family))
|
||||
return null;
|
||||
|
||||
tokens.TryGetValue($"font.{role}.style", out var style);
|
||||
style = string.IsNullOrWhiteSpace(style) ? "Regular" : style;
|
||||
var key = $"{family}\n{style}";
|
||||
if (FontCache.TryGetValue(key, out var cached))
|
||||
return cached;
|
||||
|
||||
var font = TryCreateFontAsset(family, style)
|
||||
?? TryCreateFontAsset(family, "Regular")
|
||||
?? TryCreateFontAsset($"{family} {style}", "Regular");
|
||||
if (font == null)
|
||||
{
|
||||
Debug.LogWarning($"Aetheria Eve UI could not resolve runtime font '{family}' style '{style}'.");
|
||||
return null;
|
||||
}
|
||||
|
||||
FontCache[key] = font;
|
||||
return font;
|
||||
}
|
||||
|
||||
private static TextCoreFontAsset TryCreateFontAsset(string family, string style)
|
||||
{
|
||||
try
|
||||
{
|
||||
return TextCoreFontAsset.CreateFontAsset(
|
||||
family,
|
||||
string.IsNullOrWhiteSpace(style) ? "Regular" : style,
|
||||
90,
|
||||
9,
|
||||
GlyphRenderMode.SDFAA);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ContainsStateRefs(EveSurfaceDocument surface)
|
||||
{
|
||||
return surface?.Surface?.Root != null && ContainsStateRefs(surface.Surface.Root);
|
||||
}
|
||||
|
||||
private static bool ContainsStateRefs(EveSurfaceComponent component)
|
||||
{
|
||||
if (component.Props != null &&
|
||||
component.Props.Any(prop =>
|
||||
!string.IsNullOrWhiteSpace(prop.Value) &&
|
||||
(string.Equals(prop.Key, AetheriaRuntimeSurfaceStateRefs.Source, StringComparison.Ordinal) ||
|
||||
prop.Key.EndsWith("Ref", StringComparison.Ordinal))))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var child in component.Children)
|
||||
{
|
||||
if (ContainsStateRefs(child))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static CultMeshStateRefResolver CreateDefaultStateRefResolver()
|
||||
{
|
||||
return AetheriaEveRuntimeUnityHooks.TryCreateDefaultStateRefResolver();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class AetheriaEveRuntimeUnityHooks
|
||||
{
|
||||
public static Func<string, AetheriaRuntimeStateBootReport> ResolveStateBoot { get; set; }
|
||||
|
||||
public static Func<AetheriaRuntimeStateBootReport, string, AetheriaClientState> RuntimeState { get; set; }
|
||||
|
||||
public static Func<AetheriaRuntimeStateBootReport, string, AetheriaControl> Control { get; set; }
|
||||
|
||||
public static Func<AetheriaRuntimeStateBootReport, string, AetheriaUi> Ui { get; set; }
|
||||
|
||||
public static Func<AetheriaRuntimeStateBootReport, string, CultMeshStateRefResolver> StateRefResolver { get; set; }
|
||||
|
||||
public static AetheriaRuntimeStateBootReport RequireStateBoot(string stateFilePathOverride)
|
||||
{
|
||||
if (ResolveStateBoot == null)
|
||||
throw new InvalidOperationException("Aetheria Eve runtime hooks are not installed.");
|
||||
|
||||
return ResolveStateBoot(stateFilePathOverride ?? "");
|
||||
}
|
||||
|
||||
public static AetheriaClientState RequireRuntimeState(
|
||||
AetheriaRuntimeStateBootReport stateBoot,
|
||||
string runtimeId)
|
||||
{
|
||||
if (RuntimeState == null)
|
||||
throw new InvalidOperationException("Aetheria Eve runtime state hook is not installed.");
|
||||
|
||||
return RuntimeState(stateBoot, runtimeId ?? "");
|
||||
}
|
||||
|
||||
public static AetheriaControl RequireControl(
|
||||
AetheriaRuntimeStateBootReport stateBoot,
|
||||
string runtimeId)
|
||||
{
|
||||
if (Control == null)
|
||||
throw new InvalidOperationException("Aetheria Eve runtime control hook is not installed.");
|
||||
|
||||
return Control(stateBoot, runtimeId ?? "");
|
||||
}
|
||||
|
||||
public static AetheriaUi RequireUi(
|
||||
AetheriaRuntimeStateBootReport stateBoot,
|
||||
string runtimeId)
|
||||
{
|
||||
if (Ui == null)
|
||||
throw new InvalidOperationException("Aetheria Eve runtime UI hook is not installed.");
|
||||
|
||||
return Ui(stateBoot, runtimeId ?? "");
|
||||
}
|
||||
|
||||
public static CultMeshStateRefResolver TryCreateStateRefResolver(
|
||||
AetheriaRuntimeStateBootReport stateBoot,
|
||||
string runtimeId)
|
||||
{
|
||||
return StateRefResolver?.Invoke(stateBoot, runtimeId ?? "");
|
||||
}
|
||||
|
||||
public static CultMeshStateRefResolver TryCreateDefaultStateRefResolver()
|
||||
{
|
||||
if (ResolveStateBoot == null || StateRefResolver == null)
|
||||
return null;
|
||||
|
||||
var stateBoot = ResolveStateBoot("");
|
||||
if (!stateBoot.SupportsLocalStateFileRead || !stateBoot.StateFileExists)
|
||||
return null;
|
||||
|
||||
return StateRefResolver(stateBoot, "unity-eve-surface-host");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: eb0db70846a048e7b15232a70d9f5f8e
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"name": "GameCult.Aetheria.EveRuntime",
|
||||
"rootNamespace": "GameCult.Aetheria.EveRuntime",
|
||||
"references": [
|
||||
"GameCult.Aetheria.State.Unity",
|
||||
"GameCult.Eve.Surface",
|
||||
"GameCult.Eve.UnityScene",
|
||||
"GameCult.Eve.UnityUIToolkit"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 71b47dbc13067094fb870e349a6cbd14
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"name": "org.gamecult.aetheria.eve-runtime",
|
||||
"displayName": "Aetheria Eve Runtime",
|
||||
"version": "0.1.0",
|
||||
"unity": "2021.3",
|
||||
"description": "Aetheria provider bridge from typed CultCache Eve surfaces to generic Eve Unity lowerers.",
|
||||
"dependencies": {
|
||||
"org.gamecult.aetheria.state": "0.1.0",
|
||||
"org.gamecult.eve.unity-scene": "0.1.0",
|
||||
"org.gamecult.eve.unity-uitoolkit": "0.1.0"
|
||||
},
|
||||
"keywords": [
|
||||
"Aetheria",
|
||||
"Eve",
|
||||
"CultCache",
|
||||
"UI Toolkit"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"category": "Scripting"
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bb8d7a317e284914da0dfaccc300c17c
|
||||
PackageManifestImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bd2e1c0bcf53796468d8246c0a4bdf72
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,229 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using GameCult.Eve.Surface;
|
||||
using GameCult.Mesh;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public sealed class AetheriaClient : IDisposable
|
||||
{
|
||||
private readonly AetheriaRuntimeVerseClient _verse;
|
||||
private readonly string _clientId;
|
||||
private readonly string _sessionId;
|
||||
private readonly AetheriaRuntimeDaemonOperationsClient _operations;
|
||||
private readonly AetheriaControl _control;
|
||||
private readonly AetheriaUi _ui;
|
||||
private readonly AetheriaClientState _state;
|
||||
private CultMeshReactiveDocument<AetheriaRuntimeDaemonFrameDocument>? _daemonFrame;
|
||||
private bool _disposed;
|
||||
|
||||
private AetheriaClient(AetheriaRuntimeVerseClient verse, string clientId, string sessionId)
|
||||
{
|
||||
_verse = verse ?? throw new ArgumentNullException(nameof(verse));
|
||||
_clientId = string.IsNullOrWhiteSpace(clientId) ? AetheriaRuntimeVerseClient.DefaultRuntimeId : clientId;
|
||||
_sessionId = string.IsNullOrWhiteSpace(sessionId) ? "local" : sessionId;
|
||||
_operations = new AetheriaRuntimeDaemonOperationsClient(SendOperation);
|
||||
_control = new AetheriaControl(_operations);
|
||||
_ui = new AetheriaUi(this);
|
||||
_state = _verse.Aetheria();
|
||||
}
|
||||
|
||||
public string StatePath => _verse.StatePath;
|
||||
public string RuntimeId => _verse.RuntimeId;
|
||||
public AetheriaControl Control => _control;
|
||||
public AetheriaUi Ui => _ui;
|
||||
public AetheriaClientState State => _state;
|
||||
|
||||
internal AetheriaRuntimeDaemonOperationsClient Operations => _operations;
|
||||
|
||||
public static async Task<AetheriaClient> OpenAsync(
|
||||
string statePath,
|
||||
string runtimeId = AetheriaRuntimeVerseClient.DefaultRuntimeId,
|
||||
string sessionId = "local",
|
||||
bool startServer = false,
|
||||
bool pullOnOpen = true)
|
||||
{
|
||||
var verse = await AetheriaRuntimeVerseClient
|
||||
.OpenAsync(statePath, runtimeId, startServer, pullOnOpen)
|
||||
.ConfigureAwait(false);
|
||||
return new AetheriaClient(verse, runtimeId, sessionId);
|
||||
}
|
||||
|
||||
public static async Task<AetheriaClient> OpenRemoteAsync(
|
||||
string replicaStatePath,
|
||||
string endpoint,
|
||||
string runtimeId = AetheriaRuntimeVerseClient.DefaultRuntimeId,
|
||||
string sessionId = "remote",
|
||||
bool synchronizeOnOpen = true)
|
||||
{
|
||||
var verse = await AetheriaRuntimeVerseClient
|
||||
.OpenRemoteAsync(
|
||||
replicaStatePath,
|
||||
endpoint,
|
||||
runtimeId,
|
||||
pullOnOpen: false,
|
||||
synchronizeOnOpen: synchronizeOnOpen)
|
||||
.ConfigureAwait(false);
|
||||
return new AetheriaClient(verse, runtimeId, sessionId);
|
||||
}
|
||||
|
||||
public Task<int> RefreshRemoteAsync()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return _verse.RefreshRemoteAsync();
|
||||
}
|
||||
|
||||
public IReadOnlyList<AetheriaRuntimeCommittedCommandFactDocument> CommittedCommandFacts()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return _verse.CommittedCommandFacts();
|
||||
}
|
||||
|
||||
public static Task<AetheriaClient> OpenLocalAsync(
|
||||
DirectoryInfo gameDataDirectory,
|
||||
string runtimeId,
|
||||
string sessionId = "local",
|
||||
bool pullOnOpen = true)
|
||||
{
|
||||
if (gameDataDirectory == null) throw new ArgumentNullException(nameof(gameDataDirectory));
|
||||
|
||||
var stateBoot = AetheriaRuntimeStateBoot.Inspect(gameDataDirectory);
|
||||
if (!stateBoot.SupportsLocalStateFileRead || !stateBoot.StateFileExists)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Aetheria client requires a readable managed Verse target: {stateBoot.FailureMessage}");
|
||||
}
|
||||
|
||||
return OpenAsync(
|
||||
stateBoot.StateFilePath,
|
||||
runtimeId,
|
||||
sessionId,
|
||||
startServer: false,
|
||||
pullOnOpen: pullOnOpen);
|
||||
}
|
||||
|
||||
internal Task<AetheriaRuntimeEveCommandEnvelope> SubmitInputSettingsCommandAsync(
|
||||
AetheriaRuntimeEveCommandKind command,
|
||||
AetheriaRuntimeInputSettingsCommandBody body,
|
||||
string? clientId = null,
|
||||
bool flush = true)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return _verse.SubmitInputSettingsCommandAsync(
|
||||
command,
|
||||
body,
|
||||
string.IsNullOrWhiteSpace(clientId) ? _clientId : clientId!,
|
||||
flush);
|
||||
}
|
||||
|
||||
internal Task<AetheriaRuntimeEveCommandEnvelope> SubmitLoadoutTemplateCommandAsync(
|
||||
AetheriaRuntimeLoadoutTemplateCommit loadoutTemplate,
|
||||
string? clientId = null,
|
||||
bool flush = true)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return _verse.SubmitLoadoutTemplateCommandAsync(
|
||||
loadoutTemplate,
|
||||
string.IsNullOrWhiteSpace(clientId) ? _clientId : clientId!,
|
||||
flush);
|
||||
}
|
||||
|
||||
internal Task<AetheriaRuntimeEveCommandEnvelope> SubmitKnownSurfaceCommandAsync(
|
||||
EveSurfaceCommandRequest request,
|
||||
string? clientId = null,
|
||||
bool flush = true)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return _verse.SubmitKnownSurfaceCommandAsync(
|
||||
request,
|
||||
string.IsNullOrWhiteSpace(clientId) ? _clientId : clientId!,
|
||||
flush);
|
||||
}
|
||||
|
||||
public AetheriaRuntimeDaemonCommandEnvelope SetMoveVector(
|
||||
double directionX,
|
||||
double directionY,
|
||||
double scalarValue = 1.0)
|
||||
{
|
||||
return Control.SetMoveVector(directionX, directionY, scalarValue);
|
||||
}
|
||||
|
||||
public AetheriaRuntimeDaemonCommandEnvelope SetTarget(string targetEntityKey)
|
||||
{
|
||||
return Control.SetTarget(targetEntityKey);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope SubmitDaemonCommandDocument(
|
||||
AetheriaRuntimeDaemonCommandDocument command)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return _verse
|
||||
.SubmitDaemonCommandAsync(command)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeEveCommandEnvelope SubmitEveCommandDocument(
|
||||
AetheriaRuntimeEveCommandDocument command)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return _verse
|
||||
.SubmitEveCommandAsync(command)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_disposed = true;
|
||||
_daemonFrame?.Dispose();
|
||||
_verse.Dispose();
|
||||
}
|
||||
|
||||
private AetheriaRuntimeDaemonCommandEnvelope SendOperation(
|
||||
Func<AetheriaRuntimeDaemonOperationClient, AetheriaRuntimeDaemonFrameDocument?, AetheriaRuntimeDaemonCommandEnvelope> submit)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (submit == null) throw new ArgumentNullException(nameof(submit));
|
||||
|
||||
var frame = CurrentDaemonFrame();
|
||||
var operationClient = new AetheriaRuntimeDaemonOperationClient(
|
||||
StatePath,
|
||||
_clientId,
|
||||
frame?.SessionId ?? _sessionId,
|
||||
command => _verse
|
||||
.SubmitDaemonCommandAsync(command)
|
||||
.GetAwaiter()
|
||||
.GetResult());
|
||||
|
||||
return submit(operationClient, frame);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonFrameDocument? CurrentDaemonFrame()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
try
|
||||
{
|
||||
_daemonFrame ??= State.DaemonFrame.Reactive();
|
||||
return _daemonFrame.Current;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(AetheriaClient));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3e8912ea164c37d4d95fccb217f44667
|
||||
|
|
@ -1,344 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GameCult.Mesh;
|
||||
using GameCult.Eve.PluginFields;
|
||||
using MessagePack;
|
||||
using EveSurfaceDocument = GameCult.Eve.Surface.EveSurfaceDocument;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public enum AetheriaClientEveSurface
|
||||
{
|
||||
Game,
|
||||
GameTui,
|
||||
Editor,
|
||||
EditorTui
|
||||
}
|
||||
|
||||
public sealed class AetheriaClientState : IDisposable
|
||||
{
|
||||
private readonly Func<AetheriaRuntimeViewportBounds, CultMeshDocumentHandle<AetheriaRuntimeGameViewportDocument>> _mapViewport;
|
||||
private readonly Func<AetheriaRuntimeViewportBounds, CultMeshDocumentHandle<AetheriaRuntimeObjectsViewportDocument>> _objectsViewport;
|
||||
private readonly Func<AetheriaRuntimeViewportBounds, CultMeshDocumentHandle<AetheriaRuntimeGravityViewportDocument>> _gravityViewport;
|
||||
private readonly Func<AetheriaRuntimeViewportBounds, CultMeshDocumentHandle<EveFieldsSplatsDocument>> _renderSplatsViewport;
|
||||
private readonly Func<int, CultMeshDocumentHandle<AetheriaRuntimeZoneDetailsDocument>> _zoneDetails;
|
||||
private readonly Func<int, CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument>> _zoneDetailsSurface;
|
||||
private readonly Func<AetheriaRuntimeInventoryPanelSurfaceRequest, CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument>> _inventoryPanelSurface;
|
||||
private readonly Func<AetheriaRuntimeInventoryDropdownSurfaceRequest, CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument>> _inventoryDropdownSurface;
|
||||
private readonly Func<string, bool, bool, CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument>> _mainMenuSurfaceDocument;
|
||||
private readonly Func<int, CultMeshDocumentHandle<AetheriaRuntimeSelectedObjectDocument>> _selectedObject;
|
||||
private readonly Func<int, CultMeshDocumentHandle<AetheriaRuntimeInventoryDocument>> _inventory;
|
||||
private readonly Func<string, CultMeshDocumentHandle<AetheriaRuntimeStarbridgePlayerSeatDocument>> _starbridgePlayerSeat;
|
||||
private CultMeshReactiveDocument<AetheriaRuntimeDaemonFrameDocument>? _eveStateRefFrame;
|
||||
private CultMeshReactiveDocument<AetheriaRuntimeDaemonHealthDocument>? _eveStateRefHealth;
|
||||
private CultMeshReactiveDocument<AetheriaRuntimeDaemonCommandBoundaryDocument>? _eveStateRefCommandBoundary;
|
||||
private CultMeshReactiveDocument<AetheriaRuntimeCatalogSnapshot>? _eveStateRefCatalog;
|
||||
|
||||
internal AetheriaClientState(
|
||||
CultMeshDocumentHandle<AetheriaRuntimeDaemonProviderAdvertisementDocument> providerAdvertisement,
|
||||
CultMeshDocumentHandle<AetheriaRuntimeDaemonHealthDocument> health,
|
||||
CultMeshDocumentHandle<AetheriaRuntimeDaemonCommandBoundaryDocument> commandBoundary,
|
||||
CultMeshDocumentHandle<AetheriaRuntimeAssetManifestDocument> assetManifest,
|
||||
CultMeshDocumentHandle<AetheriaRuntimeVerseAuthorityPolicyDocument> authorityPolicy,
|
||||
CultMeshDocumentHandle<AetheriaRuntimeDaemonFrameDocument> daemonFrame,
|
||||
CultMeshDocumentHandle<AetheriaRuntimeDaemonSoaViewDocument> daemonSoaView,
|
||||
CultMeshDocumentHandle<EveSurfaceDocument> gameSurface,
|
||||
CultMeshDocumentHandle<EveSurfaceDocument> gameTuiSurface,
|
||||
CultMeshDocumentHandle<EveSurfaceDocument> editorSurface,
|
||||
CultMeshDocumentHandle<EveSurfaceDocument> editorTuiSurface,
|
||||
CultMeshDocumentHandle<AetheriaRuntimeCatalogSnapshot> catalog,
|
||||
CultMeshDocumentHandle<AetheriaRuntimeLoadoutTemplatesDocument> loadoutTemplates,
|
||||
CultMeshDocumentHandle<AetheriaRuntimePlayerSettingsDocument> playerSettings,
|
||||
CultMeshDocumentHandle<AetheriaRuntimeVerseHostSettingsDocument> verseHostSettings,
|
||||
CultMeshDocumentHandle<AetheriaRuntimeCurrentZoneDocument> currentZone,
|
||||
CultMeshDocumentHandle<AetheriaRuntimeCurrentEntityDocument> currentEntity,
|
||||
CultMeshDocumentHandle<AetheriaRuntimeCurrentDockingDocument> currentDocking,
|
||||
CultMeshDocumentHandle<AetheriaRuntimeZoneContactsDocument> zoneContacts,
|
||||
CultMeshDocumentHandle<AetheriaRuntimeStationRefitDocument> stationRefit,
|
||||
CultMeshDocumentHandle<AetheriaRuntimeSectorMapDocument> sectorMap,
|
||||
CultMeshDocumentHandle<AetheriaRuntimeZoneRenderDocument> zoneRender,
|
||||
Func<AetheriaRuntimeViewportBounds, CultMeshDocumentHandle<AetheriaRuntimeGameViewportDocument>> mapViewport,
|
||||
Func<AetheriaRuntimeViewportBounds, CultMeshDocumentHandle<AetheriaRuntimeObjectsViewportDocument>> objectsViewport,
|
||||
Func<AetheriaRuntimeViewportBounds, CultMeshDocumentHandle<AetheriaRuntimeGravityViewportDocument>> gravityViewport,
|
||||
Func<AetheriaRuntimeViewportBounds, CultMeshDocumentHandle<EveFieldsSplatsDocument>> renderSplatsViewport,
|
||||
Func<int, CultMeshDocumentHandle<AetheriaRuntimeZoneDetailsDocument>> zoneDetails,
|
||||
Func<int, CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument>> zoneDetailsSurface,
|
||||
Func<AetheriaRuntimeInventoryPanelSurfaceRequest, CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument>> inventoryPanelSurface,
|
||||
Func<AetheriaRuntimeInventoryDropdownSurfaceRequest, CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument>> inventoryDropdownSurface,
|
||||
Func<string, bool, bool, CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument>> mainMenuSurfaceDocument,
|
||||
Func<int, CultMeshDocumentHandle<AetheriaRuntimeSelectedObjectDocument>> selectedObject,
|
||||
Func<int, CultMeshDocumentHandle<AetheriaRuntimeInventoryDocument>> inventory,
|
||||
CultMeshDocumentHandle<AetheriaRuntimeStarbridgeScenarioDocument> starbridgeScenario,
|
||||
CultMeshDocumentHandle<AetheriaRuntimeStarbridgeSessionDocument> starbridgeSession,
|
||||
CultMeshDocumentHandle<AetheriaRuntimeStarbridgeSessionSummaryDocument> starbridgeSummary,
|
||||
Func<string, CultMeshDocumentHandle<AetheriaRuntimeStarbridgePlayerSeatDocument>> starbridgePlayerSeat)
|
||||
{
|
||||
ProviderAdvertisement = providerAdvertisement ?? throw new ArgumentNullException(nameof(providerAdvertisement));
|
||||
Health = health ?? throw new ArgumentNullException(nameof(health));
|
||||
CommandBoundary = commandBoundary ?? throw new ArgumentNullException(nameof(commandBoundary));
|
||||
AssetManifest = assetManifest ?? throw new ArgumentNullException(nameof(assetManifest));
|
||||
AuthorityPolicy = authorityPolicy ?? throw new ArgumentNullException(nameof(authorityPolicy));
|
||||
DaemonFrame = daemonFrame ?? throw new ArgumentNullException(nameof(daemonFrame));
|
||||
DaemonSoaView = daemonSoaView ?? throw new ArgumentNullException(nameof(daemonSoaView));
|
||||
GameSurface = gameSurface ?? throw new ArgumentNullException(nameof(gameSurface));
|
||||
GameTuiSurface = gameTuiSurface ?? throw new ArgumentNullException(nameof(gameTuiSurface));
|
||||
EditorSurface = editorSurface ?? throw new ArgumentNullException(nameof(editorSurface));
|
||||
EditorTuiSurface = editorTuiSurface ?? throw new ArgumentNullException(nameof(editorTuiSurface));
|
||||
Catalog = catalog ?? throw new ArgumentNullException(nameof(catalog));
|
||||
LoadoutTemplates = loadoutTemplates ?? throw new ArgumentNullException(nameof(loadoutTemplates));
|
||||
PlayerSettings = playerSettings ?? throw new ArgumentNullException(nameof(playerSettings));
|
||||
VerseHostSettings = verseHostSettings ?? throw new ArgumentNullException(nameof(verseHostSettings));
|
||||
CurrentZone = currentZone ?? throw new ArgumentNullException(nameof(currentZone));
|
||||
CurrentEntity = currentEntity ?? throw new ArgumentNullException(nameof(currentEntity));
|
||||
CurrentDocking = currentDocking ?? throw new ArgumentNullException(nameof(currentDocking));
|
||||
ZoneContacts = zoneContacts ?? throw new ArgumentNullException(nameof(zoneContacts));
|
||||
StationRefit = stationRefit ?? throw new ArgumentNullException(nameof(stationRefit));
|
||||
SectorMap = sectorMap ?? throw new ArgumentNullException(nameof(sectorMap));
|
||||
ZoneRender = zoneRender ?? throw new ArgumentNullException(nameof(zoneRender));
|
||||
_mapViewport = mapViewport ?? throw new ArgumentNullException(nameof(mapViewport));
|
||||
_objectsViewport = objectsViewport ?? throw new ArgumentNullException(nameof(objectsViewport));
|
||||
_gravityViewport = gravityViewport ?? throw new ArgumentNullException(nameof(gravityViewport));
|
||||
_renderSplatsViewport = renderSplatsViewport ?? throw new ArgumentNullException(nameof(renderSplatsViewport));
|
||||
_zoneDetails = zoneDetails ?? throw new ArgumentNullException(nameof(zoneDetails));
|
||||
_zoneDetailsSurface = zoneDetailsSurface ?? throw new ArgumentNullException(nameof(zoneDetailsSurface));
|
||||
_inventoryPanelSurface = inventoryPanelSurface ?? throw new ArgumentNullException(nameof(inventoryPanelSurface));
|
||||
_inventoryDropdownSurface = inventoryDropdownSurface ?? throw new ArgumentNullException(nameof(inventoryDropdownSurface));
|
||||
_mainMenuSurfaceDocument = mainMenuSurfaceDocument ?? throw new ArgumentNullException(nameof(mainMenuSurfaceDocument));
|
||||
_selectedObject = selectedObject ?? throw new ArgumentNullException(nameof(selectedObject));
|
||||
_inventory = inventory ?? throw new ArgumentNullException(nameof(inventory));
|
||||
StarbridgeScenario = starbridgeScenario ?? throw new ArgumentNullException(nameof(starbridgeScenario));
|
||||
StarbridgeSession = starbridgeSession ?? throw new ArgumentNullException(nameof(starbridgeSession));
|
||||
StarbridgeSummary = starbridgeSummary ?? throw new ArgumentNullException(nameof(starbridgeSummary));
|
||||
_starbridgePlayerSeat = starbridgePlayerSeat ?? throw new ArgumentNullException(nameof(starbridgePlayerSeat));
|
||||
}
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeDaemonProviderAdvertisementDocument> ProviderAdvertisement { get; }
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeDaemonHealthDocument> Health { get; }
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeDaemonCommandBoundaryDocument> CommandBoundary { get; }
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeAssetManifestDocument> AssetManifest { get; }
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeVerseAuthorityPolicyDocument> AuthorityPolicy { get; }
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeDaemonFrameDocument> DaemonFrame { get; }
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeDaemonSoaViewDocument> DaemonSoaView { get; }
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeCatalogSnapshot> Catalog { get; }
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeLoadoutTemplatesDocument> LoadoutTemplates { get; }
|
||||
|
||||
public CultMeshDocumentHandle<EveSurfaceDocument> GameSurface { get; }
|
||||
|
||||
public CultMeshDocumentHandle<EveSurfaceDocument> GameTuiSurface { get; }
|
||||
|
||||
public CultMeshDocumentHandle<EveSurfaceDocument> EditorSurface { get; }
|
||||
|
||||
public CultMeshDocumentHandle<EveSurfaceDocument> EditorTuiSurface { get; }
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimePlayerSettingsDocument> PlayerSettings { get; }
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeVerseHostSettingsDocument> VerseHostSettings { get; }
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeCurrentZoneDocument> CurrentZone { get; }
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeCurrentEntityDocument> CurrentEntity { get; }
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeCurrentDockingDocument> CurrentDocking { get; }
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeStarbridgeScenarioDocument> StarbridgeScenario { get; }
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeStarbridgeSessionDocument> StarbridgeSession { get; }
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeStarbridgeSessionSummaryDocument> StarbridgeSummary { get; }
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeZoneContactsDocument> ZoneContacts { get; }
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeStationRefitDocument> StationRefit { get; }
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeSectorMapDocument> SectorMap { get; }
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeZoneRenderDocument> ZoneRender { get; }
|
||||
|
||||
public static bool TryResolveEveSurface(
|
||||
string? surfaceId,
|
||||
out AetheriaClientEveSurface surface)
|
||||
{
|
||||
if (string.Equals(surfaceId, AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId, StringComparison.Ordinal))
|
||||
{
|
||||
surface = AetheriaClientEveSurface.Game;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.Equals(surfaceId, AetheriaRuntimeDaemonGameSurfaceBuilder.TuiSurfaceId, StringComparison.Ordinal))
|
||||
{
|
||||
surface = AetheriaClientEveSurface.GameTui;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.Equals(surfaceId, AetheriaRuntimeDaemonEditorSurfaceBuilder.SurfaceId, StringComparison.Ordinal))
|
||||
{
|
||||
surface = AetheriaClientEveSurface.Editor;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.Equals(surfaceId, AetheriaRuntimeDaemonEditorSurfaceBuilder.TuiSurfaceId, StringComparison.Ordinal))
|
||||
{
|
||||
surface = AetheriaClientEveSurface.EditorTui;
|
||||
return true;
|
||||
}
|
||||
|
||||
surface = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public CultMeshDocumentHandle<EveSurfaceDocument>? EveSurfaceDocument(
|
||||
string? surfaceId)
|
||||
{
|
||||
if (!TryResolveEveSurface(surfaceId, out var surface))
|
||||
return null;
|
||||
|
||||
return surface switch
|
||||
{
|
||||
AetheriaClientEveSurface.Game => GameSurface,
|
||||
AetheriaClientEveSurface.GameTui => GameTuiSurface,
|
||||
AetheriaClientEveSurface.Editor => EditorSurface,
|
||||
AetheriaClientEveSurface.EditorTui => EditorTuiSurface,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(surface), surface, null)
|
||||
};
|
||||
}
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeGameViewportDocument> GameViewport(
|
||||
AetheriaRuntimeViewportBounds viewport)
|
||||
{
|
||||
return _mapViewport(viewport ?? new AetheriaRuntimeViewportBounds());
|
||||
}
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeObjectsViewportDocument> ObjectsViewport(
|
||||
AetheriaRuntimeViewportBounds viewport)
|
||||
{
|
||||
return _objectsViewport(viewport ?? new AetheriaRuntimeViewportBounds());
|
||||
}
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeGravityViewportDocument> GravityViewport(
|
||||
AetheriaRuntimeViewportBounds viewport)
|
||||
{
|
||||
return _gravityViewport(viewport ?? new AetheriaRuntimeViewportBounds());
|
||||
}
|
||||
|
||||
public CultMeshDocumentHandle<EveFieldsSplatsDocument> RenderSplatsViewport(
|
||||
AetheriaRuntimeViewportBounds viewport)
|
||||
{
|
||||
return _renderSplatsViewport(viewport ?? new AetheriaRuntimeViewportBounds());
|
||||
}
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeZoneDetailsDocument> ZoneDetails(int zoneIndex)
|
||||
{
|
||||
if (zoneIndex < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(zoneIndex), zoneIndex, "Aetheria zone index must be non-negative.");
|
||||
|
||||
return _zoneDetails(zoneIndex);
|
||||
}
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument> ZoneDetailsSurface(int zoneIndex)
|
||||
{
|
||||
if (zoneIndex < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(zoneIndex), zoneIndex, "Aetheria zone index must be non-negative.");
|
||||
|
||||
return _zoneDetailsSurface(zoneIndex);
|
||||
}
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument> InventoryPanelSurface(
|
||||
AetheriaRuntimeInventoryPanelSurfaceRequest request)
|
||||
{
|
||||
return _inventoryPanelSurface(request ?? new AetheriaRuntimeInventoryPanelSurfaceRequest());
|
||||
}
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument> InventoryDropdownSurface(
|
||||
AetheriaRuntimeInventoryDropdownSurfaceRequest request)
|
||||
{
|
||||
return _inventoryDropdownSurface(request ?? new AetheriaRuntimeInventoryDropdownSurfaceRequest());
|
||||
}
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument> MainMenuSurface(
|
||||
string surfaceId,
|
||||
bool canOpenRuntimeInputScreen,
|
||||
bool inGame)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(surfaceId))
|
||||
throw new ArgumentException("Main menu surface id must be non-empty.", nameof(surfaceId));
|
||||
|
||||
return _mainMenuSurfaceDocument(surfaceId, canOpenRuntimeInputScreen, inGame);
|
||||
}
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeSelectedObjectDocument> SelectedObject(int entityIndex)
|
||||
{
|
||||
if (entityIndex < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(entityIndex), entityIndex, "Aetheria entity index must be non-negative.");
|
||||
|
||||
return _selectedObject(entityIndex);
|
||||
}
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeInventoryDocument> Inventory(int entityIndex)
|
||||
{
|
||||
if (entityIndex < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(entityIndex), entityIndex, "Aetheria entity index must be non-negative.");
|
||||
|
||||
return _inventory(entityIndex);
|
||||
}
|
||||
|
||||
public CultMeshDocumentHandle<AetheriaRuntimeStarbridgePlayerSeatDocument> StarbridgePlayerSeat(string seatId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(seatId))
|
||||
throw new ArgumentException("Seat id must be non-empty.", nameof(seatId));
|
||||
|
||||
return _starbridgePlayerSeat(seatId);
|
||||
}
|
||||
|
||||
public CultMeshStateRefResolver CreateEveSurfaceCultMeshStateRefResolver()
|
||||
{
|
||||
return AetheriaRuntimeStateRefResolver.CreateEveSurfaceCultMeshStateRefResolver(
|
||||
() => ReadOptionalReactive(ref _eveStateRefFrame, DaemonFrame),
|
||||
() => ReadOptionalReactive(ref _eveStateRefHealth, Health),
|
||||
() => ReadOptionalReactive(ref _eveStateRefCommandBoundary, CommandBoundary),
|
||||
() => ReadOptionalReactive(ref _eveStateRefCatalog, Catalog));
|
||||
}
|
||||
|
||||
private static TDocument? ReadOptionalReactive<TDocument>(
|
||||
ref CultMeshReactiveDocument<TDocument>? reactive,
|
||||
CultMeshDocumentHandle<TDocument> handle)
|
||||
where TDocument : class
|
||||
{
|
||||
try
|
||||
{
|
||||
reactive ??= handle.Reactive();
|
||||
return reactive.Current;
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
catch (FormatterNotRegisteredException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_eveStateRefFrame?.Dispose();
|
||||
_eveStateRefHealth?.Dispose();
|
||||
_eveStateRefCommandBoundary?.Dispose();
|
||||
_eveStateRefCatalog?.Dispose();
|
||||
_eveStateRefFrame = null;
|
||||
_eveStateRefHealth = null;
|
||||
_eveStateRefCommandBoundary = null;
|
||||
_eveStateRefCatalog = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 80b87956450e46e4a9b7ed79b9d8aee8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,244 +0,0 @@
|
|||
using System;
|
||||
using GameCult.Eve.Surface;
|
||||
using GameCult.Mesh;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public sealed class AetheriaControl
|
||||
{
|
||||
private readonly AetheriaRuntimeDaemonOperationsClient _operations;
|
||||
|
||||
internal AetheriaControl(AetheriaRuntimeDaemonOperationsClient operations)
|
||||
{
|
||||
_operations = operations ?? throw new ArgumentNullException(nameof(operations));
|
||||
}
|
||||
|
||||
public AetheriaRuntimeDaemonCommandEnvelope SetTarget(string targetEntityKey) =>
|
||||
_operations.SetTarget(targetEntityKey);
|
||||
|
||||
public CultMeshOperationReceipt ClearTarget() =>
|
||||
_operations.ClearTarget();
|
||||
|
||||
public CultMeshOperationReceipt TargetNearest() =>
|
||||
_operations.TargetNearest();
|
||||
|
||||
public CultMeshOperationReceipt TargetNext() =>
|
||||
_operations.TargetNext();
|
||||
|
||||
public CultMeshOperationReceipt TargetPrevious() =>
|
||||
_operations.TargetPrevious();
|
||||
|
||||
public CultMeshOperationReceipt TargetReticle(double directionX, double directionY, double directionZ) =>
|
||||
_operations.TargetReticle(directionX, directionY, directionZ);
|
||||
|
||||
public AetheriaRuntimeDaemonCommandEnvelope SetMoveVector(
|
||||
double directionX,
|
||||
double directionY,
|
||||
double scalarValue = 1.0) =>
|
||||
_operations.SetMoveVector(directionX, directionY, scalarValue);
|
||||
|
||||
public CultMeshOperationReceipt SetLookDirection(double directionX, double directionY, double directionZ) =>
|
||||
_operations.SetLookDirection(directionX, directionY, directionZ);
|
||||
|
||||
public CultMeshOperationReceipt SetTractorPower(double power) =>
|
||||
_operations.SetTractorPower(power);
|
||||
|
||||
public CultMeshOperationReceipt FireWeaponGroup(int weaponGroup) =>
|
||||
_operations.FireWeaponGroup(weaponGroup);
|
||||
|
||||
public CultMeshOperationReceipt SetWeaponGroupActive(int weaponGroup, bool active) =>
|
||||
_operations.SetWeaponGroupActive(weaponGroup, active);
|
||||
|
||||
public CultMeshOperationReceipt SetWeaponGroupMembership(
|
||||
string targetEntityKey,
|
||||
int equipmentIndex,
|
||||
int weaponGroup,
|
||||
bool assigned) =>
|
||||
_operations.SetWeaponGroupMembership(targetEntityKey, equipmentIndex, weaponGroup, assigned);
|
||||
|
||||
public CultMeshOperationReceipt SetBehaviorActive(int equipmentIndex, int behaviorIndex, bool active) =>
|
||||
_operations.SetBehaviorActive(equipmentIndex, behaviorIndex, active);
|
||||
|
||||
public CultMeshOperationReceipt ActivateConsumable(string itemKey) =>
|
||||
_operations.ActivateConsumable(itemKey);
|
||||
|
||||
public CultMeshOperationReceipt SensorPing() =>
|
||||
_operations.SensorPing();
|
||||
|
||||
public CultMeshOperationReceipt SetHeatsinksEnabled(bool enabled) =>
|
||||
_operations.SetHeatsinksEnabled(enabled);
|
||||
|
||||
public CultMeshOperationReceipt SetOverrideShutdown(bool enabled) =>
|
||||
_operations.SetOverrideShutdown(enabled);
|
||||
|
||||
public CultMeshOperationReceipt SetEntityOverrideShutdown(string targetEntityKey, bool enabled) =>
|
||||
_operations.SetEntityOverrideShutdown(targetEntityKey, enabled);
|
||||
|
||||
public CultMeshOperationReceipt SetItemEnabled(int equipmentIndex, bool enabled) =>
|
||||
_operations.SetItemEnabled(equipmentIndex, enabled);
|
||||
|
||||
public CultMeshOperationReceipt ToggleShieldEnabled() =>
|
||||
_operations.ToggleShieldEnabled();
|
||||
|
||||
public CultMeshOperationReceipt SetItemOverrideShutdown(
|
||||
string targetEntityKey,
|
||||
int equipmentIndex,
|
||||
bool enabled) =>
|
||||
_operations.SetItemOverrideShutdown(targetEntityKey, equipmentIndex, enabled);
|
||||
|
||||
public CultMeshOperationReceipt SetThermotoggleTargetTemperature(
|
||||
string targetEntityKey,
|
||||
int equipmentIndex,
|
||||
int behaviorIndex,
|
||||
double targetTemperature) =>
|
||||
_operations.SetThermotoggleTargetTemperature(
|
||||
targetEntityKey,
|
||||
equipmentIndex,
|
||||
behaviorIndex,
|
||||
targetTemperature);
|
||||
|
||||
public CultMeshOperationReceipt SetShutdownPerformance(
|
||||
string targetEntityKey,
|
||||
double shutdownPerformance) =>
|
||||
_operations.SetShutdownPerformance(targetEntityKey, shutdownPerformance);
|
||||
|
||||
public CultMeshOperationReceipt ToggleHullConductivity(
|
||||
string targetEntityKey,
|
||||
int x,
|
||||
int y,
|
||||
int axis) =>
|
||||
_operations.ToggleHullConductivity(targetEntityKey, x, y, axis);
|
||||
|
||||
public CultMeshOperationReceipt SetEntityName(string targetEntityKey, string name) =>
|
||||
_operations.SetEntityName(targetEntityKey, name);
|
||||
|
||||
public CultMeshOperationReceipt Dock(string targetEntityKey) =>
|
||||
_operations.Dock(targetEntityKey);
|
||||
|
||||
public CultMeshOperationReceipt DockNearest() =>
|
||||
_operations.DockNearest();
|
||||
|
||||
public CultMeshOperationReceipt Undock() =>
|
||||
_operations.Undock();
|
||||
|
||||
public CultMeshOperationReceipt Interact() =>
|
||||
_operations.Interact();
|
||||
|
||||
public CultMeshOperationReceipt SetDockedCurrentShip(string targetEntityKey) =>
|
||||
_operations.SetDockedCurrentShip(targetEntityKey);
|
||||
|
||||
public CultMeshOperationReceipt EnterWormhole(
|
||||
int targetZoneIndex,
|
||||
double positionX,
|
||||
double positionY) =>
|
||||
_operations.EnterWormhole(targetZoneIndex, positionX, positionY);
|
||||
|
||||
public CultMeshOperationReceipt TowToStation(string stationEntityKey) =>
|
||||
_operations.TowToStation(stationEntityKey);
|
||||
|
||||
public CultMeshOperationReceipt TransferCargoItem(
|
||||
string originEntityKey,
|
||||
int originCargoIndex,
|
||||
string destinationEntityKey,
|
||||
int destinationCargoIndex,
|
||||
string itemKey,
|
||||
int quantity,
|
||||
int sourceX,
|
||||
int sourceY,
|
||||
int destinationX,
|
||||
int destinationY,
|
||||
bool hasDestinationPosition) =>
|
||||
_operations.TransferCargoItem(
|
||||
originEntityKey,
|
||||
originCargoIndex,
|
||||
destinationEntityKey,
|
||||
destinationCargoIndex,
|
||||
itemKey,
|
||||
quantity,
|
||||
sourceX,
|
||||
sourceY,
|
||||
destinationX,
|
||||
destinationY,
|
||||
hasDestinationPosition);
|
||||
|
||||
public CultMeshOperationReceipt TradePurchase(
|
||||
string itemKey,
|
||||
int quantity,
|
||||
int stationCargoIndex,
|
||||
int targetCargoIndex,
|
||||
int sourceX,
|
||||
int sourceY) =>
|
||||
_operations.TradePurchase(
|
||||
itemKey,
|
||||
quantity,
|
||||
stationCargoIndex,
|
||||
targetCargoIndex,
|
||||
sourceX,
|
||||
sourceY);
|
||||
|
||||
public CultMeshOperationReceipt TradeSale(
|
||||
string itemKey,
|
||||
int quantity,
|
||||
int sourceCargoIndex,
|
||||
int sourceX,
|
||||
int sourceY) =>
|
||||
_operations.TradeSale(itemKey, quantity, sourceCargoIndex, sourceX, sourceY);
|
||||
|
||||
public CultMeshOperationReceipt RestoreLoadout(
|
||||
string dockedEntityKey,
|
||||
string templateName,
|
||||
int price) =>
|
||||
_operations.RestoreLoadout(dockedEntityKey, templateName, price);
|
||||
|
||||
public CultMeshOperationReceipt EquipItem(
|
||||
string sourceKind,
|
||||
string originEntityKey,
|
||||
int originIndex,
|
||||
string destinationEntityKey,
|
||||
string itemKey,
|
||||
int sourceX,
|
||||
int sourceY,
|
||||
int destinationX,
|
||||
int destinationY,
|
||||
bool hasDestinationPosition) =>
|
||||
_operations.EquipItem(
|
||||
sourceKind,
|
||||
originEntityKey,
|
||||
originIndex,
|
||||
destinationEntityKey,
|
||||
itemKey,
|
||||
sourceX,
|
||||
sourceY,
|
||||
destinationX,
|
||||
destinationY,
|
||||
hasDestinationPosition);
|
||||
|
||||
public CultMeshOperationReceipt StoreItem(
|
||||
string originEntityKey,
|
||||
int sourceEquipmentIndex,
|
||||
string destinationEntityKey,
|
||||
int destinationCargoIndex,
|
||||
string itemKey,
|
||||
int destinationX,
|
||||
int destinationY,
|
||||
bool hasDestinationPosition,
|
||||
string sourceKind = AetheriaRuntimeRefitSourceKinds.Equipment) =>
|
||||
_operations.StoreItem(
|
||||
originEntityKey,
|
||||
sourceEquipmentIndex,
|
||||
destinationEntityKey,
|
||||
destinationCargoIndex,
|
||||
itemKey,
|
||||
destinationX,
|
||||
destinationY,
|
||||
hasDestinationPosition,
|
||||
sourceKind);
|
||||
|
||||
public bool TrySubmitSurfaceCommand(
|
||||
EveSurfaceCommandRequest request,
|
||||
out AetheriaRuntimeDaemonCommandEnvelope? envelope) =>
|
||||
_operations.TrySubmitSurfaceCommand(request, out envelope);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7cbef1cb4f644e3aa0ed0475c7446d85
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f9ad0b6b39cf40bf951dd16f17502991
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Aetheria.State.Unity.Smoke")]
|
||||
[assembly: InternalsVisibleTo("Aetheria.State.Smoke")]
|
||||
[assembly: InternalsVisibleTo("Tests")]
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8397d4d816796f24c9faf1c1c2df522a
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GameCult.Caching;
|
||||
using MessagePack;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public static class AetheriaRuntimeAssetKinds
|
||||
{
|
||||
public const string Texture = "texture";
|
||||
public const string Sprite = "sprite";
|
||||
public const string Mesh = "mesh";
|
||||
public const string Material = "material";
|
||||
public const string Shader = "shader";
|
||||
public const string ComputeShader = "compute-shader";
|
||||
public const string Audio = "audio";
|
||||
public const string Prefab = "prefab";
|
||||
public const string VolumeProfile = "unity.volume-profile";
|
||||
public const string Font = "font";
|
||||
}
|
||||
|
||||
public static class AetheriaRuntimeAssetTransports
|
||||
{
|
||||
public const string CultMesh = "cultmesh";
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaRuntimeAssetRef
|
||||
{
|
||||
[Key(0)]
|
||||
public string AssetKey { get; set; } = "";
|
||||
|
||||
[Key(1)]
|
||||
public string Kind { get; set; } = "";
|
||||
|
||||
[Key(2)]
|
||||
public string Uri { get; set; } = "";
|
||||
|
||||
[Key(3)]
|
||||
public string Transport { get; set; } = AetheriaRuntimeAssetTransports.CultMesh;
|
||||
|
||||
[Key(4)]
|
||||
public string ContentHash { get; set; } = "";
|
||||
|
||||
[Key(5)]
|
||||
public string MimeType { get; set; } = "";
|
||||
|
||||
[Key(6)]
|
||||
public IReadOnlyDictionary<string, string> Metadata { get; set; } =
|
||||
new Dictionary<string, string>();
|
||||
|
||||
public static AetheriaRuntimeAssetRef Empty(string kind = "")
|
||||
{
|
||||
return new AetheriaRuntimeAssetRef { Kind = kind ?? "" };
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeAssetRef FromKey(
|
||||
string key,
|
||||
string kind,
|
||||
string? uri = null,
|
||||
string transport = AetheriaRuntimeAssetTransports.CultMesh,
|
||||
string? mimeType = null)
|
||||
{
|
||||
key ??= "";
|
||||
return new AetheriaRuntimeAssetRef
|
||||
{
|
||||
AssetKey = key,
|
||||
Kind = kind ?? "",
|
||||
Uri = string.IsNullOrWhiteSpace(uri) ? key : uri ?? "",
|
||||
Transport = string.IsNullOrWhiteSpace(transport)
|
||||
? AetheriaRuntimeAssetTransports.CultMesh
|
||||
: transport,
|
||||
MimeType = mimeType ?? ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[CultDocument("gamecult.aetheria.asset_manifest", "gamecult.aetheria.asset_manifest.v1")]
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaRuntimeAssetManifestDocument
|
||||
{
|
||||
[Key(0)]
|
||||
public string Schema { get; set; } = AetheriaRuntimeDaemonSchemas.AssetManifest;
|
||||
|
||||
[Key(1)]
|
||||
public string PublishedAtUtc { get; set; } = "";
|
||||
|
||||
[Key(2)]
|
||||
public string RunId { get; set; } = "";
|
||||
|
||||
[Key(3)]
|
||||
public string BaseUri { get; set; } = "cultmesh://aetheria.local/assets";
|
||||
|
||||
[Key(4)]
|
||||
public IReadOnlyList<AetheriaRuntimeAssetManifestEntry> Assets { get; set; } =
|
||||
Array.Empty<AetheriaRuntimeAssetManifestEntry>();
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaRuntimeAssetManifestEntry
|
||||
{
|
||||
[Key(0)]
|
||||
public AetheriaRuntimeAssetRef Ref { get; set; } = new AetheriaRuntimeAssetRef();
|
||||
|
||||
[Key(1)]
|
||||
public long SizeBytes { get; set; } = -1;
|
||||
|
||||
[Key(2)]
|
||||
public int Width { get; set; }
|
||||
|
||||
[Key(3)]
|
||||
public int Height { get; set; }
|
||||
|
||||
[Key(4)]
|
||||
public IReadOnlyList<string> Tags { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9ff95c77b30d4b8c8d2df0f2a4de0ef8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,815 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public static class AetheriaRuntimeAssets
|
||||
{
|
||||
public const string PresentationHullItemTagPrefix = "presentation-hull-item:";
|
||||
public const string UnityShaderBundle = "aetheria-shaders";
|
||||
public const string UnityUiBundle = "aetheria-ui";
|
||||
public const string UnityCoreBundle = "aetheria-core";
|
||||
|
||||
public static string ResolveUnityBundleName(AetheriaRuntimeAssetManifestEntry entry)
|
||||
{
|
||||
if (entry?.Ref == null)
|
||||
throw new ArgumentNullException(nameof(entry));
|
||||
|
||||
var key = entry.Ref.AssetKey ?? "";
|
||||
if (key.StartsWith("shader.", StringComparison.Ordinal) ||
|
||||
key.StartsWith("compute.", StringComparison.Ordinal) ||
|
||||
key == "material.environment.stardust" ||
|
||||
key == "texture.environment.stardust-colors" ||
|
||||
key == "texture.environment.volume-dither" ||
|
||||
key == "profile.environment.flight")
|
||||
return UnityShaderBundle;
|
||||
|
||||
if (key.StartsWith("map.", StringComparison.Ordinal) ||
|
||||
key.StartsWith("material.sector-map.", StringComparison.Ordinal) ||
|
||||
key.StartsWith("inventory.", StringComparison.Ordinal) ||
|
||||
key.StartsWith("profile.thermal.", StringComparison.Ordinal) ||
|
||||
key == "profile.death" ||
|
||||
key.StartsWith("item.", StringComparison.Ordinal) && key.EndsWith(".icon", StringComparison.Ordinal) ||
|
||||
key == "font.ui.primary")
|
||||
return UnityUiBundle;
|
||||
|
||||
if (!string.Equals(entry.Ref.Kind, AetheriaRuntimeAssetKinds.Prefab, StringComparison.Ordinal))
|
||||
return UnityCoreBundle;
|
||||
|
||||
var source = entry.Ref.Metadata.TryGetValue("unityAssetPath", out var unityAssetPath)
|
||||
? unityAssetPath
|
||||
: entry.Ref.Metadata.TryGetValue("resourcesPath", out var resourcesPath)
|
||||
? resourcesPath
|
||||
: key;
|
||||
var stem = string.Concat((source ?? key).Trim().ToLowerInvariant().Select(character =>
|
||||
char.IsLetterOrDigit(character) ? character : '-'));
|
||||
while (stem.Contains("--")) stem = stem.Replace("--", "-");
|
||||
stem = stem.Trim('-');
|
||||
if (stem.StartsWith("assets-", StringComparison.Ordinal)) stem = stem.Substring("assets-".Length);
|
||||
if (stem.StartsWith("resources-", StringComparison.Ordinal)) stem = stem.Substring("resources-".Length);
|
||||
if (stem.EndsWith("-prefab", StringComparison.Ordinal)) stem = stem.Substring(0, stem.Length - "-prefab".Length);
|
||||
const int maximumStemLength = 96;
|
||||
if (stem.Length > maximumStemLength) stem = stem.Substring(stem.Length - maximumStemLength);
|
||||
return $"aetheria-content-{stem}";
|
||||
}
|
||||
|
||||
public static string ResolveEntityPrefabAssetRef(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog = null)
|
||||
{
|
||||
if (entity == null) return "";
|
||||
var hull = catalog?.FindItem(entity.HullItemKey ?? "");
|
||||
if (hull != null && !string.IsNullOrWhiteSpace(hull.HullPrefab))
|
||||
return HullPrefabAssetKey(ResolvePresentationHull(hull, catalog!).ItemKey);
|
||||
var kind = (entity.Kind ?? "").Trim().ToLowerInvariant();
|
||||
if (kind.Contains("station")) return "prefab.entity.station";
|
||||
if (kind.Contains("projectile")) return "prefab.entity.projectile";
|
||||
if (kind.Contains("orbital")) return "prefab.entity.orbital";
|
||||
return string.Equals(entity.FactionKey, "player", StringComparison.OrdinalIgnoreCase)
|
||||
? "prefab.entity.player"
|
||||
: "prefab.entity.ship";
|
||||
}
|
||||
|
||||
public static string HullPrefabAssetKey(string hullItemKey) =>
|
||||
string.IsNullOrWhiteSpace(hullItemKey) ? "" : $"prefab.hull.{hullItemKey.Trim()}";
|
||||
public static AetheriaRuntimeAssetManifestDocument ProjectManifest(
|
||||
AetheriaRuntimeCatalogSnapshot? catalog,
|
||||
string runId = "",
|
||||
string baseUri = "cultmesh://aetheria.local/assets")
|
||||
{
|
||||
var entries = new Dictionary<string, AetheriaRuntimeAssetManifestEntry>(StringComparer.Ordinal);
|
||||
Add(entries, EnvironmentReflectionCubemap());
|
||||
Add(entries, EnvironmentPostProcessProfile());
|
||||
Add(entries, EnvironmentGravityFogShader());
|
||||
Add(entries, EnvironmentDitherTexture());
|
||||
Add(entries, EnvironmentStardustComputeShader());
|
||||
Add(entries, EnvironmentStardustMaterial());
|
||||
Add(entries, EnvironmentStardustColorTexture());
|
||||
foreach (var coreAsset in CorePresentationResources()) Add(entries, coreAsset);
|
||||
Add(entries, UiPrimaryFont());
|
||||
foreach (var mapAsset in SectorMapAssets()) Add(entries, mapAsset);
|
||||
Add(entries, MapIcon("entity.player", "Player", "Sprites/Icons/Stroked/Ship"));
|
||||
Add(entries, MapIcon("entity.ship", "Ship", "Sprites/Icons/Stroked/Ship"));
|
||||
Add(entries, MapIcon("entity.orbital", "Orbital", "Sprites/Icons/Stroked/orbital"));
|
||||
Add(entries, MapIcon("entity.station", "Station", "Sprites/Icons/station1"));
|
||||
Add(entries, MapIcon("entity.projectile", "Projectile", "Sprites/Icons/Lightning Bolt"));
|
||||
Add(entries, MapIcon("body.planet", "Planet", "Sprites/Icons/Stroked/Planet"));
|
||||
Add(entries, MapIcon("body.sun", "Sun", "Sprites/Icons/Stroked/Sun"));
|
||||
Add(entries, MapIcon("body.asteroid", "Asteroid", "Sprites/Icons/Stroked/Planet"));
|
||||
Add(entries, MapPrefab("prefab.entity.player", "Player Ship", "Prefabs/Ships/Djinni", "player"));
|
||||
Add(entries, MapPrefab("prefab.entity.ship", "Ship", "Prefabs/Ships/Djinni", "ship"));
|
||||
Add(entries, MapPrefab("prefab.entity.station", "Station", "Prefabs/Stations/AsteroidOutpost", "station"));
|
||||
Add(entries, MapPrefab("prefab.entity.orbital", "Orbital", "Prefabs/Stations/Zenith", "orbital"));
|
||||
Add(entries, MapPrefab("prefab.effect.shot.bolt", "Bolt", "Prefabs/Lightning", "effect.shot.bolt"));
|
||||
Add(entries, MapPrefab("prefab.effect.impact.shield", "Shield impact", "Prefabs/Shield", "effect.impact.shield"));
|
||||
Add(entries, MapPrefab("prefab.effect.beam.tractor", "Tractor beam", "Prefabs/Tractor Beam", "effect.beam.tractor"));
|
||||
Add(entries, DestructionEffect());
|
||||
Add(entries, MapProjectPrefab("prefab.entity.pickup", "Pickup", "Assets/Prefabs/RPG/Pickups/Tetrahedron.prefab"));
|
||||
Add(entries, MapCelestialPrefab(
|
||||
"prefab.body.planet", "Planet", "Assets/Prefabs/RPG/Planets/Planet.prefab", "Terrain Mesh", "celestial.planet"));
|
||||
Add(entries, MapCelestialPrefab(
|
||||
"prefab.body.gas-giant", "Gas Giant", "Assets/Prefabs/RPG/Planets/Gas Giant.prefab", "Sphere", "celestial.gas-giant"));
|
||||
Add(entries, MapCelestialPrefab(
|
||||
"prefab.body.sun", "Sun", "Assets/Prefabs/RPG/Planets/Sun.prefab", "Sphere", "celestial.sun"));
|
||||
Add(entries, MapCelestialPrefab(
|
||||
"prefab.body.asteroid", "Asteroid",
|
||||
"Assets/Plugins/LowPoly_AsteroidsPack/Prefabs/Asteroid_Huge_v01.prefab", "", "celestial.asteroid"));
|
||||
Add(entries, MinePrefab());
|
||||
foreach (var profile in ThermalPresentationProfiles()) Add(entries, profile);
|
||||
foreach (var inventoryAsset in InventoryUiAssets())
|
||||
Add(entries, inventoryAsset);
|
||||
|
||||
foreach (var item in catalog?.Items ?? Array.Empty<AetheriaRuntimeCatalogItem>())
|
||||
{
|
||||
if (item == null || string.IsNullOrWhiteSpace(item.ItemKey))
|
||||
continue;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(item.HullPrefab))
|
||||
{
|
||||
var presentationHull = ResolvePresentationHull(item, catalog!);
|
||||
if (!string.Equals(presentationHull.ItemKey, item.ItemKey, StringComparison.Ordinal))
|
||||
continue;
|
||||
Add(entries, MapPrefab(
|
||||
HullPrefabAssetKey(item.ItemKey),
|
||||
string.IsNullOrWhiteSpace(item.Name) ? item.ItemKey : item.Name,
|
||||
item.HullPrefab,
|
||||
"entity.hull"));
|
||||
}
|
||||
|
||||
var icon = AssetRefFromCatalogIcon(item.ActionBarIcon, $"item.{item.ItemKey}.icon");
|
||||
if (!string.IsNullOrWhiteSpace(icon.AssetKey))
|
||||
{
|
||||
Add(entries, new AetheriaRuntimeAssetManifestEntry
|
||||
{
|
||||
Ref = icon,
|
||||
Tags = new[] { "item", item.ItemKey, item.Category ?? "" }
|
||||
.Where(tag => !string.IsNullOrWhiteSpace(tag))
|
||||
.ToArray()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new AetheriaRuntimeAssetManifestDocument
|
||||
{
|
||||
PublishedAtUtc = DateTime.UtcNow.ToString("O"),
|
||||
RunId = runId ?? "",
|
||||
BaseUri = string.IsNullOrWhiteSpace(baseUri) ? "cultmesh://aetheria.local/assets" : baseUri,
|
||||
Assets = entries.Values
|
||||
.OrderBy(entry => entry.Ref.AssetKey, StringComparer.Ordinal)
|
||||
.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeCatalogItem ResolvePresentationHull(
|
||||
AetheriaRuntimeCatalogItem hull,
|
||||
AetheriaRuntimeCatalogSnapshot catalog)
|
||||
{
|
||||
var aliases = (hull.Tags ?? Array.Empty<string>())
|
||||
.Where(tag => tag != null && tag.StartsWith(PresentationHullItemTagPrefix, StringComparison.Ordinal))
|
||||
.Select(tag => tag.Substring(PresentationHullItemTagPrefix.Length).Trim())
|
||||
.Where(itemKey => itemKey.Length > 0)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
if (aliases.Length == 0)
|
||||
return hull;
|
||||
if (aliases.Length != 1)
|
||||
throw new InvalidOperationException($"Hull '{hull.ItemKey}' advertises multiple presentation hulls.");
|
||||
|
||||
var presentationHull = catalog.FindItem(aliases[0]);
|
||||
if (presentationHull == null || string.IsNullOrWhiteSpace(presentationHull.HullPrefab))
|
||||
throw new InvalidOperationException(
|
||||
$"Hull '{hull.ItemKey}' presentation hull '{aliases[0]}' is missing or has no provider prefab.");
|
||||
return presentationHull;
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeAssetRef ResolveEntityIcon(AetheriaRuntimeViewportObject? obj)
|
||||
{
|
||||
if (obj?.Controlled == true)
|
||||
return Sprite("map.entity.player", "Sprites/Icons/Stroked/Ship");
|
||||
|
||||
var kind = (obj?.Kind ?? "").Trim().ToLowerInvariant();
|
||||
if (kind.IndexOf("station", StringComparison.Ordinal) >= 0)
|
||||
return Sprite("map.entity.station", "Sprites/Icons/station1");
|
||||
if (kind.IndexOf("orbital", StringComparison.Ordinal) >= 0)
|
||||
return Sprite("map.entity.orbital", "Sprites/Icons/Stroked/orbital");
|
||||
if (kind.IndexOf("projectile", StringComparison.Ordinal) >= 0)
|
||||
return Sprite("map.entity.projectile", "Sprites/Icons/Lightning Bolt");
|
||||
|
||||
return Sprite("map.entity.ship", "Sprites/Icons/Stroked/Ship");
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeAssetRef ResolveBodyIcon(AetheriaRuntimeBodyView? body)
|
||||
{
|
||||
var kind = (body?.Kind ?? "").Trim().ToLowerInvariant();
|
||||
if (kind.IndexOf("sun", StringComparison.Ordinal) >= 0 ||
|
||||
kind.IndexOf("star", StringComparison.Ordinal) >= 0)
|
||||
return Sprite("map.body.sun", "Sprites/Icons/Stroked/Sun");
|
||||
if (kind.IndexOf("asteroid", StringComparison.Ordinal) >= 0)
|
||||
return Sprite("map.body.asteroid", "Sprites/Icons/Stroked/Planet");
|
||||
|
||||
return Sprite("map.body.planet", "Sprites/Icons/Stroked/Planet");
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeAssetRef AssetRefFromCatalogIcon(
|
||||
string? catalogPath,
|
||||
string fallbackAssetKey)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(catalogPath))
|
||||
return AetheriaRuntimeAssetRef.Empty(AetheriaRuntimeAssetKinds.Texture);
|
||||
|
||||
var resourcePath = NormalizeResourcePath(catalogPath ?? "");
|
||||
var assetKey = string.IsNullOrWhiteSpace(fallbackAssetKey)
|
||||
? $"catalog.{resourcePath}".Replace('/', '.').Replace('\\', '.')
|
||||
: fallbackAssetKey;
|
||||
return CultMeshAssetRef(
|
||||
assetKey,
|
||||
AetheriaRuntimeAssetKinds.Texture,
|
||||
resourcePath,
|
||||
"image/*");
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeAssetRef InventoryCellBackgroundAtlas()
|
||||
{
|
||||
return Texture(
|
||||
"inventory.cell.background_atlas",
|
||||
"Sprites/Flat UI/Nodes/Nodes8BG");
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeAssetRef InventoryCellForegroundAtlas()
|
||||
{
|
||||
return Texture(
|
||||
"inventory.cell.foreground_atlas",
|
||||
"Sprites/Flat UI/Nodes/Nodes8");
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeAssetRef InventoryThermalLayerAtlas()
|
||||
{
|
||||
return Texture(
|
||||
"inventory.cell.thermal_layer_atlas",
|
||||
"Sprites/Flat UI/pipes");
|
||||
}
|
||||
|
||||
public static IReadOnlyList<AetheriaRuntimeAssetManifestEntry> InventoryUiAssets()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new AetheriaRuntimeAssetManifestEntry
|
||||
{
|
||||
Ref = InventoryCellBackgroundAtlas(),
|
||||
Tags = new[] { "inventory", "cell", "background", "atlas" }
|
||||
},
|
||||
new AetheriaRuntimeAssetManifestEntry
|
||||
{
|
||||
Ref = InventoryCellForegroundAtlas(),
|
||||
Tags = new[] { "inventory", "cell", "foreground", "atlas" }
|
||||
},
|
||||
new AetheriaRuntimeAssetManifestEntry
|
||||
{
|
||||
Ref = InventoryThermalLayerAtlas(),
|
||||
Tags = new[] { "inventory", "thermal", "layer", "atlas" }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeAssetManifestEntry EnvironmentReflectionCubemap()
|
||||
{
|
||||
return new AetheriaRuntimeAssetManifestEntry
|
||||
{
|
||||
Ref = new AetheriaRuntimeAssetRef
|
||||
{
|
||||
AssetKey = "texture.environment.reflection",
|
||||
Kind = AetheriaRuntimeAssetKinds.Texture,
|
||||
Uri = CultMeshAssetUri("texture.environment.reflection"),
|
||||
MimeType = "image/vnd.radiance",
|
||||
Metadata = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["unityAssetPath"] = "Assets/Textures/studio2.hdr",
|
||||
["presentationRole"] = "environment.reflection"
|
||||
}
|
||||
},
|
||||
Tags = new[] { "presentation", "environment", "reflection", "pre-generated" }
|
||||
};
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeAssetManifestEntry EnvironmentGravityFogShader()
|
||||
{
|
||||
return new AetheriaRuntimeAssetManifestEntry
|
||||
{
|
||||
Ref = new AetheriaRuntimeAssetRef
|
||||
{
|
||||
AssetKey = "shader.environment.gravity-fog",
|
||||
Kind = AetheriaRuntimeAssetKinds.Shader,
|
||||
Uri = CultMeshAssetUri("shader.environment.gravity-fog"),
|
||||
MimeType = "application/vnd.unity.shader",
|
||||
Metadata = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["unityAssetPath"] = "Assets/Shaders/Raymarching/CloudShader.shader",
|
||||
["presentationRole"] = "environment.gravity-fog.volume",
|
||||
["unity.volume.pass.raymarch"] = "0",
|
||||
["unity.volume.pass.composite"] = "2",
|
||||
["unity.volume.quality.bootstrap"] = "ultra",
|
||||
["unity.volume.quality.ultra.keyword"] = "ULTRA_QUALITY",
|
||||
["unity.volume.quality.high.keyword"] = "HIGH_QUALITY",
|
||||
["unity.volume.quality.normal.keyword"] = "MEDIUM_QUALITY",
|
||||
["unity.volume.feature.flow.global.keyword"] = "FLOW_GLOBAL",
|
||||
["unity.volume.feature.flow.value3d.keyword"] = "FLOW_VALUE3D",
|
||||
["unity.volume.feature.noise.slope.keyword"] = "NOISE_SLOPE",
|
||||
["unity.volume.texturePort.surfaceHeight"] = "_NebulaSurfaceHeight",
|
||||
["unity.volume.texturePort.patchHeight"] = "_NebulaPatchHeight",
|
||||
["unity.volume.texturePort.patch"] = "_NebulaPatch",
|
||||
["unity.volume.texturePort.tint"] = "_NebulaTint",
|
||||
["unity.volume.texturePort.dither"] = "_DitheringTex",
|
||||
["unity.volume.texturePort.cloud"] = "_CloudTex",
|
||||
["unity.volume.vectorPort.viewportTransform"] = "_GridTransform",
|
||||
["unity.volume.matrixPort.cameraInverseViewProjection"] = "_CamInvProj",
|
||||
["unity.volume.matrixPort.cameraToWorld"] = "_CamToWorld",
|
||||
["unity.volume.vectorPort.cameraProjectionExtents"] = "_ProjectionExtents",
|
||||
["unity.volume.floatPort.raymarchOffset"] = "_RaymarchOffset",
|
||||
["unity.volume.vectorPort.ditherCoordinates"] = "_DitheringCoords",
|
||||
["unity.volume.floatPort.fillDensity"] = "_NebulaFillDensity",
|
||||
["unity.volume.floatPort.fillDistance"] = "_NebulaFillDistance",
|
||||
["unity.volume.floatPort.fillExponent"] = "_NebulaFillExponent",
|
||||
["unity.volume.floatPort.fillOffset"] = "_NebulaFillOffset",
|
||||
["unity.volume.floatPort.patchDensity"] = "_NebulaPatchDensity",
|
||||
["unity.volume.floatPort.floorOffset"] = "_NebulaFloorOffset",
|
||||
["unity.volume.floatPort.floorBlend"] = "_NebulaFloorBlend",
|
||||
["unity.volume.floatPort.patchBlend"] = "_NebulaPatchBlend",
|
||||
["unity.volume.floatPort.luminance"] = "_NebulaLuminance",
|
||||
["unity.volume.floatPort.extinction"] = "_ExtinctionCoefficient",
|
||||
["unity.volume.floatPort.tintLodExponent"] = "_TintLodExponent",
|
||||
["unity.volume.floatPort.safetyDistance"] = "_SafetyDistance",
|
||||
["unity.volume.floatPort.flowScale"] = "_FlowScale",
|
||||
["unity.volume.floatPort.flowAmplitude"] = "_FlowAmplitude",
|
||||
["unity.volume.floatPort.flowScroll"] = "_FlowScroll",
|
||||
["unity.volume.floatPort.flowPeriod"] = "_FlowPeriod",
|
||||
["unity.volume.floatPort.flowSlopeAmplitude"] = "_FlowSlopeAmplitude",
|
||||
["unity.volume.floatPort.flowSwirlAmplitude"] = "_FlowSwirlAmplitude",
|
||||
["unity.volume.floatPort.noiseScale"] = "_NebulaNoiseScale",
|
||||
["unity.volume.floatPort.noiseAmplitude"] = "_NebulaNoiseAmplitude",
|
||||
["unity.volume.floatPort.noiseExponent"] = "_NebulaNoiseExponent",
|
||||
["unity.volume.floatPort.noiseSpeed"] = "_NebulaNoiseSpeed",
|
||||
["unity.volume.floatPort.noiseSlopeExponent"] = "_NebulaNoiseSlopeExponent",
|
||||
["unity.volume.floatPort.dynamicSkyBoost"] = "_DynamicSkyBoost",
|
||||
["unity.volume.floatPort.dynamicLodHigh"] = "_DynamicLodHigh",
|
||||
["unity.volume.floatPort.dynamicLodLow"] = "_DynamicLodLow",
|
||||
["unity.volume.floatPort.dynamicIntensity"] = "_DynamicIntensity",
|
||||
["unity.volume.floatPort.compositeOpacity"] = "_CompositeOpacity"
|
||||
}
|
||||
},
|
||||
Tags = new[] { "presentation", "environment", "gravity", "fog", "volume" }
|
||||
};
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeAssetManifestEntry EnvironmentPostProcessProfile()
|
||||
{
|
||||
return new AetheriaRuntimeAssetManifestEntry
|
||||
{
|
||||
Ref = new AetheriaRuntimeAssetRef
|
||||
{
|
||||
AssetKey = "profile.environment.flight",
|
||||
Kind = AetheriaRuntimeAssetKinds.VolumeProfile,
|
||||
Uri = CultMeshAssetUri("profile.environment.flight"),
|
||||
MimeType = "application/vnd.unity.volume-profile",
|
||||
Metadata = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["unityAssetPath"] = "Assets/Generated/Eve/Environment/Flight.asset",
|
||||
["presentationRole"] = "environment.post-process.flight",
|
||||
["sourceProfile"] = "Assets/Scenes/ARPG_Profiles/Postprocessing Profile.asset",
|
||||
["profileSemantics"] = "aces;bloom;fixed-dark-scene-exposure;contrast;vignette;grain"
|
||||
}
|
||||
},
|
||||
Tags = new[] { "presentation", "environment", "post-process", "flight" }
|
||||
};
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeAssetManifestEntry EnvironmentStardustComputeShader()
|
||||
{
|
||||
return new AetheriaRuntimeAssetManifestEntry
|
||||
{
|
||||
Ref = new AetheriaRuntimeAssetRef
|
||||
{
|
||||
AssetKey = "compute.environment.stardust",
|
||||
Kind = AetheriaRuntimeAssetKinds.ComputeShader,
|
||||
Uri = CultMeshAssetUri("compute.environment.stardust"),
|
||||
MimeType = "application/vnd.unity.compute-shader",
|
||||
Metadata = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["unityAssetPath"] = "Assets/Shaders/Compute/Stardust/Stardust.compute",
|
||||
["presentationRole"] = "environment.stardust.update",
|
||||
["unity.particles.kernel.update"] = "UpdateParticles",
|
||||
["unity.particles.feature.flow.global.keyword"] = "FLOW_GLOBAL",
|
||||
["unity.particles.feature.flow.value3d.keyword"] = "FLOW_VALUE3D",
|
||||
["unity.particles.feature.noise.slope.keyword"] = "NOISE_SLOPE",
|
||||
["unity.particles.texturePort.surfaceHeight"] = "_NebulaSurfaceHeight",
|
||||
["unity.particles.texturePort.tint"] = "_NebulaTint",
|
||||
["unity.particles.texturePort.hue"] = "HueTexture",
|
||||
["unity.particles.bufferPort.particles"] = "particles",
|
||||
["unity.particles.vectorPort.viewportTransform"] = "_GridTransform",
|
||||
["unity.particles.vectorPort.timeVector"] = "_Time",
|
||||
["unity.particles.floatPort.time"] = "time",
|
||||
["unity.particles.floatPort.period"] = "period",
|
||||
["unity.particles.floatPort.spacing"] = "spacing",
|
||||
["unity.particles.floatPort.ceilingHeight"] = "ceilingHeight",
|
||||
["unity.particles.floatPort.floorHeight"] = "floorHeight",
|
||||
["unity.particles.floatPort.maximumSize"] = "maximumSize",
|
||||
["unity.particles.floatPort.minimumSize"] = "minimumSize",
|
||||
["unity.particles.floatPort.minHeadroom"] = "minHeadroom",
|
||||
["unity.particles.floatPort.maxHeadroom"] = "maxHeadroom",
|
||||
["unity.particles.floatPort.heightExponent"] = "heightExponent",
|
||||
["unity.particles.intPort.span"] = "span",
|
||||
["unity.particles.floatPort.fillDensity"] = "_NebulaFillDensity",
|
||||
["unity.particles.floatPort.fillDistance"] = "_NebulaFillDistance",
|
||||
["unity.particles.floatPort.fillExponent"] = "_NebulaFillExponent",
|
||||
["unity.particles.floatPort.fillOffset"] = "_NebulaFillOffset",
|
||||
["unity.particles.floatPort.floorOffset"] = "_NebulaFloorOffset",
|
||||
["unity.particles.floatPort.floorBlend"] = "_NebulaFloorBlend",
|
||||
["unity.particles.floatPort.luminance"] = "_NebulaLuminance",
|
||||
["unity.particles.floatPort.tintLodExponent"] = "_TintLodExponent",
|
||||
["unity.particles.floatPort.flowScale"] = "_FlowScale",
|
||||
["unity.particles.floatPort.flowAmplitude"] = "_FlowAmplitude",
|
||||
["unity.particles.floatPort.flowScroll"] = "_FlowScroll",
|
||||
["unity.particles.floatPort.flowPeriod"] = "_FlowPeriod",
|
||||
["unity.particles.floatPort.flowSlopeAmplitude"] = "_FlowSlopeAmplitude",
|
||||
["unity.particles.floatPort.flowSwirlAmplitude"] = "_FlowSwirlAmplitude",
|
||||
["unity.particles.floatPort.noiseScale"] = "_NebulaNoiseScale",
|
||||
["unity.particles.floatPort.noiseAmplitude"] = "_NebulaNoiseAmplitude",
|
||||
["unity.particles.floatPort.noiseExponent"] = "_NebulaNoiseExponent",
|
||||
["unity.particles.floatPort.noiseSpeed"] = "_NebulaNoiseSpeed",
|
||||
["unity.particles.floatPort.noiseSlopeExponent"] = "_NebulaNoiseSlopeExponent",
|
||||
["unity.particles.floatPort.dynamicSkyBoost"] = "_DynamicSkyBoost",
|
||||
["unity.particles.floatPort.dynamicLodHigh"] = "_DynamicLodHigh",
|
||||
["unity.particles.floatPort.dynamicLodLow"] = "_DynamicLodLow",
|
||||
["unity.particles.floatPort.dynamicIntensity"] = "_DynamicIntensity"
|
||||
}
|
||||
},
|
||||
Tags = new[] { "presentation", "environment", "stardust", "particles", "compute" }
|
||||
};
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeAssetManifestEntry EnvironmentStardustMaterial()
|
||||
{
|
||||
return new AetheriaRuntimeAssetManifestEntry
|
||||
{
|
||||
Ref = new AetheriaRuntimeAssetRef
|
||||
{
|
||||
AssetKey = "material.environment.stardust",
|
||||
Kind = AetheriaRuntimeAssetKinds.Material,
|
||||
Uri = CultMeshAssetUri("material.environment.stardust"),
|
||||
MimeType = "application/vnd.unity.material",
|
||||
Metadata = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["unityAssetPath"] = "Assets/Shaders/Compute/Stardust/Stardust.mat",
|
||||
["presentationRole"] = "environment.stardust.render",
|
||||
["unity.particles.pass.render"] = "0",
|
||||
["unity.particles.bufferPort.particles"] = "particles",
|
||||
["unity.particles.bufferPort.quadPoints"] = "quadPoints",
|
||||
["unity.particles.texturePort.dither"] = "_DitheringTex",
|
||||
["unity.particles.vectorPort.ditherCoordinates"] = "_DitheringCoords",
|
||||
["unity.particles.intPort.frameIndex"] = "_FrameNumber"
|
||||
}
|
||||
},
|
||||
Tags = new[] { "presentation", "environment", "stardust", "particles", "material" }
|
||||
};
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeAssetManifestEntry EnvironmentStardustColorTexture()
|
||||
{
|
||||
return new AetheriaRuntimeAssetManifestEntry
|
||||
{
|
||||
Ref = new AetheriaRuntimeAssetRef
|
||||
{
|
||||
AssetKey = "texture.environment.stardust-colors",
|
||||
Kind = AetheriaRuntimeAssetKinds.Texture,
|
||||
Uri = CultMeshAssetUri("texture.environment.stardust-colors"),
|
||||
MimeType = "image/png",
|
||||
Metadata = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["unityAssetPath"] = "Assets/Resources/Gradients/blackbody.png",
|
||||
["presentationRole"] = "environment.stardust.colors"
|
||||
}
|
||||
},
|
||||
Tags = new[] { "presentation", "environment", "stardust", "pre-generated" }
|
||||
};
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeAssetManifestEntry EnvironmentDitherTexture()
|
||||
{
|
||||
return new AetheriaRuntimeAssetManifestEntry
|
||||
{
|
||||
Ref = new AetheriaRuntimeAssetRef
|
||||
{
|
||||
AssetKey = "texture.environment.volume-dither",
|
||||
Kind = AetheriaRuntimeAssetKinds.Texture,
|
||||
Uri = CultMeshAssetUri("texture.environment.volume-dither"),
|
||||
MimeType = "image/png",
|
||||
Metadata = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["unityAssetPath"] = "Assets/Resources/LDR_LLL1_0.png",
|
||||
["presentationRole"] = "environment.volume.dither"
|
||||
}
|
||||
},
|
||||
Tags = new[] { "presentation", "environment", "volume", "pre-generated" }
|
||||
};
|
||||
}
|
||||
|
||||
public static IReadOnlyList<AetheriaRuntimeAssetManifestEntry> CorePresentationResources()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
MapPresentationAsset("texture.core.sun.surface-flow", AetheriaRuntimeAssetKinds.Texture,
|
||||
"Sun surface flow", "Assets/Resources/Sun Flow/sunsurface1offset.png", "core.sun.surface-flow"),
|
||||
MapPresentationAsset("texture.core.sun.albedo", AetheriaRuntimeAssetKinds.Texture,
|
||||
"Sun albedo", "Assets/Resources/Sun Albedo/sunsurface2map.png", "core.sun.albedo"),
|
||||
MapPresentationAsset("texture.core.fx.explosion-emission", AetheriaRuntimeAssetKinds.Texture,
|
||||
"Explosion emission", "Assets/Prefabs/Fire & Explosion Effects/Textures/ExplosionEmission.tif", "core.fx.explosion-emission"),
|
||||
MapPresentationAsset("texture.core.fx.smoke-puff", AetheriaRuntimeAssetKinds.Texture,
|
||||
"Smoke puff", "Assets/Prefabs/Fire & Explosion Effects/Textures/SmokePuff01.tif", "core.fx.smoke-puff"),
|
||||
MapPresentationAsset("texture.core.fx.explosion", AetheriaRuntimeAssetKinds.Texture,
|
||||
"Explosion", "Assets/Prefabs/Fire & Explosion Effects/Textures/Explosion.tif", "core.fx.explosion"),
|
||||
MapPresentationAsset("texture.core.fx.shockwave", AetheriaRuntimeAssetKinds.Texture,
|
||||
"Shockwave", "Assets/Prefabs/Fire & Explosion Effects/Textures/shockwave.tif", "core.fx.shockwave"),
|
||||
MapPresentationAsset("texture.core.fx.sphere-normal", AetheriaRuntimeAssetKinds.Texture,
|
||||
"Sphere normal", "Assets/Prefabs/Fire & Explosion Effects/Textures/SphereNormal.tif", "core.fx.sphere-normal"),
|
||||
MapPresentationAsset("texture.core.fx.debris", AetheriaRuntimeAssetKinds.Texture,
|
||||
"Debris", "Assets/Prefabs/Fire & Explosion Effects/Textures/Debris.png", "core.fx.debris")
|
||||
};
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeAssetManifestEntry UiPrimaryFont()
|
||||
{
|
||||
return MapPresentationAsset(
|
||||
"font.ui.primary",
|
||||
AetheriaRuntimeAssetKinds.Font,
|
||||
"Primary UI font",
|
||||
"Assets/Fonts/Ubuntu/Ubuntu-L Small SDF.asset",
|
||||
"ui.font.primary");
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeAssetManifestEntry MapIcon(
|
||||
string key,
|
||||
string label,
|
||||
string resourcePath)
|
||||
{
|
||||
return new AetheriaRuntimeAssetManifestEntry
|
||||
{
|
||||
Ref = Sprite($"map.{key}", resourcePath),
|
||||
Tags = new[] { "map", "icon", label }
|
||||
};
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeAssetManifestEntry MapPrefab(
|
||||
string key,
|
||||
string label,
|
||||
string resourcePath,
|
||||
string presentationRole)
|
||||
{
|
||||
var asset = Prefab(key, resourcePath);
|
||||
asset.Metadata = asset.Metadata
|
||||
.Concat(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("presentationRole", presentationRole ?? ""),
|
||||
new KeyValuePair<string, string>("bundleAssetPath", PresentationPrefabPath(key))
|
||||
})
|
||||
.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
|
||||
return new AetheriaRuntimeAssetManifestEntry
|
||||
{
|
||||
Ref = asset,
|
||||
Tags = new[] { "world", "prefab", label }
|
||||
};
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeAssetManifestEntry MapProjectPrefab(
|
||||
string key,
|
||||
string label,
|
||||
string unityAssetPath)
|
||||
{
|
||||
return new AetheriaRuntimeAssetManifestEntry
|
||||
{
|
||||
Ref = new AetheriaRuntimeAssetRef
|
||||
{
|
||||
AssetKey = key,
|
||||
Kind = AetheriaRuntimeAssetKinds.Prefab,
|
||||
Uri = CultMeshAssetUri(key),
|
||||
MimeType = "application/vnd.unity.prefab",
|
||||
Metadata = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["unityAssetPath"] = (unityAssetPath ?? "").Replace('\\', '/'),
|
||||
["bundleAssetPath"] = PresentationPrefabPath(key)
|
||||
}
|
||||
},
|
||||
Tags = new[] { "world", "prefab", label }
|
||||
};
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeAssetManifestEntry MapCelestialPrefab(
|
||||
string key,
|
||||
string label,
|
||||
string unityAssetPath,
|
||||
string presentationVisualPath,
|
||||
string presentationRole)
|
||||
{
|
||||
var entry = MapProjectPrefab(key, label, unityAssetPath);
|
||||
var metadata = entry.Ref.Metadata.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
|
||||
metadata["presentationRole"] = presentationRole ?? "";
|
||||
if (!string.IsNullOrWhiteSpace(presentationVisualPath))
|
||||
metadata["presentationVisualPath"] = presentationVisualPath;
|
||||
entry.Ref.Metadata = metadata;
|
||||
entry.Tags = new[] { "world", "prefab", "celestial", label };
|
||||
return entry;
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeAssetManifestEntry MapProjectAsset(
|
||||
string key, string kind, string label, string unityAssetPath, string presentationRole)
|
||||
{
|
||||
return new AetheriaRuntimeAssetManifestEntry
|
||||
{
|
||||
Ref = new AetheriaRuntimeAssetRef
|
||||
{
|
||||
AssetKey = key, Kind = kind ?? "", Uri = CultMeshAssetUri(key),
|
||||
MimeType = "application/vnd.unity.asset",
|
||||
Metadata = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["unityAssetPath"] = (unityAssetPath ?? "").Replace('\\', '/'),
|
||||
["presentationRole"] = presentationRole ?? ""
|
||||
}
|
||||
},
|
||||
Tags = new[] { "presentation", "thermal", label ?? "" }
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AetheriaRuntimeAssetManifestEntry> SectorMapAssets() =>
|
||||
new[]
|
||||
{
|
||||
MapPresentationAsset(
|
||||
"material.sector-map.background",
|
||||
AetheriaRuntimeAssetKinds.Material,
|
||||
"Sector Map Galaxy",
|
||||
"Assets/Materials/UI/Galaxy.mat",
|
||||
"sector-map.background"),
|
||||
MapPresentationAsset(
|
||||
"material.sector-map.influence",
|
||||
AetheriaRuntimeAssetKinds.Material,
|
||||
"Sector Map Influence",
|
||||
"Assets/Materials/UI/Influence Map.mat",
|
||||
"sector-map.influence"),
|
||||
MapIcon("sector-map.icon.start", "Start", "Sprites/Icons/enter"),
|
||||
MapIcon("sector-map.icon.terminus", "Terminus", "Sprites/Icons/exit"),
|
||||
MapIcon("sector-map.icon.home", "Home Zone", "Sprites/Icons/Home"),
|
||||
MapIcon("sector-map.icon.executive", "Executive", "Sprites/Icons/boss")
|
||||
};
|
||||
|
||||
private static AetheriaRuntimeAssetManifestEntry MapPresentationAsset(
|
||||
string key,
|
||||
string kind,
|
||||
string label,
|
||||
string unityAssetPath,
|
||||
string presentationRole)
|
||||
{
|
||||
var entry = MapProjectAsset(key, kind, label, unityAssetPath, presentationRole);
|
||||
entry.Tags = new[] { "presentation", "sector-map", label ?? "" };
|
||||
return entry;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AetheriaRuntimeAssetManifestEntry> ThermalPresentationProfiles() =>
|
||||
new[]
|
||||
{
|
||||
MapProjectAsset("profile.thermal.heatstroke", AetheriaRuntimeAssetKinds.VolumeProfile,
|
||||
"Heatstroke", "Assets/Generated/Eve/Thermal/Heatstroke.asset",
|
||||
"post.thermal.heatstroke"),
|
||||
MapProjectAsset("profile.thermal.severe-heatstroke", AetheriaRuntimeAssetKinds.VolumeProfile,
|
||||
"Severe heatstroke", "Assets/Generated/Eve/Thermal/Severe Heatstroke.asset",
|
||||
"post.thermal.severe-heatstroke"),
|
||||
MapProjectAsset("profile.thermal.hypothermia", AetheriaRuntimeAssetKinds.VolumeProfile,
|
||||
"Hypothermia", "Assets/Generated/Eve/Thermal/Hypothermia.asset",
|
||||
"post.thermal.hypothermia"),
|
||||
MapProjectAsset("profile.thermal.severe-hypothermia", AetheriaRuntimeAssetKinds.VolumeProfile,
|
||||
"Severe hypothermia", "Assets/Generated/Eve/Thermal/Severe Hypothermia.asset",
|
||||
"post.thermal.severe-hypothermia"),
|
||||
MapProjectAsset("profile.death", AetheriaRuntimeAssetKinds.VolumeProfile,
|
||||
"Death", "Assets/Generated/Eve/Thermal/Death.asset", "post.death")
|
||||
};
|
||||
|
||||
private static AetheriaRuntimeAssetManifestEntry MinePrefab()
|
||||
{
|
||||
var entry = MapProjectPrefab(
|
||||
"prefab.entity.mine",
|
||||
"Mine",
|
||||
"Assets/Prefabs/RPG/Effects/Mine.prefab");
|
||||
entry.Ref.Metadata = new Dictionary<string, string>(entry.Ref.Metadata, StringComparer.Ordinal)
|
||||
{
|
||||
["presentationRole"] = "physical-payload.mine",
|
||||
["activePulseSeconds"] = "1",
|
||||
["triggeredPulseSeconds"] = "0.25",
|
||||
["activeEmission"] = "100",
|
||||
["triggeredEmission"] = "1000"
|
||||
};
|
||||
return entry;
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeAssetManifestEntry DestructionEffect()
|
||||
{
|
||||
var entry = MapProjectPrefab(
|
||||
"prefab.effect.entity.destroyed",
|
||||
"Entity destruction",
|
||||
"Assets/Prefabs/Fire & Explosion Effects/Prefabs/BigExplosion.prefab");
|
||||
entry.Ref.Metadata = new Dictionary<string, string>(entry.Ref.Metadata, StringComparer.Ordinal)
|
||||
{
|
||||
["presentationRole"] = "effect.feedback.entity.destroyed"
|
||||
};
|
||||
return entry;
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeAssetRef Sprite(string key, string resourcePath)
|
||||
{
|
||||
return CultMeshAssetRef(
|
||||
key,
|
||||
AetheriaRuntimeAssetKinds.Sprite,
|
||||
resourcePath,
|
||||
"image/*");
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeAssetRef Texture(string key, string resourcePath)
|
||||
{
|
||||
return CultMeshAssetRef(
|
||||
key,
|
||||
AetheriaRuntimeAssetKinds.Texture,
|
||||
resourcePath,
|
||||
"image/*");
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeAssetRef Prefab(string key, string resourcePath)
|
||||
{
|
||||
return CultMeshAssetRef(
|
||||
key,
|
||||
AetheriaRuntimeAssetKinds.Prefab,
|
||||
resourcePath,
|
||||
"application/vnd.unity.prefab");
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeAssetRef CultMeshAssetRef(
|
||||
string key,
|
||||
string kind,
|
||||
string resourcePath,
|
||||
string mimeType)
|
||||
{
|
||||
var normalizedResourcePath = NormalizeResourcePath(resourcePath);
|
||||
return new AetheriaRuntimeAssetRef
|
||||
{
|
||||
AssetKey = key ?? "",
|
||||
Kind = kind ?? "",
|
||||
Uri = CultMeshAssetUri(key),
|
||||
Transport = AetheriaRuntimeAssetTransports.CultMesh,
|
||||
MimeType = mimeType ?? "",
|
||||
Metadata = string.IsNullOrWhiteSpace(normalizedResourcePath)
|
||||
? new Dictionary<string, string>()
|
||||
: new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["resourcesPath"] = normalizedResourcePath
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static string CultMeshAssetUri(string key)
|
||||
{
|
||||
var path = (key ?? "").Trim().Replace('.', '/').Replace('\\', '/').Trim('/');
|
||||
return $"cultmesh://aetheria/assets/{path}";
|
||||
}
|
||||
|
||||
private static string PresentationPrefabPath(string key)
|
||||
{
|
||||
var fileName = string.Concat((key ?? "").Select(character =>
|
||||
char.IsLetterOrDigit(character) || character == '.' || character == '-'
|
||||
? character
|
||||
: '_'));
|
||||
return $"Assets/Generated/Eve/ProviderPrefabs/{fileName}.prefab";
|
||||
}
|
||||
|
||||
private static string NormalizeResourcePath(string path)
|
||||
{
|
||||
path ??= "";
|
||||
const string resourcesPrefix = "Assets/Resources/";
|
||||
if (path.StartsWith(resourcesPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
path = path.Substring(resourcesPrefix.Length);
|
||||
|
||||
var extensionIndex = path.LastIndexOf('.');
|
||||
if (extensionIndex > 0)
|
||||
path = path.Substring(0, extensionIndex);
|
||||
|
||||
return path.Replace('\\', '/').Trim('/');
|
||||
}
|
||||
|
||||
private static void Add(
|
||||
Dictionary<string, AetheriaRuntimeAssetManifestEntry> entries,
|
||||
AetheriaRuntimeAssetManifestEntry entry)
|
||||
{
|
||||
var key = entry.Ref?.AssetKey ?? "";
|
||||
if (key.Length == 0)
|
||||
return;
|
||||
|
||||
entries[key] = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8e38efc9379f4a7ea4b5df5ca079b5c1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public static class AetheriaRuntimeBehaviorKinds
|
||||
{
|
||||
public const string StatModifier = "StatModifier";
|
||||
public const string GuidedWeapon = "GuidedWeapon";
|
||||
public const string Launcher = "Launcher";
|
||||
public const string InstantWeapon = "InstantWeapon";
|
||||
public const string ConstantWeapon = "ConstantWeapon";
|
||||
public const string ChargedWeapon = "ChargedWeapon";
|
||||
public const string AutoWeapon = "AutoWeapon";
|
||||
public const string DeployableWeapon = "DeployableWeapon";
|
||||
public const string AetherDrive = "AetherDrive";
|
||||
public const string Thruster = "Thruster";
|
||||
public const string VelocityConversion = "VelocityConversion";
|
||||
public const string VelocityLimit = "VelocityLimit";
|
||||
public const string Reflector = "Reflector";
|
||||
public const string Visibility = "Visibility";
|
||||
public const string Sensor = "Sensor";
|
||||
public const string TurretController = "TurretController";
|
||||
|
||||
private static readonly ISet<string> WeaponKinds = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
GuidedWeapon,
|
||||
Launcher,
|
||||
InstantWeapon,
|
||||
ConstantWeapon,
|
||||
ChargedWeapon,
|
||||
AutoWeapon,
|
||||
DeployableWeapon
|
||||
};
|
||||
|
||||
public static bool IsWeapon(string kind)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(kind) && WeaponKinds.Contains(kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3f03e4d7d6e841a4a9a37b4c9d25cc5a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public enum AetheriaRuntimeBehaviorFieldValueKind
|
||||
{
|
||||
Number,
|
||||
Integer,
|
||||
Boolean,
|
||||
PerformanceStat,
|
||||
Temperature
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimeBehaviorFieldMetadata
|
||||
{
|
||||
public AetheriaRuntimeBehaviorFieldMetadata(string name, int key, AetheriaRuntimeBehaviorFieldValueKind valueKind)
|
||||
{
|
||||
Name = name ?? "";
|
||||
Key = key;
|
||||
ValueKind = valueKind;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public int Key { get; }
|
||||
public AetheriaRuntimeBehaviorFieldValueKind ValueKind { get; }
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimeBehaviorMetadata
|
||||
{
|
||||
public AetheriaRuntimeBehaviorMetadata(
|
||||
string kind,
|
||||
string parentKind,
|
||||
IReadOnlyList<AetheriaRuntimeBehaviorFieldMetadata> displayFields)
|
||||
{
|
||||
Kind = kind ?? "";
|
||||
ParentKind = parentKind ?? "";
|
||||
DisplayFields = displayFields ?? Array.Empty<AetheriaRuntimeBehaviorFieldMetadata>();
|
||||
}
|
||||
|
||||
public string Kind { get; }
|
||||
public string ParentKind { get; }
|
||||
public IReadOnlyList<AetheriaRuntimeBehaviorFieldMetadata> DisplayFields { get; }
|
||||
}
|
||||
|
||||
public static class AetheriaRuntimeBehaviorMetadataCatalog
|
||||
{
|
||||
private static readonly AetheriaRuntimeBehaviorFieldMetadata[] WeaponFields =
|
||||
{
|
||||
Stat("Damage", 2),
|
||||
Stat("Penetration", 3),
|
||||
Stat("DamageSpread", 4),
|
||||
Stat("MinRange", 5),
|
||||
Stat("Range", 6),
|
||||
Stat("Energy", 9),
|
||||
Stat("Heat", 10),
|
||||
Stat("Visibility", 11),
|
||||
Integer("MagazineSize", 13),
|
||||
Number("ReloadTime", 14),
|
||||
Stat("Spread", 15),
|
||||
Stat("Velocity", 16)
|
||||
};
|
||||
|
||||
private static readonly AetheriaRuntimeBehaviorFieldMetadata[] InstantWeaponFields =
|
||||
With(WeaponFields, Stat("BurstCount", 17), Stat("BurstTime", 18), Stat("Cooldown", 19), Boolean("SingleAmmoBurst", 20));
|
||||
|
||||
private static readonly AetheriaRuntimeBehaviorFieldMetadata[] LockWeaponFields =
|
||||
With(InstantWeaponFields, Stat("LockSpeed", 21), Stat("SensorImpact", 22), Stat("LockAngle", 23),
|
||||
Stat("DirectionImpact", 24), Stat("Decay", 25));
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, AetheriaRuntimeBehaviorMetadata> ByKind =
|
||||
new[]
|
||||
{
|
||||
Behavior("AetherDrive", "", Stat("MaximumRpm", 3), Stat("CouplingEfficiency", 6), Stat("Torque", 7), Stat("EnergyDraw", 9), Stat("PassiveCoupling", 10)),
|
||||
Behavior("Capacitor", "", Stat("Capacity", 1), Stat("Efficiency", 2)),
|
||||
Behavior("ChargedWeapon", AetheriaRuntimeBehaviorKinds.InstantWeapon, With(InstantWeaponFields,
|
||||
Stat("ChargeTime", 21), Stat("ChargeEnergy", 22), Stat("ChargeHeat", 23),
|
||||
Boolean("CanFireEarly", 24), Number("FailureCharge", 25), Number("FailureDamage", 26),
|
||||
Number("ChargeDamageMultiplier", 27), Number("ChargeSpreadMultiplier", 28),
|
||||
Number("ChargeBurstCountMultiplier", 29), Number("ChargeVisibilityMultiplier", 30),
|
||||
Number("ChargeVelocityMultiplier", 31), Number("ChargeHeatMultiplier", 32))),
|
||||
Behavior("Cockpit", ""),
|
||||
Behavior("ConstantWeapon", "Weapon", With(WeaponFields, Number("AmmoIntervalDuration", 17))),
|
||||
Behavior(AetheriaRuntimeBehaviorKinds.GuidedWeapon, AetheriaRuntimeBehaviorKinds.InstantWeapon, With(InstantWeaponFields, Stat("MissileVelocity", 26))),
|
||||
Behavior(AetheriaRuntimeBehaviorKinds.Launcher, "LockWeapon", With(LockWeaponFields, Stat("MissileVelocity", 31))),
|
||||
Behavior("LockWeapon", AetheriaRuntimeBehaviorKinds.InstantWeapon, LockWeaponFields),
|
||||
Behavior(AetheriaRuntimeBehaviorKinds.InstantWeapon, "Weapon", InstantWeaponFields),
|
||||
Behavior(AetheriaRuntimeBehaviorKinds.AutoWeapon, AetheriaRuntimeBehaviorKinds.InstantWeapon, InstantWeaponFields),
|
||||
Behavior(AetheriaRuntimeBehaviorKinds.DeployableWeapon, AetheriaRuntimeBehaviorKinds.InstantWeapon,
|
||||
With(InstantWeaponFields, Number("ActivationDelay", 26), Number("Lifetime", 27),
|
||||
Stat("TriggerRadius", 28), Number("DetonationDelay", 29), Stat("BlastRadius", 30))),
|
||||
Behavior("Cooldown", "", Stat("Cooldown", 1)),
|
||||
Behavior("EnergyDraw", "", Stat("EnergyDraw", 1)),
|
||||
Behavior("Heat", "", Stat("Heat", 1)),
|
||||
Behavior("HeatStorage", ""),
|
||||
Behavior("ItemUsage", ""),
|
||||
Behavior("MiningTool", "", Stat("DamagePerSecond", 1), Stat("Efficiency", 2), Stat("Penetration", 3), Stat("Range", 4)),
|
||||
Behavior("Radiator", "", Stat("Emissivity", 1), Stat("PumpedHeat", 2), Temperature("TemperatureFloor", 3), Stat("WasteHeat", 4), Stat("EnergyUsage", 5), Stat("ThermalMass", 6)),
|
||||
Behavior("Reactor", "", Stat("Charge", 1), Stat("Efficiency", 2), Stat("OverloadEfficiency", 3), Stat("ThrottlingFactor", 4)),
|
||||
Behavior("Reflector", "", Stat("CrossSection", 1)),
|
||||
Behavior("ResourceScanner", "", Stat("Range", 1), Stat("MinimumDensity", 2), Stat("ScanDuration", 3)),
|
||||
Behavior("Sensor", "", Stat("Sensitivity", 3), Stat("PingBoost", 5), Stat("PingEnergy", 6), Stat("PingVisibility", 7), Stat("PingRange", 8), Stat("PingCooldown", 9)),
|
||||
Behavior("Shield", "", Stat("Efficiency", 1), Stat("EnergyUsage", 2)),
|
||||
Behavior("Switch", ""),
|
||||
Behavior("Thermotoggle", "", Temperature("TargetTemperature", 1)),
|
||||
Behavior("Thruster", "", Stat("Thrust", 1), Stat("Visibility", 2), Stat("Heat", 3), Stat("EnergyUsage", 4)),
|
||||
Behavior("Trigger", ""),
|
||||
Behavior("TurretController", ""),
|
||||
Behavior("VelocityConversion", "", Stat("Lambda", 1)),
|
||||
Behavior("VelocityLimit", "", Stat("TopSpeed", 1)),
|
||||
Behavior("Visibility", "", Stat("Visibility", 1)),
|
||||
Behavior("Weapon", "", WeaponFields),
|
||||
Behavior("Wear", "")
|
||||
}
|
||||
.ToDictionary(metadata => metadata.Kind, StringComparer.Ordinal);
|
||||
|
||||
public static AetheriaRuntimeBehaviorMetadata Get(string kind)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(kind) && ByKind.TryGetValue(kind, out var metadata)
|
||||
? metadata
|
||||
: null!;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<AetheriaRuntimeBehaviorMetadata> All => ByKind.Values.ToArray();
|
||||
|
||||
public static bool IsKindOrDescendant(string candidateKind, string expectedKind)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(candidateKind) || string.IsNullOrWhiteSpace(expectedKind))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentKind = candidateKind;
|
||||
while (!string.IsNullOrWhiteSpace(currentKind))
|
||||
{
|
||||
if (string.Equals(currentKind, expectedKind, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
currentKind = ByKind.TryGetValue(currentKind, out var metadata)
|
||||
? metadata.ParentKind
|
||||
: "";
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorMetadata Behavior(
|
||||
string kind,
|
||||
string parentKind,
|
||||
params AetheriaRuntimeBehaviorFieldMetadata[] displayFields)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorMetadata(kind, parentKind, displayFields);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorFieldMetadata Stat(string name, int key)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorFieldMetadata(name, key, AetheriaRuntimeBehaviorFieldValueKind.PerformanceStat);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorFieldMetadata Number(string name, int key)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorFieldMetadata(name, key, AetheriaRuntimeBehaviorFieldValueKind.Number);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorFieldMetadata Integer(string name, int key)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorFieldMetadata(name, key, AetheriaRuntimeBehaviorFieldValueKind.Integer);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorFieldMetadata Boolean(string name, int key)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorFieldMetadata(name, key, AetheriaRuntimeBehaviorFieldValueKind.Boolean);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorFieldMetadata Temperature(string name, int key)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorFieldMetadata(name, key, AetheriaRuntimeBehaviorFieldValueKind.Temperature);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorFieldMetadata[] With(
|
||||
AetheriaRuntimeBehaviorFieldMetadata[] baseFields,
|
||||
params AetheriaRuntimeBehaviorFieldMetadata[] extraFields)
|
||||
{
|
||||
return baseFields.Concat(extraFields).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8a6d7af26b164b8fa8629e2b8efbe9c0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,495 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public static class AetheriaRuntimeBehaviorSimulation
|
||||
{
|
||||
public static void Step(
|
||||
int zoneIndex,
|
||||
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
|
||||
IEnumerable<AetheriaRuntimeDaemonBehaviorIntent>? intents,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog,
|
||||
double deltaSeconds)
|
||||
{
|
||||
if (catalog == null || deltaSeconds <= 0)
|
||||
return;
|
||||
|
||||
var intentArray = (intents ?? Enumerable.Empty<AetheriaRuntimeDaemonBehaviorIntent>()).ToArray();
|
||||
foreach (var entity in entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
|
||||
{
|
||||
if (entity == null)
|
||||
continue;
|
||||
|
||||
AetheriaRuntimeBehaviorStateProjector.EnsureEquipmentBehaviorStates(entity, catalog);
|
||||
ApplyControlIntents(zoneIndex, entity, intentArray);
|
||||
ExecuteEquipmentChains(entity, catalog, deltaSeconds);
|
||||
ProjectCooldownProgress(entity);
|
||||
}
|
||||
}
|
||||
|
||||
public static double Apply(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot catalog,
|
||||
int targetEquipmentIndex,
|
||||
AetheriaRuntimeBehaviorPayload targetBehavior,
|
||||
int targetFieldKey,
|
||||
double baseline) =>
|
||||
Apply(entity, catalog, targetEquipmentIndex, targetBehavior, targetFieldKey, baseline, null);
|
||||
|
||||
internal static double Apply(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot catalog,
|
||||
int targetEquipmentIndex,
|
||||
AetheriaRuntimeBehaviorPayload targetBehavior,
|
||||
int targetFieldKey,
|
||||
double baseline,
|
||||
IReadOnlyList<AetheriaRuntimeEquippedBehavior>? equippedModifiers)
|
||||
{
|
||||
var targetStatName = StatName(targetBehavior?.Kind, targetFieldKey);
|
||||
if (entity == null || catalog == null || targetEquipmentIndex < 0 ||
|
||||
targetBehavior == null || string.IsNullOrWhiteSpace(targetStatName))
|
||||
return baseline;
|
||||
|
||||
double multiplier = 1;
|
||||
double constant = 0;
|
||||
foreach (var modifier in ActiveEquippedModifiers(entity, catalog, equippedModifiers)
|
||||
.Concat(ActiveConsumableModifiers(entity, catalog)))
|
||||
{
|
||||
if (!Targets(modifier.Payload, entity, catalog, targetEquipmentIndex, targetBehavior, targetStatName))
|
||||
continue;
|
||||
if (IsMultiplier(modifier.Payload))
|
||||
multiplier *= modifier.Value;
|
||||
else
|
||||
constant += modifier.Value;
|
||||
}
|
||||
return baseline * multiplier + constant;
|
||||
}
|
||||
|
||||
public static void ReportSpecializedResult(
|
||||
AetheriaRuntimeEquippedBehavior behavior,
|
||||
bool succeeded)
|
||||
{
|
||||
if (behavior?.State == null || !behavior.State.ChainReached)
|
||||
return;
|
||||
behavior.State.ChainSucceeded = succeeded;
|
||||
}
|
||||
|
||||
public static void CompleteDeferredChains(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog,
|
||||
double deltaSeconds)
|
||||
{
|
||||
if (entity == null || catalog == null || deltaSeconds <= 0)
|
||||
return;
|
||||
|
||||
var states = EquipmentStates(entity);
|
||||
var online = OnlineStates(entity);
|
||||
var equipment = entity.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
|
||||
for (var equipmentIndex = 0; equipmentIndex < equipment.Count; equipmentIndex++)
|
||||
{
|
||||
var item = catalog.FindItem(equipment[equipmentIndex]?.Item?.ItemKey ?? "");
|
||||
var payloads = item?.BehaviorPayloads ?? Array.Empty<AetheriaRuntimeBehaviorPayload>();
|
||||
foreach (var group in Indexed(payloads).GroupBy(value => value.Payload.Group).OrderBy(value => value.Key))
|
||||
{
|
||||
var entries = group.ToArray();
|
||||
var deferredOffset = Array.FindIndex(entries, entry =>
|
||||
states.TryGetValue((equipmentIndex, entry.Index), out var state) &&
|
||||
state.ChainReached && IsDeferred(entry.Payload.Kind));
|
||||
if (deferredOffset < 0)
|
||||
continue;
|
||||
var deferredState = states[(equipmentIndex, entries[deferredOffset].Index)];
|
||||
if (!deferredState.ChainSucceeded || deferredState.ChainCompleted)
|
||||
continue;
|
||||
deferredState.ChainCompleted = true;
|
||||
|
||||
foreach (var entry in entries.Skip(deferredOffset + 1))
|
||||
{
|
||||
if (!states.TryGetValue((equipmentIndex, entry.Index), out var state))
|
||||
break;
|
||||
state.ChainReached = true;
|
||||
if (IsDeferred(entry.Payload.Kind) ||
|
||||
!ExecuteInline(entity, catalog, online, equipmentIndex, entry, state, deltaSeconds))
|
||||
break;
|
||||
}
|
||||
}
|
||||
ProjectModifierState(entity, catalog, equipmentIndex, payloads, states);
|
||||
}
|
||||
ProjectCooldownProgress(entity);
|
||||
}
|
||||
|
||||
public static int CountTargets(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot catalog,
|
||||
AetheriaRuntimeBehaviorPayload modifier)
|
||||
{
|
||||
var count = 0;
|
||||
var equipment = entity.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
|
||||
for (var equipmentIndex = 0; equipmentIndex < equipment.Count; equipmentIndex++)
|
||||
{
|
||||
var item = catalog.FindItem(equipment[equipmentIndex]?.Item?.ItemKey ?? "");
|
||||
foreach (var behavior in item?.BehaviorPayloads ?? Array.Empty<AetheriaRuntimeBehaviorPayload>())
|
||||
{
|
||||
if (behavior == null)
|
||||
continue;
|
||||
foreach (var stat in PerformanceStats(behavior.Kind))
|
||||
if (Targets(modifier, entity, catalog, equipmentIndex, behavior, stat.Name))
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static void ApplyControlIntents(
|
||||
int zoneIndex,
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
IReadOnlyList<AetheriaRuntimeDaemonBehaviorIntent> intents)
|
||||
{
|
||||
var states = entity.BehaviorStates ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>();
|
||||
foreach (var intent in intents.Where(value => value != null &&
|
||||
((AetheriaRuntimeRunCheckpointCommit.TryParseEntityKey(value.ActorEntityKey ?? "", out var actorZone, out var actorEntity) &&
|
||||
actorZone == zoneIndex && actorEntity == entity.EntityIndex) ||
|
||||
string.Equals(value.ActorEntityKey, entity.EntityId, StringComparison.Ordinal))))
|
||||
{
|
||||
var state = states.FirstOrDefault(value => value != null &&
|
||||
string.Equals(value.OwnerKind, AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind, StringComparison.Ordinal) &&
|
||||
value.OwnerIndex == intent.EquipmentIndex && value.BehaviorIndex == intent.BehaviorIndex);
|
||||
if (state == null)
|
||||
continue;
|
||||
if (string.Equals(state.BehaviorKind, "Switch", StringComparison.Ordinal))
|
||||
state.SwitchActivated = intent.Active;
|
||||
else if (string.Equals(state.BehaviorKind, "Trigger", StringComparison.Ordinal) && intent.Active)
|
||||
state.TriggerPulled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExecuteEquipmentChains(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot catalog,
|
||||
double deltaSeconds)
|
||||
{
|
||||
var states = EquipmentStates(entity);
|
||||
foreach (var state in states.Values)
|
||||
{
|
||||
state.ChainReached = false;
|
||||
state.ChainSucceeded = false;
|
||||
state.ChainCompleted = false;
|
||||
}
|
||||
var online = OnlineStates(entity);
|
||||
var equipment = entity.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
|
||||
for (var equipmentIndex = 0; equipmentIndex < equipment.Count; equipmentIndex++)
|
||||
{
|
||||
var equipped = equipment[equipmentIndex]?.Item;
|
||||
var item = catalog.FindItem(equipped?.ItemKey ?? "");
|
||||
var operational = equipped != null && equipped.Enabled && equipped.Durability > 0.01 &&
|
||||
(!online.TryGetValue(equipmentIndex, out var equipmentState) || equipmentState.Online);
|
||||
var payloads = item?.BehaviorPayloads ?? Array.Empty<AetheriaRuntimeBehaviorPayload>();
|
||||
|
||||
foreach (var entry in Indexed(payloads))
|
||||
if (entry.Payload != null && string.Equals(entry.Payload.Kind, "StatModifier", StringComparison.Ordinal) &&
|
||||
states.TryGetValue((equipmentIndex, entry.Index), out var modifierState))
|
||||
modifierState.StatModifierExecuted = false;
|
||||
|
||||
if (operational)
|
||||
foreach (var group in Indexed(payloads).GroupBy(value => value.Payload.Group).OrderBy(value => value.Key))
|
||||
foreach (var entry in group)
|
||||
{
|
||||
if (!states.TryGetValue((equipmentIndex, entry.Index), out var state))
|
||||
break;
|
||||
state.ChainReached = true;
|
||||
if (IsDeferred(entry.Payload.Kind) ||
|
||||
!ExecuteInline(entity, catalog, online, equipmentIndex, entry, state, deltaSeconds))
|
||||
break;
|
||||
}
|
||||
|
||||
ProjectModifierState(entity, catalog, equipmentIndex, payloads, states);
|
||||
|
||||
foreach (var entry in Indexed(payloads))
|
||||
{
|
||||
if (entry.Payload == null || !string.Equals(entry.Payload.Kind, "Cooldown", StringComparison.Ordinal) ||
|
||||
!states.TryGetValue((equipmentIndex, entry.Index), out var state))
|
||||
continue;
|
||||
var duration = Evaluate(entity, catalog, equipmentIndex, entry.Index, entry.Payload, state, 1);
|
||||
state.CooldownProgress -= deltaSeconds / Math.Max(double.Epsilon, duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ExecuteInline(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot catalog,
|
||||
IReadOnlyDictionary<int, AetheriaRuntimeEquipmentStateCommit> online,
|
||||
int equipmentIndex,
|
||||
(AetheriaRuntimeBehaviorPayload Payload, int Index) entry,
|
||||
AetheriaRuntimeBehaviorStateCommit state,
|
||||
double deltaSeconds)
|
||||
{
|
||||
if (string.Equals(entry.Payload.Kind, "Switch", StringComparison.Ordinal) && !state.SwitchActivated)
|
||||
return false;
|
||||
if (string.Equals(entry.Payload.Kind, "Trigger", StringComparison.Ordinal))
|
||||
{
|
||||
if (!state.TriggerPulled)
|
||||
return false;
|
||||
state.TriggerPulled = false;
|
||||
}
|
||||
if (string.Equals(entry.Payload.Kind, "Thermotoggle", StringComparison.Ordinal))
|
||||
{
|
||||
var temperature = AetheriaRuntimeThermalSimulation.EquipmentTemperature(entity, catalog, equipmentIndex);
|
||||
if (!((temperature < state.ThermotoggleTargetTemperature) ^ ReadBool(entry.Payload, 2)))
|
||||
return false;
|
||||
}
|
||||
if (string.Equals(entry.Payload.Kind, "Cooldown", StringComparison.Ordinal))
|
||||
{
|
||||
if (state.CooldownProgress >= 0)
|
||||
return false;
|
||||
state.CooldownProgress = 1;
|
||||
}
|
||||
if (string.Equals(entry.Payload.Kind, "EnergyDraw", StringComparison.Ordinal))
|
||||
{
|
||||
var demand = Evaluate(entity, catalog, equipmentIndex, entry.Index, entry.Payload, state, 1) *
|
||||
(ReadBool(entry.Payload, 2) ? deltaSeconds : 1);
|
||||
if (!AetheriaRuntimeEnergySimulation.TryConsume(entity, catalog, demand))
|
||||
return false;
|
||||
}
|
||||
if (string.Equals(entry.Payload.Kind, "Heat", StringComparison.Ordinal))
|
||||
{
|
||||
var heat = Evaluate(entity, catalog, equipmentIndex, entry.Index, entry.Payload, state, 1) *
|
||||
(ReadBool(entry.Payload, 2) ? deltaSeconds : 1);
|
||||
AetheriaRuntimeThermalSimulation.AddHeatToEquipment(entity, catalog, equipmentIndex, heat);
|
||||
}
|
||||
if (string.Equals(entry.Payload.Kind, "ItemUsage", StringComparison.Ordinal))
|
||||
{
|
||||
var itemKey = ReadItemKey(entry.Payload, 1);
|
||||
if (string.IsNullOrWhiteSpace(itemKey) ||
|
||||
!AetheriaRuntimeCargoTransactions.TryFind(entity, itemKey, out var cargoIndex, out var x, out var y) ||
|
||||
!AetheriaRuntimeCargoTransactions.TryRemoveQuantity(entity, cargoIndex, itemKey, x, y, 1, out _))
|
||||
return false;
|
||||
}
|
||||
if (string.Equals(entry.Payload.Kind, "Wear", StringComparison.Ordinal))
|
||||
{
|
||||
var wear = online.TryGetValue(equipmentIndex, out var wearState) ? wearState.Wear : 0;
|
||||
AetheriaRuntimeThermalSimulation.ApplyWear(
|
||||
entity, equipmentIndex, wear * (ReadBool(entry.Payload, 1, true) ? deltaSeconds : 1));
|
||||
}
|
||||
if (string.Equals(entry.Payload.Kind, "StatModifier", StringComparison.Ordinal))
|
||||
state.StatModifierExecuted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsDeferred(string? kind) =>
|
||||
string.Equals(kind, AetheriaRuntimeBehaviorKinds.Thruster, StringComparison.Ordinal) ||
|
||||
string.Equals(kind, AetheriaRuntimeBehaviorKinds.AetherDrive, StringComparison.Ordinal) ||
|
||||
string.Equals(kind, "Radiator", StringComparison.Ordinal) ||
|
||||
string.Equals(kind, "MiningTool", StringComparison.Ordinal) ||
|
||||
string.Equals(kind, "ResourceScanner", StringComparison.Ordinal);
|
||||
|
||||
private static IEnumerable<(AetheriaRuntimeBehaviorPayload Payload, int Index)> Indexed(
|
||||
IReadOnlyList<AetheriaRuntimeBehaviorPayload> payloads) =>
|
||||
payloads.Select((payload, index) => (Payload: payload, Index: index))
|
||||
.Where(value => value.Payload != null);
|
||||
|
||||
private static Dictionary<(int OwnerIndex, int BehaviorIndex), AetheriaRuntimeBehaviorStateCommit>
|
||||
EquipmentStates(AetheriaRuntimeEntitySnapshotCommit entity) =>
|
||||
(entity.BehaviorStates ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>())
|
||||
.Where(value => value != null && string.Equals(value.OwnerKind,
|
||||
AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind, StringComparison.Ordinal))
|
||||
.ToDictionary(value => (value.OwnerIndex, value.BehaviorIndex));
|
||||
|
||||
private static Dictionary<int, AetheriaRuntimeEquipmentStateCommit> OnlineStates(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity) =>
|
||||
(entity.EquipmentStates ?? Array.Empty<AetheriaRuntimeEquipmentStateCommit>())
|
||||
.Where(value => value != null).ToDictionary(value => value.EquipmentIndex);
|
||||
|
||||
private static void ProjectModifierState(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot catalog,
|
||||
int equipmentIndex,
|
||||
IReadOnlyList<AetheriaRuntimeBehaviorPayload> payloads,
|
||||
IReadOnlyDictionary<(int OwnerIndex, int BehaviorIndex), AetheriaRuntimeBehaviorStateCommit> states)
|
||||
{
|
||||
foreach (var entry in Indexed(payloads))
|
||||
{
|
||||
if (!string.Equals(entry.Payload.Kind, "StatModifier", StringComparison.Ordinal) ||
|
||||
!states.TryGetValue((equipmentIndex, entry.Index), out var state))
|
||||
continue;
|
||||
state.StatModifierApplied = state.StatModifierExecuted;
|
||||
state.StatModifierTargetStatCount = CountTargets(entity, catalog, entry.Payload);
|
||||
}
|
||||
}
|
||||
|
||||
private static double Evaluate(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot catalog,
|
||||
int equipmentIndex,
|
||||
int behaviorIndex,
|
||||
AetheriaRuntimeBehaviorPayload payload,
|
||||
AetheriaRuntimeBehaviorStateCommit state,
|
||||
int fieldKey)
|
||||
{
|
||||
var equipment = entity.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
|
||||
if (equipmentIndex < 0 || equipmentIndex >= equipment.Count || equipment[equipmentIndex]?.Item == null)
|
||||
return 0;
|
||||
var item = equipment[equipmentIndex].Item;
|
||||
var typed = catalog.FindItem(item.ItemKey ?? "");
|
||||
if (typed == null)
|
||||
return 0;
|
||||
var behavior = new AetheriaRuntimeEquippedBehavior(
|
||||
entity, catalog, equipmentIndex, behaviorIndex, equipment[equipmentIndex], item, typed, payload, state);
|
||||
var thermal = (entity.EquipmentStates ?? Array.Empty<AetheriaRuntimeEquipmentStateCommit>())
|
||||
.FirstOrDefault(value => value?.EquipmentIndex == equipmentIndex)?.ThermalPerformance ?? 1;
|
||||
return behavior.EvaluateStat(fieldKey, thermal);
|
||||
}
|
||||
|
||||
private static void ProjectCooldownProgress(AetheriaRuntimeEntitySnapshotCommit entity)
|
||||
{
|
||||
var retained = (entity.BehaviorProgress ?? Array.Empty<AetheriaRuntimeBehaviorProgressCommit>())
|
||||
.Where(value => value != null &&
|
||||
!(string.Equals(value.OwnerKind, AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind, StringComparison.Ordinal) &&
|
||||
string.Equals(value.BehaviorKind, "Cooldown", StringComparison.Ordinal)));
|
||||
var cooldowns = (entity.BehaviorStates ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>())
|
||||
.Where(value => value != null &&
|
||||
string.Equals(value.OwnerKind, AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind, StringComparison.Ordinal) &&
|
||||
string.Equals(value.BehaviorKind, "Cooldown", StringComparison.Ordinal))
|
||||
.Select(value => new AetheriaRuntimeBehaviorProgressCommit
|
||||
{
|
||||
OwnerKind = value.OwnerKind,
|
||||
OwnerIndex = value.OwnerIndex,
|
||||
BehaviorIndex = value.BehaviorIndex,
|
||||
BehaviorKind = value.BehaviorKind,
|
||||
Progress = Math.Max(0, Math.Min(1, value.CooldownProgress))
|
||||
});
|
||||
entity.BehaviorProgress = retained.Concat(cooldowns)
|
||||
.OrderBy(value => value.OwnerKind, StringComparer.Ordinal)
|
||||
.ThenBy(value => value.OwnerIndex)
|
||||
.ThenBy(value => value.BehaviorIndex)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static IEnumerable<ModifierValue> ActiveEquippedModifiers(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot catalog,
|
||||
IReadOnlyList<AetheriaRuntimeEquippedBehavior>? equippedModifiers = null)
|
||||
{
|
||||
foreach (var behavior in equippedModifiers ??
|
||||
AetheriaRuntimeEquippedBehaviorQueries.FindOperational(entity, catalog, "StatModifier"))
|
||||
{
|
||||
if (!behavior.State.StatModifierApplied)
|
||||
continue;
|
||||
var value = Field(behavior.Payload, 2)?.Value;
|
||||
var thermal = (entity.EquipmentStates ?? Array.Empty<AetheriaRuntimeEquipmentStateCommit>())
|
||||
.FirstOrDefault(state => state?.EquipmentIndex == behavior.EquipmentIndex)?.ThermalPerformance ?? 1;
|
||||
yield return new ModifierValue(behavior.Payload,
|
||||
AetheriaRuntimeDaemonItemStatQueries.EvaluatePerformanceStat(value, behavior.Item, thermal));
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<ModifierValue> ActiveConsumableModifiers(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot catalog)
|
||||
{
|
||||
foreach (var effect in entity.ActiveConsumables ?? Array.Empty<AetheriaRuntimeActiveConsumableCommit>())
|
||||
{
|
||||
var item = catalog.FindItem(effect?.ItemKey ?? "");
|
||||
if (effect == null || item == null)
|
||||
continue;
|
||||
var elapsed = effect.Duration <= 0 ? 1 : Math.Max(0, Math.Min(1,
|
||||
(effect.Duration - effect.RemainingDuration) / effect.Duration));
|
||||
var effectiveness = item.EffectivenessCurveKeys == null || item.EffectivenessCurveKeys.Count == 0
|
||||
? 1 : AetheriaRuntimeDaemonItemStatQueries.SampleCurve(item.EffectivenessCurveKeys, elapsed);
|
||||
foreach (var entry in (item.BehaviorPayloads ?? Array.Empty<AetheriaRuntimeBehaviorPayload>())
|
||||
.Select((payload, index) => (Payload: payload, Index: index)))
|
||||
{
|
||||
var state = (effect.BehaviorStates ?? Array.Empty<AetheriaRuntimeConsumableBehaviorStateCommit>())
|
||||
.FirstOrDefault(value => value?.BehaviorIndex == entry.Index);
|
||||
if (entry.Payload != null && string.Equals(entry.Payload.Kind, "StatModifier", StringComparison.Ordinal) &&
|
||||
state?.StatModifierApplied == true)
|
||||
yield return new ModifierValue(entry.Payload,
|
||||
AetheriaRuntimeDaemonItemStatQueries.EvaluateConsumablePerformanceStat(
|
||||
Field(entry.Payload, 2)?.Value, effect.Quality, effectiveness));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool Targets(
|
||||
AetheriaRuntimeBehaviorPayload modifier,
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot catalog,
|
||||
int targetEquipmentIndex,
|
||||
AetheriaRuntimeBehaviorPayload targetBehavior,
|
||||
string targetStatName)
|
||||
{
|
||||
var reference = Field(modifier, 1)?.Value;
|
||||
var expectedKind = NormalizeKind(ChildString(reference, 1));
|
||||
if (!AetheriaRuntimeBehaviorMetadataCatalog.IsKindOrDescendant(targetBehavior?.Kind ?? "", expectedKind) ||
|
||||
!string.Equals(ChildString(reference, 2), targetStatName, StringComparison.Ordinal))
|
||||
return false;
|
||||
var requiredKind = NormalizeKind(ReadString(Field(modifier, 4)?.Value));
|
||||
if (string.IsNullOrWhiteSpace(requiredKind))
|
||||
return true;
|
||||
var targetItemKey = (entity.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
||||
.ElementAtOrDefault(targetEquipmentIndex)?.Item?.ItemKey ?? "";
|
||||
return (catalog.FindItem(targetItemKey)?.BehaviorPayloads ?? Array.Empty<AetheriaRuntimeBehaviorPayload>())
|
||||
.Any(value => AetheriaRuntimeBehaviorMetadataCatalog.IsKindOrDescendant(value?.Kind ?? "", requiredKind));
|
||||
}
|
||||
|
||||
private static bool IsMultiplier(AetheriaRuntimeBehaviorPayload payload)
|
||||
{
|
||||
var value = Field(payload, 3)?.Value;
|
||||
return string.Equals(ReadString(value), "Multiplier", StringComparison.OrdinalIgnoreCase) ||
|
||||
(string.IsNullOrWhiteSpace(ReadString(value)) && Math.Round(value?.NumberValue ?? 0) == 1);
|
||||
}
|
||||
|
||||
private static string NormalizeKind(string value) =>
|
||||
!string.IsNullOrWhiteSpace(value) && value.EndsWith("Data", StringComparison.Ordinal)
|
||||
? value.Substring(0, value.Length - 4) : value ?? "";
|
||||
|
||||
private static string StatName(string? behaviorKind, int fieldKey) =>
|
||||
PerformanceStats(behaviorKind).FirstOrDefault(value => value.Key == fieldKey)?.Name ?? "";
|
||||
|
||||
private static IEnumerable<AetheriaRuntimeBehaviorFieldMetadata> PerformanceStats(string? behaviorKind) =>
|
||||
AetheriaRuntimeBehaviorMetadataCatalog.Get(behaviorKind ?? "")?.DisplayFields?
|
||||
.Where(value => value.ValueKind == AetheriaRuntimeBehaviorFieldValueKind.PerformanceStat)
|
||||
?? Enumerable.Empty<AetheriaRuntimeBehaviorFieldMetadata>();
|
||||
|
||||
private static AetheriaRuntimeBehaviorField? Field(AetheriaRuntimeBehaviorPayload payload, int key) =>
|
||||
(payload?.Fields ?? Array.Empty<AetheriaRuntimeBehaviorField>()).FirstOrDefault(value => value?.Key == key);
|
||||
|
||||
private static bool ReadBool(AetheriaRuntimeBehaviorPayload payload, int key, bool fallback = false)
|
||||
{
|
||||
var field = Field(payload, key);
|
||||
return field == null ? fallback : field.Value?.BoolValue ?? fallback;
|
||||
}
|
||||
|
||||
private static string ReadItemKey(AetheriaRuntimeBehaviorPayload payload, int key)
|
||||
{
|
||||
var value = Field(payload, key)?.Value;
|
||||
return !string.IsNullOrWhiteSpace(value?.ItemKeyValue)
|
||||
? value.ItemKeyValue
|
||||
: ReadString(value);
|
||||
}
|
||||
|
||||
private static string ChildString(AetheriaRuntimeBehaviorValue? value, int index) =>
|
||||
value != null && value.Children != null && value.Children.Count > index
|
||||
? ReadString(value.Children[index]) : "";
|
||||
|
||||
private static string ReadString(AetheriaRuntimeBehaviorValue? value) =>
|
||||
!string.IsNullOrWhiteSpace(value?.StringValue) ? value.StringValue :
|
||||
!string.IsNullOrWhiteSpace(value?.LegacyIdValue) ? value.LegacyIdValue :
|
||||
value?.ItemKeyValue ?? "";
|
||||
|
||||
private readonly struct ModifierValue
|
||||
{
|
||||
public ModifierValue(AetheriaRuntimeBehaviorPayload payload, double value)
|
||||
{
|
||||
Payload = payload;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public AetheriaRuntimeBehaviorPayload Payload { get; }
|
||||
public double Value { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5f127e58b7fe83d41aae3d2c07e92b93
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public static class AetheriaRuntimeBehaviorStateProjector
|
||||
{
|
||||
public const string EquipmentOwnerKind = "equipment";
|
||||
|
||||
public static void EnsureEquipmentBehaviorStates(
|
||||
AetheriaRuntimeEntitySnapshotCommit? entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog)
|
||||
{
|
||||
if (entity == null)
|
||||
return;
|
||||
|
||||
var existing = entity.BehaviorStates ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>();
|
||||
if (EquipmentBehaviorStatesMatch(existing, entity.Equipment, catalog))
|
||||
return;
|
||||
var projected = CreateEquipmentBehaviorStates(entity.Equipment, catalog);
|
||||
if (projected.Length == 0)
|
||||
{
|
||||
entity.BehaviorStates = existing
|
||||
.Where(state => state != null && !string.Equals(state.OwnerKind, EquipmentOwnerKind, StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
return;
|
||||
}
|
||||
|
||||
var existingByKey = existing
|
||||
.Where(state => state != null)
|
||||
.GroupBy(StateKey, StringComparer.Ordinal)
|
||||
.ToDictionary(group => group.Key, group => group.First(), StringComparer.Ordinal);
|
||||
var nonEquipmentStates = existing
|
||||
.Where(state => state != null && !string.Equals(state.OwnerKind, EquipmentOwnerKind, StringComparison.Ordinal));
|
||||
var equipmentStates = projected
|
||||
.Select(state => existingByKey.TryGetValue(StateKey(state), out var current) ? current : state);
|
||||
entity.BehaviorStates = nonEquipmentStates
|
||||
.Concat(equipmentStates)
|
||||
.OrderBy(state => state.OwnerKind, StringComparer.Ordinal)
|
||||
.ThenBy(state => state.OwnerIndex)
|
||||
.ThenBy(state => state.BehaviorIndex)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeBehaviorStateCommit[] CreateEquipmentBehaviorStates(
|
||||
IReadOnlyList<AetheriaRuntimeLoadoutItemSlotCommit>? equipment,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog)
|
||||
{
|
||||
catalog ??= new AetheriaRuntimeCatalogSnapshot();
|
||||
return (equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
||||
.SelectMany((slot, equipmentIndex) => CreateEquipmentBehaviorStates(slot?.Item, equipmentIndex, catalog))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static IEnumerable<AetheriaRuntimeBehaviorStateCommit> CreateEquipmentBehaviorStates(
|
||||
AetheriaRuntimeLoadoutItemCommit? item,
|
||||
int equipmentIndex,
|
||||
AetheriaRuntimeCatalogSnapshot catalog)
|
||||
{
|
||||
if (item == null || equipmentIndex < 0)
|
||||
yield break;
|
||||
|
||||
var typedItem = catalog.FindItem(item.ItemKey ?? "");
|
||||
var payloads = typedItem?.BehaviorPayloads ?? Array.Empty<AetheriaRuntimeBehaviorPayload>();
|
||||
for (var behaviorIndex = 0; behaviorIndex < payloads.Count; behaviorIndex++)
|
||||
{
|
||||
var payload = payloads[behaviorIndex];
|
||||
if (payload == null || string.IsNullOrWhiteSpace(payload.Kind))
|
||||
continue;
|
||||
|
||||
var state = new AetheriaRuntimeBehaviorStateCommit
|
||||
{
|
||||
OwnerKind = EquipmentOwnerKind,
|
||||
OwnerIndex = equipmentIndex,
|
||||
BehaviorIndex = behaviorIndex,
|
||||
BehaviorKind = payload.Kind
|
||||
};
|
||||
if (string.Equals(payload.Kind, "Thermotoggle", StringComparison.Ordinal))
|
||||
state.ThermotoggleTargetTemperature = (payload.Fields ?? Array.Empty<AetheriaRuntimeBehaviorField>())
|
||||
.FirstOrDefault(field => field != null && field.Key == 1)?.Value?.NumberValue ?? 0;
|
||||
yield return state;
|
||||
}
|
||||
}
|
||||
|
||||
private static string StateKey(AetheriaRuntimeBehaviorStateCommit? state)
|
||||
{
|
||||
if (state == null)
|
||||
return "";
|
||||
|
||||
return string.Join(
|
||||
"/",
|
||||
state.OwnerKind ?? "",
|
||||
state.OwnerIndex.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||
state.BehaviorIndex.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||
state.BehaviorKind ?? "");
|
||||
}
|
||||
|
||||
private static bool EquipmentBehaviorStatesMatch(
|
||||
IReadOnlyList<AetheriaRuntimeBehaviorStateCommit> existing,
|
||||
IReadOnlyList<AetheriaRuntimeLoadoutItemSlotCommit>? equipment,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog)
|
||||
{
|
||||
catalog ??= new AetheriaRuntimeCatalogSnapshot();
|
||||
var matched = 0;
|
||||
var slots = equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
|
||||
for (var equipmentIndex = 0; equipmentIndex < slots.Count; equipmentIndex++)
|
||||
{
|
||||
var item = slots[equipmentIndex]?.Item;
|
||||
var payloads = catalog.FindItem(item?.ItemKey ?? "")?.BehaviorPayloads ??
|
||||
Array.Empty<AetheriaRuntimeBehaviorPayload>();
|
||||
for (var behaviorIndex = 0; behaviorIndex < payloads.Count; behaviorIndex++)
|
||||
{
|
||||
var payload = payloads[behaviorIndex];
|
||||
if (payload == null || string.IsNullOrWhiteSpace(payload.Kind))
|
||||
continue;
|
||||
matched++;
|
||||
if (!existing.Any(state => state != null &&
|
||||
string.Equals(state.OwnerKind, EquipmentOwnerKind, StringComparison.Ordinal) &&
|
||||
state.OwnerIndex == equipmentIndex &&
|
||||
state.BehaviorIndex == behaviorIndex &&
|
||||
string.Equals(state.BehaviorKind, payload.Kind, StringComparison.Ordinal)))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return existing.Count(state => state != null &&
|
||||
string.Equals(state.OwnerKind, EquipmentOwnerKind, StringComparison.Ordinal)) == matched;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c014edb10e374064983c53c270e56b25
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public static class AetheriaRuntimeCargoCapacityQueries
|
||||
{
|
||||
public static int Quantity(AetheriaRuntimeEntitySnapshotCommit? entity) =>
|
||||
(entity?.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>())
|
||||
.Where(bay => bay != null)
|
||||
.SelectMany(bay => bay.Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
||||
.Where(slot => slot?.Item != null)
|
||||
.Sum(slot => Math.Max(0, slot.Item.Quantity));
|
||||
|
||||
public static double Capacity(AetheriaRuntimeEntitySnapshotCommit? entity, AetheriaRuntimeCatalogSnapshot? catalog)
|
||||
{
|
||||
if (entity == null || catalog == null)
|
||||
return 0;
|
||||
var hull = catalog.FindItem(entity.HullItemKey ?? "");
|
||||
if (hull == null)
|
||||
return 0;
|
||||
return Math.Max(0, AetheriaRuntimeDaemonItemStatQueries.EvaluatePerformanceStat(
|
||||
hull.HullCapacity,
|
||||
new AetheriaRuntimeLoadoutItemCommit { ItemKey = hull.ItemKey, Quality = 1, Durability = 1, Enabled = true },
|
||||
1));
|
||||
}
|
||||
|
||||
public static double Occupied(AetheriaRuntimeEntitySnapshotCommit? entity, AetheriaRuntimeCatalogSnapshot? catalog)
|
||||
{
|
||||
if (entity == null || catalog == null)
|
||||
return 0;
|
||||
var equipment = (entity.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
||||
.Sum(slot => Volume(slot?.Item, catalog));
|
||||
var cargoBays = (entity.CargoBays ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
||||
.Sum(slot => Volume(slot?.Item, catalog));
|
||||
var dockingBays = (entity.DockingBays ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
||||
.Sum(slot => Volume(slot?.Item, catalog));
|
||||
var cargo = (entity.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>())
|
||||
.SelectMany(bay => bay?.Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
||||
.Sum(slot => Volume(slot?.Item, catalog));
|
||||
var dockingCargo = (entity.DockingBayContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>())
|
||||
.SelectMany(bay => bay?.Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
||||
.Sum(slot => Volume(slot?.Item, catalog));
|
||||
return equipment + cargoBays + dockingBays + cargo + dockingCargo;
|
||||
}
|
||||
|
||||
public static double Available(AetheriaRuntimeEntitySnapshotCommit? entity, AetheriaRuntimeCatalogSnapshot? catalog) =>
|
||||
Math.Max(0, Capacity(entity, catalog) - Occupied(entity, catalog));
|
||||
|
||||
public static int UnitsThatFit(AetheriaRuntimeEntitySnapshotCommit? entity, AetheriaRuntimeCatalogSnapshot? catalog, string itemKey)
|
||||
{
|
||||
return UnitsThatFit(entity, catalog, itemKey, 0);
|
||||
}
|
||||
|
||||
public static int UnitsThatFit(
|
||||
AetheriaRuntimeEntitySnapshotCommit? entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog,
|
||||
string itemKey,
|
||||
int cargoIndex)
|
||||
{
|
||||
var volume = catalog?.FindItem(itemKey ?? "")?.Volume ?? 0;
|
||||
if (volume <= 0)
|
||||
return int.MaxValue;
|
||||
var cargoBays = (entity?.CargoBays ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
||||
.Select(slot => catalog?.FindItem(slot?.Item?.ItemKey ?? ""))
|
||||
.ToArray();
|
||||
if (cargoIndex < 0 || cargoIndex >= cargoBays.Length)
|
||||
return 0;
|
||||
var capacity = cargoBays[cargoIndex]!.InteriorOccupiedCells;
|
||||
var bay = (entity?.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>())
|
||||
.ElementAtOrDefault(cargoIndex);
|
||||
var occupied = (bay?.Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
||||
.Sum(slot => Volume(slot?.Item, catalog!));
|
||||
return Math.Max(0, (int)Math.Floor((capacity - occupied) / volume));
|
||||
}
|
||||
|
||||
private static double Volume(AetheriaRuntimeLoadoutItemCommit? item, AetheriaRuntimeCatalogSnapshot catalog) =>
|
||||
item == null ? 0 : Math.Max(0, catalog.FindItem(item.ItemKey ?? "")?.Volume ?? 0) * Math.Max(1, item.Quantity);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 65b37579114393648b8c881ae488399b
|
||||
|
|
@ -1,734 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using GameCult.Eve.Surface;
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public sealed class AetheriaRuntimeCargoItemMetric
|
||||
{
|
||||
public AetheriaRuntimeCargoItemMetric(string id, string label, string value, string valueRef = "")
|
||||
{
|
||||
Id = id ?? "";
|
||||
Label = label ?? "";
|
||||
Value = value ?? "";
|
||||
ValueRef = valueRef ?? "";
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
public string Label { get; }
|
||||
public string Value { get; }
|
||||
public string ValueRef { get; }
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimeCargoItemSection
|
||||
{
|
||||
public AetheriaRuntimeCargoItemSection(
|
||||
string id,
|
||||
string title,
|
||||
IReadOnlyList<AetheriaRuntimeCargoItemMetric> metrics)
|
||||
{
|
||||
Id = id ?? "";
|
||||
Title = title ?? "";
|
||||
Metrics = metrics ?? Array.Empty<AetheriaRuntimeCargoItemMetric>();
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
public string Title { get; }
|
||||
public IReadOnlyList<AetheriaRuntimeCargoItemMetric> Metrics { get; }
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimeCargoItemObservation
|
||||
{
|
||||
public AetheriaRuntimeCargoItemObservation(
|
||||
string itemKey,
|
||||
int quantity,
|
||||
bool isEquippable,
|
||||
double quality,
|
||||
double durability,
|
||||
double temperature,
|
||||
bool overrideShutdown)
|
||||
{
|
||||
ItemKey = itemKey ?? "";
|
||||
Quantity = quantity;
|
||||
IsEquippable = isEquippable;
|
||||
Quality = quality;
|
||||
Durability = durability;
|
||||
Temperature = temperature;
|
||||
OverrideShutdown = overrideShutdown;
|
||||
}
|
||||
|
||||
public string ItemKey { get; }
|
||||
public int Quantity { get; }
|
||||
public bool IsEquippable { get; }
|
||||
public double Quality { get; }
|
||||
public double Durability { get; }
|
||||
public double Temperature { get; }
|
||||
public bool OverrideShutdown { get; }
|
||||
}
|
||||
|
||||
public static class AetheriaRuntimeCargoItemDetailsSurfaceBuilder
|
||||
{
|
||||
public const string SurfaceId = "aetheria.inventory.cargo_item_details";
|
||||
public const string Close = "aetheria.inventory.cargo_item_details.close";
|
||||
|
||||
public static AetheriaRuntimeSurfaceDocument Build(
|
||||
AetheriaRuntimeCatalogItem typedItem,
|
||||
AetheriaRuntimeCargoItemObservation item,
|
||||
string manufacturer,
|
||||
string tier,
|
||||
Func<float, string> formatValue,
|
||||
Func<float, string> formatTemperature,
|
||||
DateTime updatedAtUtc = default(DateTime),
|
||||
long version = 1)
|
||||
{
|
||||
if (updatedAtUtc == default(DateTime))
|
||||
updatedAtUtc = DateTime.UtcNow;
|
||||
|
||||
var itemName = typedItem?.Name ?? "";
|
||||
var description = typedItem?.Description ?? "";
|
||||
manufacturer ??= "";
|
||||
var quantity = item?.Quantity ?? 0;
|
||||
tier ??= "";
|
||||
var mass = typedItem == null ? "" : FormatValue(Mass(typedItem, quantity), formatValue);
|
||||
var price = typedItem?.Price ?? 0;
|
||||
var durability = "";
|
||||
var thermalRange = "";
|
||||
var behaviorSections = Array.Empty<AetheriaRuntimeCargoItemSection>();
|
||||
|
||||
if (typedItem != null && item != null && item.IsEquippable)
|
||||
{
|
||||
durability = $"{(int)(item.Durability / MaxDurability(typedItem, item) * 100)}%";
|
||||
thermalRange = FormatTemperatureRange(typedItem, formatTemperature);
|
||||
behaviorSections = ProjectBehaviorSections(typedItem, item, formatValue, formatTemperature).ToArray();
|
||||
}
|
||||
var hasEquipmentStatus = !string.IsNullOrWhiteSpace(tier) ||
|
||||
!string.IsNullOrWhiteSpace(durability) ||
|
||||
!string.IsNullOrWhiteSpace(thermalRange);
|
||||
var updated = updatedAtUtc.ToString("O", CultureInfo.InvariantCulture);
|
||||
|
||||
var children = new List<AetheriaRuntimeSurfaceComponent>
|
||||
{
|
||||
Card(
|
||||
$"{SurfaceId}.summary",
|
||||
itemName,
|
||||
Text(
|
||||
$"{SurfaceId}.description",
|
||||
string.IsNullOrWhiteSpace(description)
|
||||
? "No typed item description is available."
|
||||
: description),
|
||||
Text(
|
||||
$"{SurfaceId}.note",
|
||||
"The observing client supplies the selected item; the shared runtime surface owns cargo-item inspection layout."),
|
||||
Metric($"{SurfaceId}.manufacturer", "Manufacturer", manufacturer),
|
||||
Metric($"{SurfaceId}.mass", "Mass", mass))
|
||||
};
|
||||
|
||||
if (price > 0)
|
||||
{
|
||||
children.Add(Card(
|
||||
$"{SurfaceId}.market.card",
|
||||
"Market",
|
||||
Metric($"{SurfaceId}.price", "Price", price.ToString("N0"))));
|
||||
}
|
||||
|
||||
if (quantity > 0)
|
||||
{
|
||||
children.Add(Card(
|
||||
$"{SurfaceId}.quantity.card",
|
||||
"Quantity",
|
||||
Metric($"{SurfaceId}.quantity", "Units", quantity.ToString())));
|
||||
}
|
||||
|
||||
if (hasEquipmentStatus)
|
||||
{
|
||||
children.Add(Card(
|
||||
$"{SurfaceId}.status.card",
|
||||
"Status",
|
||||
Metric($"{SurfaceId}.tier", "Tier", tier),
|
||||
Metric($"{SurfaceId}.durability", "Durability", durability),
|
||||
Metric($"{SurfaceId}.temperature_range", "Thermal Range", thermalRange)));
|
||||
}
|
||||
|
||||
foreach (var section in behaviorSections)
|
||||
{
|
||||
if (section?.Metrics == null || section.Metrics.Count == 0)
|
||||
continue;
|
||||
|
||||
children.Add(Card(
|
||||
string.IsNullOrWhiteSpace(section.Id)
|
||||
? $"{SurfaceId}.behavior.{SafeId(section.Title)}"
|
||||
: section.Id,
|
||||
section.Title,
|
||||
section.Metrics
|
||||
.Select(metric => Metric(metric.Id, metric.Label, metric.Value, metric.ValueRef))
|
||||
.ToArray()));
|
||||
}
|
||||
|
||||
children.Add(ButtonRow(
|
||||
$"{SurfaceId}.actions",
|
||||
Button($"{SurfaceId}.close", "Close", Close)));
|
||||
|
||||
return new AetheriaRuntimeSurfaceDocument(
|
||||
providerId: "aetheria",
|
||||
providerKind: "inventory.menu",
|
||||
title: "Inventory Cargo Item Details",
|
||||
version: version,
|
||||
updatedAtUtc: updated,
|
||||
surface: new AetheriaRuntimeSurfaceTree(
|
||||
SurfaceId,
|
||||
Node(
|
||||
$"{SurfaceId}.root",
|
||||
"surface",
|
||||
Array.Empty<(string Key, string Value)>(),
|
||||
children.ToArray()),
|
||||
Array.Empty<AetheriaRuntimeSurfaceStyleToken>()),
|
||||
commands: new[]
|
||||
{
|
||||
new AetheriaRuntimeSurfaceCommandTemplate(Close, "Close", AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport)
|
||||
});
|
||||
}
|
||||
|
||||
private static IEnumerable<AetheriaRuntimeCargoItemSection> ProjectBehaviorSections(
|
||||
AetheriaRuntimeCatalogItem typedItem,
|
||||
AetheriaRuntimeCargoItemObservation item,
|
||||
Func<float, string> formatValue,
|
||||
Func<float, string> formatTemperature)
|
||||
{
|
||||
foreach (var behavior in typedItem.BehaviorPayloads ?? Array.Empty<AetheriaRuntimeBehaviorPayload>())
|
||||
{
|
||||
if (string.Equals(behavior.Kind, AetheriaRuntimeBehaviorKinds.StatModifier, StringComparison.Ordinal))
|
||||
{
|
||||
var statReference = ReadStatReference(FindTypedBehaviorField(behavior, 1)?.Value);
|
||||
var modifier = ReadPerformanceStat(FindTypedBehaviorField(behavior, 2)?.Value);
|
||||
var modifierType = ReadEnum(
|
||||
FindTypedBehaviorField(behavior, 3)?.Value,
|
||||
AetheriaRuntimeCargoItemStatModifierType.Constant);
|
||||
yield return new AetheriaRuntimeCargoItemSection(
|
||||
$"{SurfaceId}.behavior.{behavior.Kind}.stat_modifier",
|
||||
"Stat Modifier",
|
||||
new[]
|
||||
{
|
||||
new AetheriaRuntimeCargoItemMetric(
|
||||
$"{SurfaceId}.behavior.{behavior.Kind}.target",
|
||||
$"{SplitCamelCase(statReference.Target)}:{SplitCamelCase(statReference.Stat)}",
|
||||
$"{(modifierType == AetheriaRuntimeCargoItemStatModifierType.Constant ? "+" : "x")}{FormatCurrentItemStat(modifier, item, formatValue)}",
|
||||
AetheriaRuntimeDaemonItemStatQueries.ItemStatRef(
|
||||
item.ItemKey,
|
||||
behavior.Kind,
|
||||
behavior.Group,
|
||||
2))
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
var metadata = AetheriaRuntimeBehaviorMetadataCatalog.Get(behavior.Kind);
|
||||
if (metadata == null)
|
||||
continue;
|
||||
|
||||
var fields = metadata.DisplayFields
|
||||
.Select(field => ProjectBehaviorMetric(behavior, field, item, formatValue, formatTemperature))
|
||||
.Where(metric => metric != null)
|
||||
.ToArray();
|
||||
|
||||
if (fields.Length == 0)
|
||||
continue;
|
||||
|
||||
yield return new AetheriaRuntimeCargoItemSection(
|
||||
$"{SurfaceId}.behavior.{behavior.Kind}",
|
||||
FormatTypeName(behavior.Kind),
|
||||
fields);
|
||||
}
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeCargoItemMetric ProjectBehaviorMetric(
|
||||
AetheriaRuntimeBehaviorPayload behavior,
|
||||
AetheriaRuntimeBehaviorFieldMetadata field,
|
||||
AetheriaRuntimeCargoItemObservation item,
|
||||
Func<float, string> formatValue,
|
||||
Func<float, string> formatTemperature)
|
||||
{
|
||||
var payloadField = FindTypedBehaviorField(behavior, field.Key);
|
||||
if (payloadField == null)
|
||||
return null;
|
||||
|
||||
string value;
|
||||
switch (field.ValueKind)
|
||||
{
|
||||
case AetheriaRuntimeBehaviorFieldValueKind.Number:
|
||||
value = FormatValue(payloadField.Value.NumberValue, formatValue);
|
||||
break;
|
||||
case AetheriaRuntimeBehaviorFieldValueKind.Temperature:
|
||||
value = FormatTemperature(payloadField.Value.NumberValue, formatTemperature);
|
||||
break;
|
||||
case AetheriaRuntimeBehaviorFieldValueKind.Integer:
|
||||
value = ((int)payloadField.Value.NumberValue).ToString(CultureInfo.InvariantCulture);
|
||||
break;
|
||||
case AetheriaRuntimeBehaviorFieldValueKind.PerformanceStat:
|
||||
value = FormatCurrentItemStat(payloadField.Value, item, formatValue);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AetheriaRuntimeCargoItemMetric(
|
||||
$"{SurfaceId}.behavior.{behavior.Kind}.{field.Key}",
|
||||
SplitCamelCase(field.Name),
|
||||
value,
|
||||
field.ValueKind == AetheriaRuntimeBehaviorFieldValueKind.PerformanceStat
|
||||
? AetheriaRuntimeDaemonItemStatQueries.ItemStatRef(
|
||||
item.ItemKey,
|
||||
behavior.Kind,
|
||||
behavior.Group,
|
||||
field.Key)
|
||||
: "");
|
||||
}
|
||||
|
||||
private static string FormatCurrentItemStat(
|
||||
AetheriaRuntimePerformanceStat stat,
|
||||
AetheriaRuntimeCargoItemObservation item,
|
||||
Func<float, string> formatValue)
|
||||
{
|
||||
return FormatValue(
|
||||
AetheriaRuntimeDaemonItemStatQueries.EvaluatePerformanceStat(
|
||||
ToBehaviorValue(stat),
|
||||
ToLoadoutItem(item),
|
||||
item?.Temperature ?? 0),
|
||||
formatValue);
|
||||
}
|
||||
|
||||
private static string FormatCurrentItemStat(
|
||||
AetheriaRuntimeBehaviorValue stat,
|
||||
AetheriaRuntimeCargoItemObservation item,
|
||||
Func<float, string> formatValue)
|
||||
{
|
||||
return FormatValue(
|
||||
AetheriaRuntimeDaemonItemStatQueries.EvaluatePerformanceStat(
|
||||
stat,
|
||||
ToLoadoutItem(item),
|
||||
item?.Temperature ?? 0),
|
||||
formatValue);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeLoadoutItemCommit ToLoadoutItem(AetheriaRuntimeCargoItemObservation item)
|
||||
{
|
||||
return AetheriaRuntimeDaemonItemStatQueries.ItemCommit(
|
||||
item?.ItemKey ?? "",
|
||||
item?.Quality ?? 1,
|
||||
item?.Durability ?? 1,
|
||||
enabled: true,
|
||||
overrideShutdown: item != null && item.OverrideShutdown,
|
||||
temperature: item?.Temperature ?? 0);
|
||||
}
|
||||
|
||||
private static double Mass(AetheriaRuntimeCatalogItem typedItem, int quantity)
|
||||
{
|
||||
if (typedItem == null)
|
||||
return 0;
|
||||
|
||||
return quantity > 0
|
||||
? typedItem.Mass * quantity
|
||||
: typedItem.Mass;
|
||||
}
|
||||
|
||||
private static double MaxDurability(
|
||||
AetheriaRuntimeCatalogItem typedItem,
|
||||
AetheriaRuntimeCargoItemObservation item)
|
||||
{
|
||||
if (typedItem != null && typedItem.Durability > 0)
|
||||
return typedItem.Durability;
|
||||
|
||||
return Math.Max(item?.Durability ?? 1, 1);
|
||||
}
|
||||
|
||||
private static string FormatTemperatureRange(
|
||||
AetheriaRuntimeCatalogItem item,
|
||||
Func<float, string> formatTemperature)
|
||||
{
|
||||
if (item.MaximumTemperature > item.MinimumTemperature)
|
||||
{
|
||||
return
|
||||
$"{FormatTemperature(item.MinimumTemperature, formatTemperature)} to " +
|
||||
$"{FormatTemperature(item.MaximumTemperature, formatTemperature)}";
|
||||
}
|
||||
|
||||
return "No typed thermal range";
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorField FindTypedBehaviorField(
|
||||
AetheriaRuntimeBehaviorPayload behavior,
|
||||
int? key)
|
||||
{
|
||||
return key == null
|
||||
? null
|
||||
: behavior?.Fields?.FirstOrDefault(field => field.Key == key.Value);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimePerformanceStat ReadPerformanceStat(AetheriaRuntimeBehaviorValue value)
|
||||
{
|
||||
return new AetheriaRuntimePerformanceStat(
|
||||
ChildNumber(value, 0),
|
||||
ChildNumber(value, 1),
|
||||
ChildNumber(value, 2),
|
||||
ChildNumber(value, 3),
|
||||
ChildNumber(value, 4),
|
||||
ReadStatRecipe(ChildValue(value, 5)));
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeStatRecipe ReadStatRecipe(AetheriaRuntimeBehaviorValue value)
|
||||
{
|
||||
if (value == null || value.Children.Count == 0)
|
||||
return null;
|
||||
|
||||
var modifiers = ChildValue(value, 1)?.Children
|
||||
.Select(ReadStatRecipeModifier)
|
||||
.Where(modifier => modifier != null)
|
||||
.ToArray() ?? Array.Empty<AetheriaRuntimeStatRecipeModifier>();
|
||||
|
||||
return new AetheriaRuntimeStatRecipe(ChildNumber(value, 0), modifiers);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeStatRecipeModifier ReadStatRecipeModifier(AetheriaRuntimeBehaviorValue value)
|
||||
{
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
return new AetheriaRuntimeStatRecipeModifier(
|
||||
ChildString(value, 0),
|
||||
ChildString(value, 1),
|
||||
ChildNumber(value, 2),
|
||||
ReadCurveKeys(ChildValue(value, 3)),
|
||||
value.Children.Count <= 4 || ChildValue(value, 4)?.BoolValue == true);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AetheriaRuntimeCurveKey> ReadCurveKeys(AetheriaRuntimeBehaviorValue value)
|
||||
{
|
||||
if (value?.Children == null || value.Children.Count == 0)
|
||||
return Array.Empty<AetheriaRuntimeCurveKey>();
|
||||
|
||||
return value.Children
|
||||
.Where(key => key.Children.Count >= 4)
|
||||
.Select(key => new AetheriaRuntimeCurveKey(
|
||||
ChildNumber(key, 0),
|
||||
ChildNumber(key, 1),
|
||||
ChildNumber(key, 2),
|
||||
ChildNumber(key, 3)))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeCargoItemStatReference ReadStatReference(AetheriaRuntimeBehaviorValue value)
|
||||
{
|
||||
return new AetheriaRuntimeCargoItemStatReference(
|
||||
ChildString(value, 1),
|
||||
ChildString(value, 2));
|
||||
}
|
||||
|
||||
private static T ReadEnum<T>(AetheriaRuntimeBehaviorValue value, T fallback) where T : struct
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value?.StringValue) && Enum.TryParse(value.StringValue, true, out T parsed))
|
||||
return parsed;
|
||||
|
||||
return value != null && Enum.IsDefined(typeof(T), (int)value.NumberValue)
|
||||
? (T)Enum.ToObject(typeof(T), (int)value.NumberValue)
|
||||
: fallback;
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorValue ToBehaviorValue(AetheriaRuntimePerformanceStat stat)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorValue(
|
||||
"performance-stat",
|
||||
"",
|
||||
0,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
new[]
|
||||
{
|
||||
Number(stat?.Min ?? 0),
|
||||
Number(stat?.Max ?? 0),
|
||||
Number(stat?.HeatExponentMultiplier ?? 0),
|
||||
Number(stat?.DurabilityExponentMultiplier ?? 0),
|
||||
Number(stat?.QualityExponent ?? 0),
|
||||
StatRecipeValue(stat?.Recipe)
|
||||
},
|
||||
EmptyMapEntries());
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorValue StatRecipeValue(AetheriaRuntimeStatRecipe recipe)
|
||||
{
|
||||
if (recipe == null)
|
||||
return EmptyValue("stat-recipe");
|
||||
|
||||
return new AetheriaRuntimeBehaviorValue(
|
||||
"stat-recipe",
|
||||
"",
|
||||
0,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
new[]
|
||||
{
|
||||
Number(recipe.BaseValue),
|
||||
new AetheriaRuntimeBehaviorValue(
|
||||
"stat-recipe-modifiers",
|
||||
"",
|
||||
0,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
(recipe.Modifiers ?? Array.Empty<AetheriaRuntimeStatRecipeModifier>())
|
||||
.Select(StatRecipeModifierValue)
|
||||
.ToArray(),
|
||||
EmptyMapEntries())
|
||||
},
|
||||
EmptyMapEntries());
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorValue StatRecipeModifierValue(AetheriaRuntimeStatRecipeModifier modifier)
|
||||
{
|
||||
if (modifier == null)
|
||||
return EmptyValue("stat-recipe-modifier");
|
||||
|
||||
return new AetheriaRuntimeBehaviorValue(
|
||||
"stat-recipe-modifier",
|
||||
"",
|
||||
0,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
new[]
|
||||
{
|
||||
TextValue(modifier.Condition),
|
||||
TextValue(modifier.Operation),
|
||||
Number(modifier.Amount),
|
||||
CurveValue(modifier.CurveKeys),
|
||||
BoolValue(modifier.Enabled)
|
||||
},
|
||||
EmptyMapEntries());
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorValue CurveValue(IReadOnlyList<AetheriaRuntimeCurveKey> keys)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorValue(
|
||||
"curve",
|
||||
"",
|
||||
0,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
(keys ?? Array.Empty<AetheriaRuntimeCurveKey>())
|
||||
.Select(key => new AetheriaRuntimeBehaviorValue(
|
||||
"curve-key",
|
||||
"",
|
||||
0,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
new[]
|
||||
{
|
||||
Number(key.Time),
|
||||
Number(key.Value),
|
||||
Number(key.InTangent),
|
||||
Number(key.OutTangent)
|
||||
},
|
||||
EmptyMapEntries()))
|
||||
.ToArray(),
|
||||
EmptyMapEntries());
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorValue Number(double value)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorValue("", "", value, false, "", "", Array.Empty<AetheriaRuntimeBehaviorValue>(), EmptyMapEntries());
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorValue TextValue(string value)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorValue("", value ?? "", 0, false, "", "", Array.Empty<AetheriaRuntimeBehaviorValue>(), EmptyMapEntries());
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorValue BoolValue(bool value)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorValue("", "", 0, value, "", "", Array.Empty<AetheriaRuntimeBehaviorValue>(), EmptyMapEntries());
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorValue EmptyValue(string kind)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorValue(kind ?? "", "", 0, false, "", "", Array.Empty<AetheriaRuntimeBehaviorValue>(), EmptyMapEntries());
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AetheriaRuntimeBehaviorMapEntry> EmptyMapEntries()
|
||||
{
|
||||
return Array.Empty<AetheriaRuntimeBehaviorMapEntry>();
|
||||
}
|
||||
|
||||
private static double ChildNumber(AetheriaRuntimeBehaviorValue value, int index)
|
||||
{
|
||||
return value != null && value.Children.Count > index ? value.Children[index].NumberValue : 0;
|
||||
}
|
||||
|
||||
private static string ChildString(AetheriaRuntimeBehaviorValue value, int index)
|
||||
{
|
||||
return value != null && value.Children.Count > index ? value.Children[index].StringValue ?? "" : "";
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorValue ChildValue(AetheriaRuntimeBehaviorValue value, int index)
|
||||
{
|
||||
return value != null && value.Children.Count > index ? value.Children[index] : null;
|
||||
}
|
||||
|
||||
private static string FormatValue(
|
||||
double value,
|
||||
Func<float, string> formatValue)
|
||||
{
|
||||
return formatValue == null
|
||||
? value.ToString("0.###", CultureInfo.InvariantCulture)
|
||||
: formatValue((float)value);
|
||||
}
|
||||
|
||||
private static string FormatTemperature(
|
||||
double value,
|
||||
Func<float, string> formatTemperature)
|
||||
{
|
||||
return formatTemperature == null
|
||||
? value.ToString("0.###", CultureInfo.InvariantCulture)
|
||||
: formatTemperature((float)value);
|
||||
}
|
||||
|
||||
private static string FormatTypeName(string typeName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(typeName))
|
||||
return "";
|
||||
|
||||
return SplitCamelCase(typeName.StartsWith("I", StringComparison.Ordinal) && typeName.Length > 1
|
||||
? typeName.Substring(1)
|
||||
: typeName);
|
||||
}
|
||||
|
||||
private static string SplitCamelCase(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return "";
|
||||
|
||||
return string.Concat(value.Select((character, index) =>
|
||||
index > 0 && char.IsUpper(character) ? " " + character : character.ToString()));
|
||||
}
|
||||
|
||||
private static string SafeId(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return "empty";
|
||||
|
||||
return new string(value
|
||||
.Select(character => char.IsLetterOrDigit(character) ? char.ToLowerInvariant(character) : '-')
|
||||
.ToArray()).Trim('-');
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent Card(
|
||||
string id,
|
||||
string title,
|
||||
params AetheriaRuntimeSurfaceComponent[] children)
|
||||
{
|
||||
return Node(id, "card", new[] { ("title", title ?? "") }, children);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent Metric(string id, string label, string value, string valueRef = "")
|
||||
{
|
||||
var props = new List<(string Key, string Value)>
|
||||
{
|
||||
("label", label ?? ""),
|
||||
("value", value ?? "")
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(valueRef))
|
||||
props.Add(AetheriaRuntimeSurfaceStateRefs.ValueRef(valueRef));
|
||||
|
||||
return Node(id, "metric", props);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent Text(string id, string value)
|
||||
{
|
||||
return Node(id, "text", new[] { ("value", value ?? "") });
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent Button(string id, string label, string command)
|
||||
{
|
||||
return Node(id, "control.button", new[] { ("label", label ?? ""), ("command", command ?? "") });
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent ButtonRow(
|
||||
string id,
|
||||
params AetheriaRuntimeSurfaceComponent[] children)
|
||||
{
|
||||
return Node(id, "row", Array.Empty<(string Key, string Value)>(), children);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent Node(
|
||||
string id,
|
||||
string kind,
|
||||
IEnumerable<(string Key, string Value)> props,
|
||||
params AetheriaRuntimeSurfaceComponent[] children)
|
||||
{
|
||||
return new AetheriaRuntimeSurfaceComponent(
|
||||
id,
|
||||
kind,
|
||||
props.ToDictionary(prop => prop.Key, prop => prop.Value ?? "", StringComparer.Ordinal),
|
||||
children ?? Array.Empty<AetheriaRuntimeSurfaceComponent>());
|
||||
}
|
||||
}
|
||||
|
||||
public enum AetheriaRuntimeCargoItemDetailsCommandKind
|
||||
{
|
||||
Unknown = 0,
|
||||
Close = 1
|
||||
}
|
||||
|
||||
internal enum AetheriaRuntimeCargoItemStatModifierType
|
||||
{
|
||||
Constant = 0,
|
||||
Multiplier = 1
|
||||
}
|
||||
|
||||
internal readonly struct AetheriaRuntimeCargoItemStatReference
|
||||
{
|
||||
public AetheriaRuntimeCargoItemStatReference(string target, string stat)
|
||||
{
|
||||
Target = target ?? "";
|
||||
Stat = stat ?? "";
|
||||
}
|
||||
|
||||
public string Target { get; }
|
||||
public string Stat { get; }
|
||||
}
|
||||
|
||||
public readonly struct AetheriaRuntimeCargoItemDetailsCommand
|
||||
{
|
||||
public AetheriaRuntimeCargoItemDetailsCommand(AetheriaRuntimeCargoItemDetailsCommandKind kind)
|
||||
{
|
||||
Kind = kind;
|
||||
}
|
||||
|
||||
public AetheriaRuntimeCargoItemDetailsCommandKind Kind { get; }
|
||||
}
|
||||
|
||||
public static class AetheriaRuntimeCargoItemDetailsSurfaceCommands
|
||||
{
|
||||
public static bool TryRead(
|
||||
EveSurfaceCommandRequest request,
|
||||
out AetheriaRuntimeCargoItemDetailsCommand command)
|
||||
{
|
||||
command = default;
|
||||
if (request == null ||
|
||||
!string.Equals(request.SurfaceId, AetheriaRuntimeCargoItemDetailsSurfaceBuilder.SurfaceId, StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
if (string.Equals(request.Operation?.OperationId, AetheriaRuntimeCargoItemDetailsSurfaceBuilder.Close, StringComparison.Ordinal))
|
||||
{
|
||||
command = new AetheriaRuntimeCargoItemDetailsCommand(AetheriaRuntimeCargoItemDetailsCommandKind.Close);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e9160e97be310004cb18268ccd436a3e
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public static class AetheriaRuntimeCargoTransactions
|
||||
{
|
||||
public static bool TryRemoveQuantity(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
int cargoIndex,
|
||||
string itemKey,
|
||||
int x,
|
||||
int y,
|
||||
int quantity,
|
||||
out AetheriaRuntimeLoadoutItemSlotCommit slot)
|
||||
{
|
||||
slot = null!;
|
||||
var cargoContents = (entity?.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>()).ToArray();
|
||||
if (cargoIndex < 0 || cargoIndex >= cargoContents.Length || cargoContents[cargoIndex] == null)
|
||||
return false;
|
||||
var items = (cargoContents[cargoIndex].Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>()).ToList();
|
||||
var itemIndex = items.FindIndex(candidate => candidate?.Item != null &&
|
||||
string.Equals(candidate.Item.ItemKey, itemKey, StringComparison.Ordinal) &&
|
||||
candidate.X == x && candidate.Y == y);
|
||||
if (itemIndex < 0) return false;
|
||||
var source = items[itemIndex];
|
||||
var sourceQuantity = Math.Max(1, source.Item.Quantity);
|
||||
if (quantity <= 0 || quantity > sourceQuantity) return false;
|
||||
slot = Clone(source, quantity);
|
||||
if (quantity == sourceQuantity) items.RemoveAt(itemIndex);
|
||||
else source.Item.Quantity = sourceQuantity - quantity;
|
||||
cargoContents[cargoIndex] = new AetheriaRuntimeCargoBayLoadoutCommit { Items = items.ToArray() };
|
||||
entity!.CargoContents = cargoContents;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryFind(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
string itemKey,
|
||||
out int cargoIndex,
|
||||
out int x,
|
||||
out int y)
|
||||
{
|
||||
var cargo = entity?.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>();
|
||||
for (var bay = 0; bay < cargo.Count; bay++)
|
||||
foreach (var slot in cargo[bay]?.Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
||||
{
|
||||
if (slot?.Item == null || !string.Equals(slot.Item.ItemKey, itemKey, StringComparison.Ordinal)) continue;
|
||||
cargoIndex = bay; x = slot.X; y = slot.Y; return true;
|
||||
}
|
||||
cargoIndex = x = y = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeLoadoutItemSlotCommit Clone(AetheriaRuntimeLoadoutItemSlotCommit source, int quantity) => new()
|
||||
{
|
||||
X = source.X, Y = source.Y, Rotation = source.Rotation ?? "None",
|
||||
Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{
|
||||
ItemKey = source.Item.ItemKey, Quality = source.Item.Quality, Durability = source.Item.Durability,
|
||||
Quantity = quantity, Enabled = source.Item.Enabled, OverrideShutdown = source.Item.OverrideShutdown,
|
||||
Temperature = source.Item.Temperature
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: aeffd152980489d42a76727ed45cadcb
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public static class AetheriaRuntimeCatalogCommands
|
||||
{
|
||||
public const string SurfaceId = "aetheria.catalog.operator";
|
||||
public const string Refresh = "aetheria.catalog.refresh";
|
||||
|
||||
public static bool IsKnown(string command)
|
||||
{
|
||||
return command == Refresh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b149e8e695183924fb74e86cdb871be4
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 521f121b64367a548837968dda720594
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4ae4a16968aa48e4a9244a3bfec4bd16
|
||||
|
|
@ -1,218 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using GameCult.Eve.Surface;
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public static class AetheriaRuntimeClientTargetCommands
|
||||
{
|
||||
public const string SurfaceId = "aetheria.client_target";
|
||||
public const string Refresh = "aetheria.client_target.refresh";
|
||||
public const string CycleTargetKind = "aetheria.client_target.kind.cycle";
|
||||
public const string SetTitle = "aetheria.client_target.title.set";
|
||||
public const string SetVerseId = "aetheria.client_target.verse_id.set";
|
||||
public const string SetRuntimeId = "aetheria.client_target.runtime_id.set";
|
||||
public const string SetCultMeshAddress = "aetheria.client_target.cultmesh_address.set";
|
||||
public const string SetStateFilePath = "aetheria.client_target.state_file_path.set";
|
||||
public const string SetDiscoveryEndpoints = "aetheria.client_target.discovery_endpoints.set";
|
||||
public const string DiscoverVerses = "aetheria.client_target.discovery.refresh";
|
||||
public const string SelectDiscoveredVerse = "aetheria.client_target.discovery.select";
|
||||
public const string SyncReplica = "aetheria.client_target.replica.sync";
|
||||
|
||||
public static bool IsKnown(string command)
|
||||
{
|
||||
return command == Refresh ||
|
||||
command == CycleTargetKind ||
|
||||
command == SetTitle ||
|
||||
command == SetVerseId ||
|
||||
command == SetRuntimeId ||
|
||||
command == SetCultMeshAddress ||
|
||||
command == SetStateFilePath ||
|
||||
command == SetDiscoveryEndpoints ||
|
||||
command == DiscoverVerses ||
|
||||
command == SelectDiscoveredVerse ||
|
||||
command == SyncReplica;
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_5_3_OR_NEWER
|
||||
public static class AetheriaRuntimeClientTargetSurfaceCommands
|
||||
{
|
||||
public static bool TryRequest(
|
||||
AetheriaClientTarget target,
|
||||
EveSurfaceCommandRequest request,
|
||||
out AetheriaRuntimeClientTargetDocument? document)
|
||||
{
|
||||
document = null;
|
||||
if (!TryRead(request, out var operation))
|
||||
return false;
|
||||
|
||||
document = Request(target, operation);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryRead(
|
||||
EveSurfaceCommandRequest request,
|
||||
out AetheriaClientTargetOperation operation)
|
||||
{
|
||||
operation = default;
|
||||
if (request == null)
|
||||
return false;
|
||||
|
||||
var command = request.Operation?.OperationId ?? "";
|
||||
switch (command)
|
||||
{
|
||||
case AetheriaRuntimeClientTargetCommands.Refresh:
|
||||
operation = new AetheriaClientTargetOperation(AetheriaClientTargetOperationKind.Refresh);
|
||||
return true;
|
||||
case AetheriaRuntimeClientTargetCommands.CycleTargetKind:
|
||||
operation = new AetheriaClientTargetOperation(AetheriaClientTargetOperationKind.CycleTransport);
|
||||
return true;
|
||||
case AetheriaRuntimeClientTargetCommands.SetTitle:
|
||||
operation = new AetheriaClientTargetOperation(
|
||||
AetheriaClientTargetOperationKind.SetTitle,
|
||||
value: ReadPayloadValue(request, "value"));
|
||||
return true;
|
||||
case AetheriaRuntimeClientTargetCommands.SetVerseId:
|
||||
operation = new AetheriaClientTargetOperation(
|
||||
AetheriaClientTargetOperationKind.SetVerseId,
|
||||
value: ReadPayloadValue(request, "value"));
|
||||
return true;
|
||||
case AetheriaRuntimeClientTargetCommands.SetRuntimeId:
|
||||
operation = new AetheriaClientTargetOperation(
|
||||
AetheriaClientTargetOperationKind.SetRuntimeId,
|
||||
value: ReadPayloadValue(request, "value"));
|
||||
return true;
|
||||
case AetheriaRuntimeClientTargetCommands.SetCultMeshAddress:
|
||||
operation = new AetheriaClientTargetOperation(
|
||||
AetheriaClientTargetOperationKind.SetCultMeshAddress,
|
||||
value: ReadPayloadValue(request, "value"));
|
||||
return true;
|
||||
case AetheriaRuntimeClientTargetCommands.SetStateFilePath:
|
||||
operation = new AetheriaClientTargetOperation(
|
||||
AetheriaClientTargetOperationKind.SetStateFilePath,
|
||||
value: ReadPayloadValue(request, "value"));
|
||||
return true;
|
||||
case AetheriaRuntimeClientTargetCommands.SetDiscoveryEndpoints:
|
||||
operation = new AetheriaClientTargetOperation(
|
||||
AetheriaClientTargetOperationKind.SetDiscoveryEndpoints,
|
||||
discoveryEndpoints: ParseDiscoveryEndpoints(ReadPayloadValue(request, "value")));
|
||||
return true;
|
||||
case AetheriaRuntimeClientTargetCommands.DiscoverVerses:
|
||||
operation = new AetheriaClientTargetOperation(AetheriaClientTargetOperationKind.DiscoverVerses);
|
||||
return true;
|
||||
case AetheriaRuntimeClientTargetCommands.SelectDiscoveredVerse:
|
||||
operation = new AetheriaClientTargetOperation(
|
||||
AetheriaClientTargetOperationKind.SelectDiscoveredVerse,
|
||||
verseId: ReadPayloadValue(request, "verseId"),
|
||||
title: ReadPayloadValue(request, "title"),
|
||||
cultMeshAddress: ReadPayloadValue(request, "cultMeshAddress"),
|
||||
discoveryEndpoints: ParseDiscoveryEndpoints(ReadPayloadValue(request, "discoveryEndpoints")));
|
||||
return true;
|
||||
case AetheriaRuntimeClientTargetCommands.SyncReplica:
|
||||
operation = new AetheriaClientTargetOperation(AetheriaClientTargetOperationKind.SyncReplica);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeClientTargetDocument Request(
|
||||
AetheriaClientTarget target,
|
||||
AetheriaClientTargetOperation operation)
|
||||
{
|
||||
switch (operation.Kind)
|
||||
{
|
||||
case AetheriaClientTargetOperationKind.Refresh:
|
||||
return target.Refresh();
|
||||
case AetheriaClientTargetOperationKind.CycleTransport:
|
||||
return target.CycleTransport();
|
||||
case AetheriaClientTargetOperationKind.SetTitle:
|
||||
return target.RequestTitle(operation.Value);
|
||||
case AetheriaClientTargetOperationKind.SetVerseId:
|
||||
return target.RequestVerseId(operation.Value);
|
||||
case AetheriaClientTargetOperationKind.SetRuntimeId:
|
||||
return target.RequestRuntimeId(operation.Value);
|
||||
case AetheriaClientTargetOperationKind.SetCultMeshAddress:
|
||||
return target.RequestCultMeshAddress(operation.Value);
|
||||
case AetheriaClientTargetOperationKind.SetStateFilePath:
|
||||
return target.RequestStateFilePath(operation.Value);
|
||||
case AetheriaClientTargetOperationKind.SetDiscoveryEndpoints:
|
||||
return target.RequestDiscoveryEndpoints(operation.DiscoveryEndpoints);
|
||||
case AetheriaClientTargetOperationKind.DiscoverVerses:
|
||||
return target.DiscoverVerses();
|
||||
case AetheriaClientTargetOperationKind.SelectDiscoveredVerse:
|
||||
return target.SelectDiscoveredVerse(
|
||||
operation.VerseId,
|
||||
operation.Title,
|
||||
operation.CultMeshAddress,
|
||||
operation.DiscoveryEndpoints);
|
||||
case AetheriaClientTargetOperationKind.SyncReplica:
|
||||
return target.SyncReplica();
|
||||
default:
|
||||
throw new ArgumentException(
|
||||
$"Unknown Aetheria client-target operation '{operation.Kind}'.",
|
||||
nameof(operation));
|
||||
}
|
||||
}
|
||||
|
||||
private static string ReadPayloadValue(EveSurfaceCommandRequest request, string key)
|
||||
{
|
||||
return request.Payload.GetString(key);
|
||||
}
|
||||
|
||||
private static string[] ParseDiscoveryEndpoints(string value)
|
||||
{
|
||||
return (value ?? "")
|
||||
.Split(new[] { ',', ';', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(endpoint => endpoint.Trim())
|
||||
.Where(endpoint => !string.IsNullOrWhiteSpace(endpoint))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private enum AetheriaClientTargetOperationKind
|
||||
{
|
||||
Unknown = 0,
|
||||
Refresh = 1,
|
||||
CycleTransport = 2,
|
||||
SetTitle = 3,
|
||||
SetVerseId = 4,
|
||||
SetRuntimeId = 5,
|
||||
SetCultMeshAddress = 6,
|
||||
SetStateFilePath = 7,
|
||||
SetDiscoveryEndpoints = 8,
|
||||
DiscoverVerses = 9,
|
||||
SelectDiscoveredVerse = 10,
|
||||
SyncReplica = 11
|
||||
}
|
||||
|
||||
private readonly struct AetheriaClientTargetOperation
|
||||
{
|
||||
public AetheriaClientTargetOperation(
|
||||
AetheriaClientTargetOperationKind kind,
|
||||
string value = "",
|
||||
string verseId = "",
|
||||
string title = "",
|
||||
string cultMeshAddress = "",
|
||||
IReadOnlyList<string>? discoveryEndpoints = null)
|
||||
{
|
||||
Kind = kind;
|
||||
Value = value ?? "";
|
||||
VerseId = verseId ?? "";
|
||||
Title = title ?? "";
|
||||
CultMeshAddress = cultMeshAddress ?? "";
|
||||
DiscoveryEndpoints = discoveryEndpoints?.ToArray() ?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
public AetheriaClientTargetOperationKind Kind { get; }
|
||||
public string Value { get; }
|
||||
public string VerseId { get; }
|
||||
public string Title { get; }
|
||||
public string CultMeshAddress { get; }
|
||||
public IReadOnlyList<string> DiscoveryEndpoints { get; }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6bd3516801c897b40b02efef67e17fe9
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Mesh;
|
||||
using MessagePack;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public static class AetheriaRuntimeClientTargetKinds
|
||||
{
|
||||
public const string StateFile = "state-file";
|
||||
public const string CultMeshVerse = "cultmesh-verse";
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaRuntimeClientTargetDocument
|
||||
{
|
||||
public const string SchemaId = "gamecult.aetheria.runtime_client_target.v1";
|
||||
public const string SchemaName = "aetheria.runtime_client_target";
|
||||
public const string SchemaVersion = "aetheria.runtime_client_target.v1";
|
||||
public const string DocumentKey = "local:aetheria.runtime_client_target.v1";
|
||||
|
||||
[Key(0)] public string Schema { get; set; } = SchemaId;
|
||||
[Key(1)] public string TargetKind { get; set; } = AetheriaRuntimeClientTargetKinds.StateFile;
|
||||
[Key(2)] public string Title { get; set; } = "Local Aetheria";
|
||||
[Key(3)] public string VerseId { get; set; } = "aetheria.local";
|
||||
[Key(4)] public string CultMeshAddress { get; set; } = "asgard.local.aetheria/eve";
|
||||
[Key(5)] public string StateFilePath { get; set; } = "";
|
||||
[Key(6)] public string UpdatedAtUtc { get; set; } = "";
|
||||
[Key(7)] public string[] DiscoveryEndpoints { get; set; } = Array.Empty<string>();
|
||||
[Key(8)] public AetheriaRuntimeDiscoveredVerse[] DiscoveredVerses { get; set; } = Array.Empty<AetheriaRuntimeDiscoveredVerse>();
|
||||
[Key(9)] public string LastDiscoveryAtUtc { get; set; } = "";
|
||||
[Key(10)] public string LastDiscoveryError { get; set; } = "";
|
||||
[Key(11)] public string ReplicaStateFilePath { get; set; } = "";
|
||||
[Key(12)] public string LastReplicaSyncAtUtc { get; set; } = "";
|
||||
[Key(13)] public string LastReplicaSyncError { get; set; } = "";
|
||||
[Key(14)] public string RuntimeId { get; set; } = AetheriaRuntimeStateBoundary.DefaultClientRuntimeId;
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaRuntimeDiscoveredVerse
|
||||
{
|
||||
[Key(0)] public string VerseId { get; set; } = "";
|
||||
[Key(1)] public string DisplayName { get; set; } = "";
|
||||
[Key(2)] public string AuthorityModel { get; set; } = "";
|
||||
[Key(3)] public string TransportVersion { get; set; } = "";
|
||||
[Key(4)] public string RulesHash { get; set; } = "";
|
||||
[Key(5)] public string Description { get; set; } = "";
|
||||
[Key(6)] public string[] DiscoveryEndpoints { get; set; } = Array.Empty<string>();
|
||||
[Key(7)] public string[] AuthorityRuntimeIds { get; set; } = Array.Empty<string>();
|
||||
[Key(8)] public string ParentVerseId { get; set; } = "";
|
||||
}
|
||||
|
||||
public static class AetheriaRuntimeClientTargetStore
|
||||
{
|
||||
public static AetheriaRuntimeClientTargetDocument ReadOrInitialize(
|
||||
string clientTargetPath,
|
||||
string defaultStateFilePath)
|
||||
{
|
||||
if (File.Exists(clientTargetPath))
|
||||
return Read(clientTargetPath);
|
||||
|
||||
var document = CreateDefault(defaultStateFilePath);
|
||||
Write(clientTargetPath, document);
|
||||
return document;
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeClientTargetDocument CreateDefault(string defaultStateFilePath)
|
||||
{
|
||||
var gameDataDirectory = ResolveGameDataDirectory(defaultStateFilePath);
|
||||
return new AetheriaRuntimeClientTargetDocument
|
||||
{
|
||||
Schema = AetheriaRuntimeClientTargetDocument.SchemaId,
|
||||
TargetKind = AetheriaRuntimeClientTargetKinds.StateFile,
|
||||
Title = "Local Aetheria",
|
||||
VerseId = "aetheria.local",
|
||||
CultMeshAddress = "asgard.local.aetheria/eve",
|
||||
RuntimeId = AetheriaRuntimeStateBoundary.DefaultClientRuntimeId,
|
||||
StateFilePath = defaultStateFilePath ?? "",
|
||||
ReplicaStateFilePath = AetheriaRuntimeStateBoundary.GetReplicaStateFilePath(gameDataDirectory, "aetheria.local"),
|
||||
UpdatedAtUtc = DateTime.UtcNow.ToString("O")
|
||||
};
|
||||
}
|
||||
|
||||
public static void Write(string clientTargetPath, AetheriaRuntimeClientTargetDocument document)
|
||||
{
|
||||
if (document == null) throw new ArgumentNullException(nameof(document));
|
||||
|
||||
document.Schema = AetheriaRuntimeClientTargetDocument.SchemaId;
|
||||
document.DiscoveryEndpoints ??= Array.Empty<string>();
|
||||
document.DiscoveredVerses ??= Array.Empty<AetheriaRuntimeDiscoveredVerse>();
|
||||
document.LastDiscoveryAtUtc ??= "";
|
||||
document.LastDiscoveryError ??= "";
|
||||
document.ReplicaStateFilePath = NormalizeReplicaStateFilePath(document, clientTargetPath, document.ReplicaStateFilePath);
|
||||
document.LastReplicaSyncAtUtc ??= "";
|
||||
document.LastReplicaSyncError ??= "";
|
||||
if (string.IsNullOrWhiteSpace(document.RuntimeId))
|
||||
document.RuntimeId = AetheriaRuntimeStateBoundary.DefaultClientRuntimeId;
|
||||
foreach (var verse in document.DiscoveredVerses)
|
||||
{
|
||||
if (verse == null)
|
||||
continue;
|
||||
|
||||
verse.VerseId ??= "";
|
||||
verse.DisplayName ??= "";
|
||||
verse.AuthorityModel ??= "";
|
||||
verse.TransportVersion ??= "";
|
||||
verse.RulesHash ??= "";
|
||||
verse.Description ??= "";
|
||||
verse.DiscoveryEndpoints ??= Array.Empty<string>();
|
||||
verse.AuthorityRuntimeIds ??= Array.Empty<string>();
|
||||
verse.ParentVerseId ??= "";
|
||||
}
|
||||
|
||||
document.UpdatedAtUtc = string.IsNullOrWhiteSpace(document.UpdatedAtUtc)
|
||||
? DateTime.UtcNow.ToString("O")
|
||||
: document.UpdatedAtUtc;
|
||||
|
||||
CultMesh.WriteSingleFileDocumentPayload(
|
||||
clientTargetPath,
|
||||
new CultRecordKey(AetheriaRuntimeClientTargetDocument.DocumentKey),
|
||||
new CultMeshSingleFileDocumentSchema(
|
||||
AetheriaRuntimeClientTargetDocument.SchemaId,
|
||||
AetheriaRuntimeClientTargetDocument.SchemaName,
|
||||
AetheriaRuntimeClientTargetDocument.SchemaVersion),
|
||||
document.UpdatedAtUtc,
|
||||
MessagePackSerializer.Serialize(document));
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeClientTargetDocument Update(
|
||||
string clientTargetPath,
|
||||
string defaultStateFilePath,
|
||||
Action<AetheriaRuntimeClientTargetDocument> mutate)
|
||||
{
|
||||
if (mutate == null) throw new ArgumentNullException(nameof(mutate));
|
||||
|
||||
var document = ReadOrInitialize(clientTargetPath, defaultStateFilePath);
|
||||
mutate(document);
|
||||
document.ReplicaStateFilePath = NormalizeReplicaStateFilePath(document, clientTargetPath, document.ReplicaStateFilePath);
|
||||
Write(clientTargetPath, document);
|
||||
return document;
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeClientTargetDocument Read(string clientTargetPath)
|
||||
{
|
||||
return MessagePackSerializer.Deserialize<AetheriaRuntimeClientTargetDocument>(
|
||||
CultMesh.ReadSingleFileDocumentPayload(
|
||||
clientTargetPath,
|
||||
new CultRecordKey(AetheriaRuntimeClientTargetDocument.DocumentKey),
|
||||
AetheriaRuntimeClientTargetDocument.SchemaId));
|
||||
}
|
||||
|
||||
private static string NormalizeReplicaStateFilePath(
|
||||
AetheriaRuntimeClientTargetDocument document,
|
||||
string clientTargetPath,
|
||||
string configuredPath)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(configuredPath))
|
||||
return Path.GetFullPath(configuredPath);
|
||||
|
||||
var gameDataDirectory = ResolveGameDataDirectoryFromClientTarget(clientTargetPath);
|
||||
return AetheriaRuntimeStateBoundary.GetReplicaStateFilePath(gameDataDirectory, document?.VerseId ?? "");
|
||||
}
|
||||
|
||||
private static DirectoryInfo ResolveGameDataDirectory(string defaultStateFilePath)
|
||||
{
|
||||
var fullPath = Path.GetFullPath(string.IsNullOrWhiteSpace(defaultStateFilePath) ? "." : defaultStateFilePath);
|
||||
var directory = Path.GetDirectoryName(fullPath);
|
||||
return new DirectoryInfo(string.IsNullOrWhiteSpace(directory) ? "." : directory);
|
||||
}
|
||||
|
||||
private static DirectoryInfo ResolveGameDataDirectoryFromClientTarget(string clientTargetPath)
|
||||
{
|
||||
var fullPath = Path.GetFullPath(string.IsNullOrWhiteSpace(clientTargetPath) ? "." : clientTargetPath);
|
||||
var directory = Path.GetDirectoryName(fullPath);
|
||||
return new DirectoryInfo(string.IsNullOrWhiteSpace(directory) ? "." : directory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3aca91844413c7d46afaf6168a04ca8d
|
||||
|
|
@ -1,365 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public static class AetheriaRuntimeClientTargetSurfaceBuilder
|
||||
{
|
||||
public static AetheriaRuntimeSurfaceDocument Build(
|
||||
AetheriaRuntimeStateBootReport stateBoot,
|
||||
AetheriaRuntimeVerseHostSettingsDocument verseHost,
|
||||
string updatedAtUtc,
|
||||
long version = 1)
|
||||
{
|
||||
var targetKind = stateBoot.TargetKind ?? AetheriaRuntimeClientTargetKinds.StateFile;
|
||||
var targetTitle = stateBoot.Title ?? "";
|
||||
var targetVerseId = stateBoot.VerseId ?? "";
|
||||
var targetRuntimeId = string.IsNullOrWhiteSpace(stateBoot.RuntimeId) ? AetheriaRuntimeStateBoundary.DefaultClientRuntimeId : stateBoot.RuntimeId;
|
||||
var targetCultMeshAddress = stateBoot.CultMeshAddress ?? "";
|
||||
var targetStateFilePath = stateBoot.StateFilePath ?? "";
|
||||
var targetReplicaStateFilePath = stateBoot.ReplicaStateFilePath ?? "";
|
||||
var discoveryEndpointsText = string.Join(", ", stateBoot.DiscoveryEndpoints ?? Array.Empty<string>());
|
||||
var discoveredVerses = stateBoot.DiscoveredVerses ?? Array.Empty<AetheriaRuntimeDiscoveredVerse>();
|
||||
var targetSource = stateBoot.TargetSource ?? "";
|
||||
var bootFailureMessage = stateBoot.FailureMessage ?? "";
|
||||
var hostTitle = verseHost?.Title ?? targetTitle;
|
||||
var hostVerseId = verseHost?.VerseId ?? targetVerseId;
|
||||
var hostVisibility = verseHost?.Visibility ?? "unknown";
|
||||
var hostCultMeshAddress = verseHost?.CultMeshAddress ?? targetCultMeshAddress;
|
||||
updatedAtUtc ??= "";
|
||||
|
||||
var targetKindLabel = string.Equals(targetKind, AetheriaRuntimeClientTargetKinds.CultMeshVerse, StringComparison.Ordinal)
|
||||
? "CultMesh Verse"
|
||||
: "Local State File";
|
||||
var visibilityLabel = string.IsNullOrWhiteSpace(hostVisibility) ? "unknown" : hostVisibility;
|
||||
var visibilityActionLabel = string.Equals(visibilityLabel, "public", StringComparison.OrdinalIgnoreCase)
|
||||
? "Make Private"
|
||||
: "Make Public";
|
||||
var lastDiscoveryLabel = string.IsNullOrWhiteSpace(stateBoot.LastDiscoveryAtUtc) ? "never" : stateBoot.LastDiscoveryAtUtc;
|
||||
var lastReplicaSyncLabel = string.IsNullOrWhiteSpace(stateBoot.LastReplicaSyncAtUtc) ? "never" : stateBoot.LastReplicaSyncAtUtc;
|
||||
var summaryNote = !stateBoot.SupportsLocalStateFileRead && !string.IsNullOrWhiteSpace(bootFailureMessage)
|
||||
? bootFailureMessage
|
||||
: string.Equals(targetSource, "state-path-override", StringComparison.Ordinal)
|
||||
? "AETHERIA_STATE_PATH is overriding the persisted client target. Update the environment if you want boot to follow the saved target again."
|
||||
: "Client target edits persist in aetheria-client.cc. Verse discovery and selection mutate the same typed owner. Verse visibility changes append provider-owned Eve requests for the daemon bridge. Remote Verse targets hydrate a cache-only local replica before observers read them.";
|
||||
|
||||
var discoveryChildren = new List<AetheriaRuntimeSurfaceComponent>
|
||||
{
|
||||
TextInput(
|
||||
"aetheria.clientTarget.discovery.endpoints",
|
||||
"Discovery Endpoints",
|
||||
discoveryEndpointsText,
|
||||
AetheriaRuntimeClientTargetCommands.SetDiscoveryEndpoints),
|
||||
Metric("aetheria.clientTarget.discovery.lastScan", "Last Scan", lastDiscoveryLabel)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(stateBoot.LastDiscoveryError))
|
||||
{
|
||||
discoveryChildren.Add(Text(
|
||||
"aetheria.clientTarget.discovery.error",
|
||||
stateBoot.LastDiscoveryError));
|
||||
}
|
||||
|
||||
if (discoveredVerses.Length == 0)
|
||||
{
|
||||
discoveryChildren.Add(Text(
|
||||
"aetheria.clientTarget.discovery.empty",
|
||||
string.IsNullOrWhiteSpace(discoveryEndpointsText)
|
||||
? "Add one or more cultnet:// discovery endpoints to scan for public or federated Aetheria Verses."
|
||||
: "No Verse descriptors are cached yet for the configured discovery endpoints."));
|
||||
}
|
||||
else
|
||||
{
|
||||
discoveryChildren.Add(Metric(
|
||||
"aetheria.clientTarget.discovery.count",
|
||||
"Known Verses",
|
||||
discoveredVerses.Length.ToString()));
|
||||
discoveryChildren.Add(Row(
|
||||
"aetheria.clientTarget.discovery.list",
|
||||
discoveredVerses
|
||||
.Select((verse, index) => BuildDiscoveredVerseNode(targetKind, targetVerseId, verse, index))
|
||||
.ToArray()));
|
||||
}
|
||||
|
||||
discoveryChildren.Add(ButtonRow(
|
||||
"aetheria.clientTarget.discovery.actions",
|
||||
Button(
|
||||
"aetheria.clientTarget.discovery.scan",
|
||||
"Discover",
|
||||
AetheriaRuntimeClientTargetCommands.DiscoverVerses),
|
||||
Button(
|
||||
"aetheria.clientTarget.discovery.refresh",
|
||||
"Refresh",
|
||||
AetheriaRuntimeClientTargetCommands.Refresh)));
|
||||
|
||||
return new AetheriaRuntimeSurfaceDocument(
|
||||
providerId: "aetheria",
|
||||
providerKind: "game.menu",
|
||||
title: "Aetheria Verse Settings",
|
||||
version: version,
|
||||
updatedAtUtc: updatedAtUtc,
|
||||
surface: new AetheriaRuntimeSurfaceTree(
|
||||
AetheriaRuntimeClientTargetCommands.SurfaceId,
|
||||
Node(
|
||||
"aetheria.clientTarget.root",
|
||||
"surface",
|
||||
Array.Empty<(string Key, string Value)>(),
|
||||
Node(
|
||||
"aetheria.clientTarget.summary",
|
||||
"card",
|
||||
new[] { ("title", "Client Target") },
|
||||
Metric("aetheria.clientTarget.summary.target", "Target", stateBoot.TargetLabel),
|
||||
Metric("aetheria.clientTarget.summary.transport", "Transport", targetKindLabel),
|
||||
Metric("aetheria.clientTarget.summary.source", "Target Source", targetSource),
|
||||
Text("aetheria.clientTarget.summary.note", summaryNote)),
|
||||
Node(
|
||||
"aetheria.clientTarget.target",
|
||||
"card",
|
||||
new[] { ("title", "Target Fields") },
|
||||
TextInput(
|
||||
"aetheria.clientTarget.target.title",
|
||||
"Title",
|
||||
targetTitle,
|
||||
AetheriaRuntimeClientTargetCommands.SetTitle),
|
||||
TextInput(
|
||||
"aetheria.clientTarget.target.verseId",
|
||||
"Verse Id",
|
||||
targetVerseId,
|
||||
AetheriaRuntimeClientTargetCommands.SetVerseId),
|
||||
TextInput(
|
||||
"aetheria.clientTarget.target.runtimeId",
|
||||
"Runtime Id",
|
||||
targetRuntimeId,
|
||||
AetheriaRuntimeClientTargetCommands.SetRuntimeId),
|
||||
TextInput(
|
||||
"aetheria.clientTarget.target.cultMeshAddress",
|
||||
"CultMesh Address",
|
||||
targetCultMeshAddress,
|
||||
AetheriaRuntimeClientTargetCommands.SetCultMeshAddress),
|
||||
TextInput(
|
||||
"aetheria.clientTarget.target.stateFilePath",
|
||||
"State File Path",
|
||||
targetStateFilePath,
|
||||
AetheriaRuntimeClientTargetCommands.SetStateFilePath),
|
||||
Metric(
|
||||
"aetheria.clientTarget.target.replicaStateFilePath",
|
||||
"Replica State File",
|
||||
targetReplicaStateFilePath),
|
||||
Metric(
|
||||
"aetheria.clientTarget.target.replicaSyncAt",
|
||||
"Replica Sync",
|
||||
lastReplicaSyncLabel),
|
||||
Text(
|
||||
"aetheria.clientTarget.target.replicaSyncError",
|
||||
stateBoot.LastReplicaSyncError),
|
||||
ButtonRow(
|
||||
"aetheria.clientTarget.target.actions",
|
||||
Button(
|
||||
"aetheria.clientTarget.target.cycleTransport",
|
||||
string.Equals(targetKind, AetheriaRuntimeClientTargetKinds.CultMeshVerse, StringComparison.Ordinal)
|
||||
? "Use Local State File"
|
||||
: "Use CultMesh Verse",
|
||||
AetheriaRuntimeClientTargetCommands.CycleTargetKind),
|
||||
Button(
|
||||
"aetheria.clientTarget.target.syncReplica",
|
||||
"Sync Replica",
|
||||
AetheriaRuntimeClientTargetCommands.SyncReplica),
|
||||
Button(
|
||||
"aetheria.clientTarget.target.refresh",
|
||||
"Refresh",
|
||||
AetheriaRuntimeClientTargetCommands.Refresh))),
|
||||
Node(
|
||||
"aetheria.clientTarget.discovery",
|
||||
"card",
|
||||
new[] { ("title", "Verse Discovery") },
|
||||
discoveryChildren.ToArray()),
|
||||
Node(
|
||||
"aetheria.clientTarget.host",
|
||||
"card",
|
||||
new[] { ("title", "Daemon Verse Host") },
|
||||
Metric("aetheria.clientTarget.host.verse", "Verse", BuildVerseLabel(hostTitle, hostVerseId)),
|
||||
Metric("aetheria.clientTarget.host.visibility", "Visibility", visibilityLabel),
|
||||
Metric("aetheria.clientTarget.host.cultMesh", "CultMesh", hostCultMeshAddress),
|
||||
ButtonRow(
|
||||
"aetheria.clientTarget.host.actions",
|
||||
Button(
|
||||
"aetheria.clientTarget.host.toggleVisibility",
|
||||
visibilityActionLabel,
|
||||
AetheriaRuntimeVerseHostCommands.CycleVisibility),
|
||||
Button(
|
||||
"aetheria.clientTarget.host.refresh",
|
||||
"Refresh Host",
|
||||
AetheriaRuntimeVerseHostCommands.Refresh))),
|
||||
Node(
|
||||
"aetheria.clientTarget.host.noteCard",
|
||||
"card",
|
||||
new[] { ("title", "Ownership") },
|
||||
Text(
|
||||
"aetheria.clientTarget.host.note",
|
||||
"Client target edits persist through the managed target document. Visibility changes append provider-owned Eve requests against the selected Verse target."))),
|
||||
Array.Empty<AetheriaRuntimeSurfaceStyleToken>()),
|
||||
commands: new[]
|
||||
{
|
||||
new AetheriaRuntimeSurfaceCommandTemplate(
|
||||
AetheriaRuntimeClientTargetCommands.Refresh,
|
||||
"Refresh",
|
||||
AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
|
||||
new AetheriaRuntimeSurfaceCommandTemplate(
|
||||
AetheriaRuntimeClientTargetCommands.CycleTargetKind,
|
||||
"Cycle Transport",
|
||||
AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
|
||||
new AetheriaRuntimeSurfaceCommandTemplate(
|
||||
AetheriaRuntimeClientTargetCommands.SetTitle,
|
||||
"Set Title",
|
||||
AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
|
||||
new AetheriaRuntimeSurfaceCommandTemplate(
|
||||
AetheriaRuntimeClientTargetCommands.SetVerseId,
|
||||
"Set Verse Id",
|
||||
AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
|
||||
new AetheriaRuntimeSurfaceCommandTemplate(
|
||||
AetheriaRuntimeClientTargetCommands.SetRuntimeId,
|
||||
"Set Runtime Id",
|
||||
AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
|
||||
new AetheriaRuntimeSurfaceCommandTemplate(
|
||||
AetheriaRuntimeClientTargetCommands.SetCultMeshAddress,
|
||||
"Set CultMesh Address",
|
||||
AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
|
||||
new AetheriaRuntimeSurfaceCommandTemplate(
|
||||
AetheriaRuntimeClientTargetCommands.SetStateFilePath,
|
||||
"Set State File Path",
|
||||
AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
|
||||
new AetheriaRuntimeSurfaceCommandTemplate(
|
||||
AetheriaRuntimeClientTargetCommands.SetDiscoveryEndpoints,
|
||||
"Set Discovery Endpoints",
|
||||
AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
|
||||
new AetheriaRuntimeSurfaceCommandTemplate(
|
||||
AetheriaRuntimeClientTargetCommands.DiscoverVerses,
|
||||
"Discover Verses",
|
||||
AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
|
||||
new AetheriaRuntimeSurfaceCommandTemplate(
|
||||
AetheriaRuntimeClientTargetCommands.SelectDiscoveredVerse,
|
||||
"Select Discovered Verse",
|
||||
AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
|
||||
new AetheriaRuntimeSurfaceCommandTemplate(
|
||||
AetheriaRuntimeClientTargetCommands.SyncReplica,
|
||||
"Sync Replica",
|
||||
AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
|
||||
new AetheriaRuntimeSurfaceCommandTemplate(
|
||||
AetheriaRuntimeVerseHostCommands.CycleVisibility,
|
||||
"Toggle Visibility",
|
||||
"cultmesh"),
|
||||
new AetheriaRuntimeSurfaceCommandTemplate(
|
||||
AetheriaRuntimeVerseHostCommands.Refresh,
|
||||
"Refresh Host",
|
||||
"cultmesh")
|
||||
});
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent Metric(string id, string label, string value)
|
||||
{
|
||||
return Node(id, "metric", new[] { ("label", label), ("value", value ?? "") });
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent Text(string id, string value)
|
||||
{
|
||||
return Node(id, "text", new[] { ("value", value ?? "") });
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent Button(string id, string label, string command)
|
||||
{
|
||||
return Node(id, "control.button", new[] { ("label", label ?? ""), ("command", command ?? "") });
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent TextInput(string id, string label, string value, string command)
|
||||
{
|
||||
return Node(
|
||||
id,
|
||||
"control.text",
|
||||
new[] { ("label", label ?? ""), ("value", value ?? ""), ("command", command ?? "") });
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent ButtonRow(
|
||||
string id,
|
||||
params AetheriaRuntimeSurfaceComponent[] children)
|
||||
{
|
||||
return Node(id, "row", Array.Empty<(string Key, string Value)>(), children);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent Row(
|
||||
string id,
|
||||
params AetheriaRuntimeSurfaceComponent[] children)
|
||||
{
|
||||
return Node(id, "inspector.kv", Array.Empty<(string Key, string Value)>(), children);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent Node(
|
||||
string id,
|
||||
string kind,
|
||||
IEnumerable<(string Key, string Value)> props,
|
||||
params AetheriaRuntimeSurfaceComponent[] children)
|
||||
{
|
||||
return new AetheriaRuntimeSurfaceComponent(
|
||||
id,
|
||||
kind,
|
||||
props.ToDictionary(prop => prop.Key, prop => prop.Value, StringComparer.Ordinal),
|
||||
children ?? Array.Empty<AetheriaRuntimeSurfaceComponent>());
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent BuildDiscoveredVerseNode(
|
||||
string targetKind,
|
||||
string targetVerseId,
|
||||
AetheriaRuntimeDiscoveredVerse verse,
|
||||
int index)
|
||||
{
|
||||
var verseLabel = BuildVerseLabel(verse);
|
||||
var address = verse.DiscoveryEndpoints.FirstOrDefault() ?? "";
|
||||
var isSelected = string.Equals(targetKind, AetheriaRuntimeClientTargetKinds.CultMeshVerse, StringComparison.Ordinal) &&
|
||||
string.Equals(targetVerseId, verse.VerseId, StringComparison.Ordinal);
|
||||
var buttonLabel = isSelected ? "Selected" : "Select";
|
||||
|
||||
return Node(
|
||||
$"aetheria.clientTarget.discovery.verse.{index}",
|
||||
"inspector.kv",
|
||||
Array.Empty<(string Key, string Value)>(),
|
||||
Metric($"aetheria.clientTarget.discovery.verse.{index}.label", "Verse", verseLabel),
|
||||
Metric($"aetheria.clientTarget.discovery.verse.{index}.authority", "Authority", verse.AuthorityModel),
|
||||
Metric($"aetheria.clientTarget.discovery.verse.{index}.transport", "Transport", verse.TransportVersion),
|
||||
Metric($"aetheria.clientTarget.discovery.verse.{index}.endpoint", "Endpoint", address),
|
||||
Text(
|
||||
$"aetheria.clientTarget.discovery.verse.{index}.description",
|
||||
string.IsNullOrWhiteSpace(verse.Description)
|
||||
? "No public description."
|
||||
: verse.Description),
|
||||
Node(
|
||||
$"aetheria.clientTarget.discovery.verse.{index}.action",
|
||||
"control.button",
|
||||
new[]
|
||||
{
|
||||
("label", buttonLabel),
|
||||
("command", AetheriaRuntimeClientTargetCommands.SelectDiscoveredVerse),
|
||||
("verseId", verse.VerseId ?? ""),
|
||||
("title", verse.DisplayName ?? ""),
|
||||
("cultMeshAddress", address),
|
||||
("discoveryEndpoints", string.Join(", ", verse.DiscoveryEndpoints ?? Array.Empty<string>()))
|
||||
}));
|
||||
}
|
||||
|
||||
private static string BuildVerseLabel(AetheriaRuntimeDiscoveredVerse verse)
|
||||
{
|
||||
return verse == null
|
||||
? "Unknown Verse"
|
||||
: BuildVerseLabel(verse.DisplayName, verse.VerseId);
|
||||
}
|
||||
|
||||
private static string BuildVerseLabel(string displayName, string verseId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(displayName))
|
||||
return string.IsNullOrWhiteSpace(verseId) ? "Unknown Verse" : verseId;
|
||||
|
||||
return string.IsNullOrWhiteSpace(verseId) || string.Equals(displayName, verseId, StringComparison.Ordinal)
|
||||
? displayName
|
||||
: $"{displayName} ({verseId})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7c51ae1e0547a6c45b454932a285a555
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public sealed class AetheriaRuntimeCommittedFactImportResult
|
||||
{
|
||||
public AetheriaRuntimeCommittedFactImportResult(
|
||||
AetheriaRuntimeDaemonTickResult tick,
|
||||
IReadOnlyList<string> acceptedFactIds,
|
||||
IReadOnlyList<string> rejectedFactIds,
|
||||
IReadOnlyList<string> duplicateFactIds)
|
||||
{
|
||||
Tick = tick ?? throw new ArgumentNullException(nameof(tick));
|
||||
AcceptedFactIds = acceptedFactIds ?? Array.Empty<string>();
|
||||
RejectedFactIds = rejectedFactIds ?? Array.Empty<string>();
|
||||
DuplicateFactIds = duplicateFactIds ?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
public AetheriaRuntimeDaemonTickResult Tick { get; }
|
||||
public IReadOnlyList<string> AcceptedFactIds { get; }
|
||||
public IReadOnlyList<string> RejectedFactIds { get; }
|
||||
public IReadOnlyList<string> DuplicateFactIds { get; }
|
||||
public AetheriaRuntimeRunCheckpointCommit Run => Tick.Run;
|
||||
public AetheriaRuntimeDaemonFrameDocument Frame => Tick.Frame;
|
||||
}
|
||||
|
||||
public static class AetheriaRuntimeCommittedFactImporter
|
||||
{
|
||||
public static AetheriaRuntimeCommittedFactImportResult ImportIntoFrame(
|
||||
string stateFilePath,
|
||||
AetheriaRuntimeDaemonFrameDocument? currentFrame,
|
||||
IEnumerable<AetheriaRuntimeCommittedCommandFactDocument> facts,
|
||||
AetheriaRuntimeVerseAuthorityPolicyDocument? policy,
|
||||
IEnumerable<AetheriaRuntimeAuthorityLeaseDocument>? leases,
|
||||
string localRuntimeId,
|
||||
string daemonId,
|
||||
string sessionId,
|
||||
string verseId,
|
||||
IAetheriaRuntimeWorldPhysics worldPhysics,
|
||||
IEnumerable<string>? importedFactIds = null,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog = null)
|
||||
{
|
||||
if (worldPhysics == null)
|
||||
throw new ArgumentNullException(nameof(worldPhysics));
|
||||
|
||||
currentFrame ??= new AetheriaRuntimeDaemonFrameDocument
|
||||
{
|
||||
Run = new AetheriaRuntimeRunCheckpointCommit(),
|
||||
FixedDeltaSeconds = 0.02
|
||||
};
|
||||
|
||||
var alreadyImported = new HashSet<string>(
|
||||
importedFactIds ?? Array.Empty<string>(),
|
||||
StringComparer.Ordinal);
|
||||
var acceptedFacts = new List<string>();
|
||||
var rejectedFacts = new List<string>();
|
||||
var duplicateFacts = new List<string>();
|
||||
var commands = new List<AetheriaRuntimeDaemonCommandDocument>();
|
||||
var acceptedCommands = new List<AcceptedCommandFact>();
|
||||
|
||||
foreach (var fact in facts ?? Enumerable.Empty<AetheriaRuntimeCommittedCommandFactDocument>())
|
||||
{
|
||||
if (fact == null || string.IsNullOrWhiteSpace(fact.FactId))
|
||||
continue;
|
||||
|
||||
if (!alreadyImported.Add(fact.FactId))
|
||||
{
|
||||
duplicateFacts.Add(fact.FactId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.Equals(fact.Outcome, AetheriaRuntimeCommandFactOutcomes.Applied, StringComparison.Ordinal))
|
||||
{
|
||||
rejectedFacts.Add(fact.FactId);
|
||||
continue;
|
||||
}
|
||||
|
||||
var command = NormalizeCommand(fact);
|
||||
var decision = AetheriaRuntimeAuthorityRouter.Authorize(
|
||||
command,
|
||||
policy,
|
||||
leases,
|
||||
localRuntimeId);
|
||||
if (!decision.Authorized)
|
||||
{
|
||||
rejectedFacts.Add(fact.FactId);
|
||||
continue;
|
||||
}
|
||||
|
||||
acceptedFacts.Add(fact.FactId);
|
||||
commands.Add(command);
|
||||
acceptedCommands.Add(new AcceptedCommandFact(fact.FactId, command.CommandId));
|
||||
}
|
||||
|
||||
var fixedDeltaSeconds = currentFrame.FixedDeltaSeconds > 0
|
||||
? currentFrame.FixedDeltaSeconds
|
||||
: 0.02;
|
||||
var frameId = currentFrame.FrameId + 1;
|
||||
var simulationTimeSeconds = currentFrame.SimulationTimeSeconds + fixedDeltaSeconds;
|
||||
var tick = AetheriaRuntimeDaemonTickRunner.Tick(
|
||||
stateFilePath,
|
||||
currentFrame.Run ?? new AetheriaRuntimeRunCheckpointCommit(),
|
||||
new AetheriaRuntimeDaemonTickOptions
|
||||
{
|
||||
DaemonId = string.IsNullOrWhiteSpace(daemonId) ? localRuntimeId : daemonId,
|
||||
SessionId = string.IsNullOrWhiteSpace(sessionId) ? currentFrame.SessionId : sessionId,
|
||||
VerseId = string.IsNullOrWhiteSpace(verseId) ? "aetheria.local" : verseId,
|
||||
FrameId = frameId,
|
||||
SimulationTimeSeconds = simulationTimeSeconds,
|
||||
FixedDeltaSeconds = fixedDeltaSeconds,
|
||||
ObservedCommands = commands,
|
||||
AccountedCommandIds = currentFrame.AccountedCommandIds,
|
||||
CumulativeAppliedCommandIds = currentFrame.CumulativeAppliedCommandIds,
|
||||
CumulativeRejectedCommandIds = currentFrame.CumulativeRejectedCommandIds,
|
||||
Catalog = catalog ?? EmptyCatalog(),
|
||||
WorldPhysics = worldPhysics,
|
||||
BuildPublications = false
|
||||
});
|
||||
|
||||
var operationRejectedCommandIds = new HashSet<string>(
|
||||
tick.OperationResult.RejectedCommandIds ?? Array.Empty<string>(),
|
||||
StringComparer.Ordinal);
|
||||
if (operationRejectedCommandIds.Count > 0)
|
||||
{
|
||||
acceptedFacts.Clear();
|
||||
foreach (var item in acceptedCommands)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(item.CommandId) && operationRejectedCommandIds.Contains(item.CommandId))
|
||||
rejectedFacts.Add(item.FactId);
|
||||
else
|
||||
acceptedFacts.Add(item.FactId);
|
||||
}
|
||||
}
|
||||
|
||||
return new AetheriaRuntimeCommittedFactImportResult(
|
||||
tick,
|
||||
acceptedFacts.Distinct(StringComparer.Ordinal).ToArray(),
|
||||
rejectedFacts.Distinct(StringComparer.Ordinal).ToArray(),
|
||||
duplicateFacts.Distinct(StringComparer.Ordinal).ToArray());
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeDaemonCommandDocument NormalizeCommand(
|
||||
AetheriaRuntimeCommittedCommandFactDocument fact)
|
||||
{
|
||||
var command = fact.Command ?? new AetheriaRuntimeDaemonCommandDocument();
|
||||
command.Schema = AetheriaRuntimeDaemonSchemas.Command;
|
||||
if (string.IsNullOrWhiteSpace(command.CommandId))
|
||||
command.CommandId = fact.CommandId;
|
||||
if (string.IsNullOrWhiteSpace(command.AuthorRuntimeId))
|
||||
command.AuthorRuntimeId = fact.SourceRuntimeId;
|
||||
if (string.IsNullOrWhiteSpace(command.ClientId))
|
||||
command.ClientId = fact.SourceRuntimeId;
|
||||
if (string.IsNullOrWhiteSpace(command.SubjectKey))
|
||||
command.SubjectKey = fact.SubjectKey;
|
||||
if (string.IsNullOrWhiteSpace(command.ClaimKind))
|
||||
command.ClaimKind = fact.ClaimKind;
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeCatalogSnapshot EmptyCatalog()
|
||||
{
|
||||
return new AetheriaRuntimeCatalogSnapshot(
|
||||
Array.Empty<AetheriaRuntimeCatalogItem>(),
|
||||
Array.Empty<AetheriaRuntimeCorporation>(),
|
||||
Array.Empty<AetheriaRuntimeNameFile>());
|
||||
}
|
||||
|
||||
private sealed class AcceptedCommandFact
|
||||
{
|
||||
public AcceptedCommandFact(string factId, string commandId)
|
||||
{
|
||||
FactId = factId ?? "";
|
||||
CommandId = commandId ?? "";
|
||||
}
|
||||
|
||||
public string FactId { get; }
|
||||
public string CommandId { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ec790b3f056eeb343813a139ea4fea8f
|
||||
|
|
@ -1,442 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public static class AetheriaRuntimeConsumableSimulation
|
||||
{
|
||||
public static void Step(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
IEnumerable<AetheriaRuntimeDaemonConsumableIntent> intents,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog,
|
||||
long frameId,
|
||||
double deltaSeconds)
|
||||
{
|
||||
if (run == null || deltaSeconds <= 0)
|
||||
return;
|
||||
|
||||
foreach (var zone in run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
|
||||
StepZone(run, zone, zone?.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>(), intents, catalog, frameId, deltaSeconds);
|
||||
}
|
||||
|
||||
public static void StepZone(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeZoneSnapshotCommit? zone,
|
||||
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
|
||||
IEnumerable<AetheriaRuntimeDaemonConsumableIntent> intents,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog,
|
||||
long frameId,
|
||||
double deltaSeconds)
|
||||
{
|
||||
if (run == null || zone == null || deltaSeconds <= 0)
|
||||
return;
|
||||
|
||||
var intentIndex = 0;
|
||||
foreach (var intent in intents ?? Enumerable.Empty<AetheriaRuntimeDaemonConsumableIntent>())
|
||||
{
|
||||
if (AetheriaRuntimeRunCheckpointCommit.TryParseEntityKey(intent?.ActorEntityKey ?? "", out var intentZone, out _) &&
|
||||
intentZone == zone.ZoneIndex)
|
||||
Activate(run, intent, catalog, frameId, intentIndex);
|
||||
intentIndex++;
|
||||
}
|
||||
|
||||
foreach (var entity in entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
|
||||
{
|
||||
if (entity == null)
|
||||
continue;
|
||||
|
||||
var active = (entity.ActiveConsumables ?? Array.Empty<AetheriaRuntimeActiveConsumableCommit>())
|
||||
.Where(effect => effect != null)
|
||||
.ToArray();
|
||||
for (var index = 0; index < active.Length; index++)
|
||||
{
|
||||
EnsureEffectIdentity(zone, entity, active[index], index);
|
||||
ExecuteBehaviors(run, zone, entity, active[index], catalog, frameId, deltaSeconds);
|
||||
active[index].RemainingDuration -= deltaSeconds;
|
||||
if (active[index].RemainingDuration < 0)
|
||||
{
|
||||
AetheriaRuntimeGameEvents.Append(run, new AetheriaRuntimeGameEventCommit
|
||||
{
|
||||
EventId = $"frame:{frameId}:zone:{zone.ZoneIndex}:entity:{entity.EntityIndex}:consumable:{active[index].EffectId}:expired",
|
||||
Kind = "consumable.expired",
|
||||
FrameId = frameId,
|
||||
ZoneIndex = zone.ZoneIndex,
|
||||
SourceEntityIndex = entity.EntityIndex,
|
||||
ItemKey = active[index].ItemKey
|
||||
});
|
||||
}
|
||||
}
|
||||
entity.ActiveConsumables = active.Where(effect => effect.RemainingDuration >= 0).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExecuteBehaviors(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeZoneSnapshotCommit zone,
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeActiveConsumableCommit effect,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog,
|
||||
long frameId,
|
||||
double deltaSeconds)
|
||||
{
|
||||
var item = catalog?.FindItem(effect.ItemKey);
|
||||
if (item == null)
|
||||
{
|
||||
BehaviorStopped(run, zone, entity, effect, frameId, -1, "missing-catalog-item");
|
||||
return;
|
||||
}
|
||||
|
||||
var elapsed = effect.Duration <= 0
|
||||
? 1
|
||||
: Math.Max(0, Math.Min(1, (effect.Duration - effect.RemainingDuration) / effect.Duration));
|
||||
var effectiveness = item.EffectivenessCurveKeys == null || item.EffectivenessCurveKeys.Count == 0
|
||||
? 1
|
||||
: AetheriaRuntimeDaemonItemStatQueries.SampleCurve(item.EffectivenessCurveKeys, elapsed);
|
||||
var payloads = item.BehaviorPayloads ?? Array.Empty<AetheriaRuntimeBehaviorPayload>();
|
||||
if (!ReconcileBehaviorStates(effect, payloads))
|
||||
{
|
||||
BehaviorStopped(run, zone, entity, effect, frameId, -1, "duplicate-behavior-id");
|
||||
return;
|
||||
}
|
||||
for (var behaviorIndex = 0; behaviorIndex < payloads.Count; behaviorIndex++)
|
||||
if (payloads[behaviorIndex] != null &&
|
||||
string.Equals(payloads[behaviorIndex].Kind, "StatModifier", StringComparison.Ordinal))
|
||||
FindBehaviorState(effect, behaviorIndex, payloads[behaviorIndex]).StatModifierTargetStatCount =
|
||||
AetheriaRuntimeBehaviorSimulation.CountTargets(entity, catalog!, payloads[behaviorIndex]);
|
||||
UpdateAlwaysUpdatedBehaviors(effect, payloads, effectiveness, deltaSeconds);
|
||||
for (var behaviorIndex = 0; behaviorIndex < payloads.Count; behaviorIndex++)
|
||||
{
|
||||
var payload = payloads[behaviorIndex];
|
||||
if (payload == null)
|
||||
continue;
|
||||
if (ExecuteBehavior(
|
||||
entity, effect, payload, behaviorIndex, catalog, effectiveness, deltaSeconds, out var reason))
|
||||
continue;
|
||||
|
||||
BehaviorStopped(run, zone, entity, effect, frameId, behaviorIndex, reason);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ExecuteBehavior(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeActiveConsumableCommit effect,
|
||||
AetheriaRuntimeBehaviorPayload payload,
|
||||
int behaviorIndex,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog,
|
||||
double effectiveness,
|
||||
double deltaSeconds,
|
||||
out string reason)
|
||||
{
|
||||
reason = "";
|
||||
switch (payload.Kind)
|
||||
{
|
||||
case "StatModifier":
|
||||
FindBehaviorState(effect, behaviorIndex, payload).StatModifierExecuted = true;
|
||||
return true;
|
||||
case "Cooldown":
|
||||
{
|
||||
var state = FindBehaviorState(effect, behaviorIndex, payload);
|
||||
if (state.ScalarState < 0)
|
||||
{
|
||||
state.ScalarState = 1;
|
||||
return true;
|
||||
}
|
||||
reason = "cooldown";
|
||||
return false;
|
||||
}
|
||||
case "EnergyDraw":
|
||||
{
|
||||
var demand = Evaluate(payload, 1, effect, effectiveness) * (ReadBool(payload, 2) ? deltaSeconds : 1);
|
||||
if (AetheriaRuntimeEnergySimulation.TryConsume(entity, catalog, demand))
|
||||
return true;
|
||||
reason = "insufficient-energy";
|
||||
return false;
|
||||
}
|
||||
case "Heat":
|
||||
// The fossil only added heat to equipped items; consumable heat was intentionally a no-op.
|
||||
return true;
|
||||
case "ItemUsage":
|
||||
{
|
||||
var itemKey = ReadItemKey(payload, 1);
|
||||
if (!string.IsNullOrWhiteSpace(itemKey) &&
|
||||
AetheriaRuntimeCargoTransactions.TryFind(entity, itemKey, out var cargoIndex, out var x, out var y) &&
|
||||
AetheriaRuntimeCargoTransactions.TryRemoveQuantity(entity, cargoIndex, itemKey, x, y, 1, out _))
|
||||
return true;
|
||||
reason = "missing-required-item";
|
||||
return false;
|
||||
}
|
||||
case "Wear":
|
||||
return true;
|
||||
default:
|
||||
reason = "unsupported-behavior:" + (payload.Kind ?? "");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ReconcileBehaviorStates(
|
||||
AetheriaRuntimeActiveConsumableCommit effect,
|
||||
IReadOnlyList<AetheriaRuntimeBehaviorPayload> payloads)
|
||||
{
|
||||
var identities = payloads
|
||||
.Select((payload, behaviorIndex) => BehaviorIdentity(payload, behaviorIndex))
|
||||
.ToArray();
|
||||
if (identities.Distinct(StringComparer.Ordinal).Count() != identities.Length)
|
||||
return false;
|
||||
|
||||
var previousStates = (effect.BehaviorStates ?? Array.Empty<AetheriaRuntimeConsumableBehaviorStateCommit>())
|
||||
.Where(state => state != null)
|
||||
.ToArray();
|
||||
var previousById = previousStates
|
||||
.Where(state => !string.IsNullOrWhiteSpace(state.BehaviorId))
|
||||
.GroupBy(state => state.BehaviorId, StringComparer.Ordinal)
|
||||
.ToDictionary(group => group.Key, group => group.First(), StringComparer.Ordinal);
|
||||
effect.BehaviorStates = payloads
|
||||
.Select((payload, behaviorIndex) =>
|
||||
{
|
||||
var kind = payload?.Kind ?? "";
|
||||
var behaviorId = identities[behaviorIndex];
|
||||
if (!previousById.TryGetValue(behaviorId, out var state))
|
||||
state = previousStates.FirstOrDefault(candidate =>
|
||||
string.IsNullOrWhiteSpace(candidate.BehaviorId) &&
|
||||
candidate.BehaviorIndex == behaviorIndex &&
|
||||
string.Equals(candidate.BehaviorKind, kind, StringComparison.Ordinal));
|
||||
state ??= new AetheriaRuntimeConsumableBehaviorStateCommit
|
||||
{
|
||||
ScalarState = 0
|
||||
};
|
||||
state.BehaviorId = behaviorId;
|
||||
state.BehaviorIndex = behaviorIndex;
|
||||
state.BehaviorKind = kind;
|
||||
return state;
|
||||
})
|
||||
.ToArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string BehaviorIdentity(AetheriaRuntimeBehaviorPayload? payload, int behaviorIndex) =>
|
||||
!string.IsNullOrWhiteSpace(payload?.BehaviorId)
|
||||
? payload.BehaviorId
|
||||
: $"legacy:{behaviorIndex}:{payload?.Kind ?? ""}";
|
||||
|
||||
private static void UpdateAlwaysUpdatedBehaviors(
|
||||
AetheriaRuntimeActiveConsumableCommit effect,
|
||||
IReadOnlyList<AetheriaRuntimeBehaviorPayload> payloads,
|
||||
double effectiveness,
|
||||
double deltaSeconds)
|
||||
{
|
||||
for (var behaviorIndex = 0; behaviorIndex < payloads.Count; behaviorIndex++)
|
||||
{
|
||||
var payload = payloads[behaviorIndex];
|
||||
if (payload == null)
|
||||
continue;
|
||||
|
||||
if (string.Equals(payload.Kind, "StatModifier", StringComparison.Ordinal))
|
||||
{
|
||||
var state = FindBehaviorState(effect, behaviorIndex, payload);
|
||||
state.StatModifierApplied = state.StatModifierExecuted;
|
||||
state.StatModifierExecuted = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.Equals(payload.Kind, "Cooldown", StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
var duration = Evaluate(payload, 1, effect, effectiveness);
|
||||
FindBehaviorState(effect, behaviorIndex, payload).ScalarState -= deltaSeconds / duration;
|
||||
}
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeConsumableBehaviorStateCommit FindBehaviorState(
|
||||
AetheriaRuntimeActiveConsumableCommit effect,
|
||||
int behaviorIndex,
|
||||
AetheriaRuntimeBehaviorPayload payload) =>
|
||||
(effect.BehaviorStates ?? Array.Empty<AetheriaRuntimeConsumableBehaviorStateCommit>())
|
||||
.Single(state => state != null &&
|
||||
string.Equals(state.BehaviorId, BehaviorIdentity(payload, behaviorIndex),
|
||||
StringComparison.Ordinal));
|
||||
|
||||
private static void EnsureEffectIdentity(
|
||||
AetheriaRuntimeZoneSnapshotCommit zone,
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeActiveConsumableCommit effect,
|
||||
int legacyIndex)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(effect.EffectId))
|
||||
return;
|
||||
effect.EffectId = $"legacy:zone:{zone.ZoneIndex}:entity:{entity.EntityIndex}:effect:{legacyIndex}";
|
||||
}
|
||||
|
||||
private static double Evaluate(
|
||||
AetheriaRuntimeBehaviorPayload payload,
|
||||
int fieldKey,
|
||||
AetheriaRuntimeActiveConsumableCommit effect,
|
||||
double effectiveness) =>
|
||||
AetheriaRuntimeDaemonItemStatQueries.EvaluateConsumablePerformanceStat(
|
||||
Field(payload, fieldKey)?.Value,
|
||||
effect.Quality,
|
||||
effectiveness);
|
||||
|
||||
private static bool ReadBool(AetheriaRuntimeBehaviorPayload payload, int fieldKey) =>
|
||||
Field(payload, fieldKey)?.Value?.BoolValue ?? false;
|
||||
|
||||
private static string ReadItemKey(AetheriaRuntimeBehaviorPayload payload, int fieldKey)
|
||||
{
|
||||
var value = Field(payload, fieldKey)?.Value;
|
||||
return value?.ItemKeyValue ?? value?.StringValue ?? "";
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorField? Field(AetheriaRuntimeBehaviorPayload payload, int fieldKey) =>
|
||||
(payload.Fields ?? Array.Empty<AetheriaRuntimeBehaviorField>())
|
||||
.FirstOrDefault(field => field != null && field.Key == fieldKey);
|
||||
|
||||
private static void BehaviorStopped(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeZoneSnapshotCommit zone,
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeActiveConsumableCommit effect,
|
||||
long frameId,
|
||||
int behaviorIndex,
|
||||
string reason)
|
||||
{
|
||||
AetheriaRuntimeGameEvents.Append(run, new AetheriaRuntimeGameEventCommit
|
||||
{
|
||||
EventId = $"frame:{frameId}:zone:{zone.ZoneIndex}:entity:{entity.EntityIndex}:consumable:{effect.EffectId}:behavior:{behaviorIndex}:stopped",
|
||||
Kind = "consumable.behavior.stopped",
|
||||
FrameId = frameId,
|
||||
ZoneIndex = zone.ZoneIndex,
|
||||
SourceEntityIndex = entity.EntityIndex,
|
||||
ItemKey = effect.ItemKey,
|
||||
ScalarValue = behaviorIndex,
|
||||
Reason = reason
|
||||
});
|
||||
}
|
||||
|
||||
public static bool CanActivate(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog,
|
||||
string itemKey)
|
||||
{
|
||||
var item = catalog?.FindItem(itemKey);
|
||||
return entity != null && item != null &&
|
||||
string.Equals(item.Category, AetheriaRuntimeItemCategories.Consumable, StringComparison.Ordinal) &&
|
||||
item.Duration > 0 &&
|
||||
(item.Stackable || !(entity.ActiveConsumables ?? Array.Empty<AetheriaRuntimeActiveConsumableCommit>())
|
||||
.Any(active => active != null && string.Equals(active.ItemKey, itemKey, StringComparison.Ordinal))) &&
|
||||
AetheriaRuntimeCargoTransactions.TryFind(entity, itemKey, out _, out _, out _);
|
||||
}
|
||||
|
||||
private static void Activate(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeDaemonConsumableIntent? intent,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog,
|
||||
long frameId,
|
||||
int intentIndex)
|
||||
{
|
||||
var itemKey = intent?.ItemKey ?? "";
|
||||
if (!TryResolveActor(run, intent?.ActorEntityKey, out var zone, out var entity))
|
||||
{
|
||||
Refuse(run, frameId, intentIndex, zone, entity, itemKey, "actor-unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
var item = catalog?.FindItem(itemKey);
|
||||
if (item == null || !string.Equals(item.Category, AetheriaRuntimeItemCategories.Consumable, StringComparison.Ordinal))
|
||||
{
|
||||
Refuse(run, frameId, intentIndex, zone, entity, itemKey, "not-consumable");
|
||||
return;
|
||||
}
|
||||
if (item.Duration <= 0)
|
||||
{
|
||||
Refuse(run, frameId, intentIndex, zone, entity, itemKey, "invalid-duration");
|
||||
return;
|
||||
}
|
||||
if (!item.Stackable && (entity.ActiveConsumables ?? Array.Empty<AetheriaRuntimeActiveConsumableCommit>())
|
||||
.Any(active => active != null && string.Equals(active.ItemKey, itemKey, StringComparison.Ordinal)))
|
||||
{
|
||||
Refuse(run, frameId, intentIndex, zone, entity, itemKey, "already-active");
|
||||
return;
|
||||
}
|
||||
if (!AetheriaRuntimeCargoTransactions.TryFind(entity, itemKey, out var cargoIndex, out var x, out var y) ||
|
||||
!AetheriaRuntimeCargoTransactions.TryRemoveQuantity(entity, cargoIndex, itemKey, x, y, 1, out var consumed))
|
||||
{
|
||||
Refuse(run, frameId, intentIndex, zone, entity, itemKey, "missing-cargo");
|
||||
return;
|
||||
}
|
||||
|
||||
entity.ActiveConsumables = (entity.ActiveConsumables ?? Array.Empty<AetheriaRuntimeActiveConsumableCommit>())
|
||||
.Where(active => active != null)
|
||||
.Append(new AetheriaRuntimeActiveConsumableCommit
|
||||
{
|
||||
EffectId = $"effect:frame:{frameId}:zone:{zone.ZoneIndex}:entity:{entity.EntityIndex}:intent:{intentIndex}",
|
||||
ItemKey = item.ItemKey,
|
||||
Quality = consumed.Item.Quality,
|
||||
Duration = item.Duration,
|
||||
RemainingDuration = item.Duration
|
||||
})
|
||||
.ToArray();
|
||||
AetheriaRuntimeGameEvents.Append(run, new AetheriaRuntimeGameEventCommit
|
||||
{
|
||||
EventId = $"frame:{frameId}:zone:{zone.ZoneIndex}:entity:{entity.EntityIndex}:consumable:{intentIndex}:activated",
|
||||
Kind = "consumable.activated",
|
||||
FrameId = frameId,
|
||||
ZoneIndex = zone.ZoneIndex,
|
||||
SourceEntityIndex = entity.EntityIndex,
|
||||
ItemKey = item.ItemKey,
|
||||
ScalarValue = consumed.Item.Quality,
|
||||
AuxiliaryValue = item.Duration
|
||||
});
|
||||
}
|
||||
|
||||
private static bool TryResolveActor(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
string? actorEntityKey,
|
||||
out AetheriaRuntimeZoneSnapshotCommit? zone,
|
||||
out AetheriaRuntimeEntitySnapshotCommit? entity)
|
||||
{
|
||||
AetheriaRuntimeRunCheckpointCommit.TryParseEntityKey(actorEntityKey ?? "", out var actorZoneIndex, out var actorEntityIndex);
|
||||
foreach (var candidateZone in run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
|
||||
foreach (var candidate in candidateZone?.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
|
||||
{
|
||||
var canonicalKey = candidate == null
|
||||
? ""
|
||||
: run.EntityRecordKey(candidateZone.ZoneIndex, candidate.EntityIndex);
|
||||
if (candidate != null &&
|
||||
(string.Equals(canonicalKey, actorEntityKey, StringComparison.Ordinal) ||
|
||||
(candidateZone.ZoneIndex == actorZoneIndex && candidate.EntityIndex == actorEntityIndex)))
|
||||
{
|
||||
zone = candidateZone;
|
||||
entity = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
zone = null;
|
||||
entity = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void Refuse(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
long frameId,
|
||||
int intentIndex,
|
||||
AetheriaRuntimeZoneSnapshotCommit? zone,
|
||||
AetheriaRuntimeEntitySnapshotCommit? entity,
|
||||
string itemKey,
|
||||
string reason)
|
||||
{
|
||||
AetheriaRuntimeGameEvents.Append(run, new AetheriaRuntimeGameEventCommit
|
||||
{
|
||||
EventId = $"frame:{frameId}:consumable:{intentIndex}:refused",
|
||||
Kind = "consumable.activation.refused",
|
||||
FrameId = frameId,
|
||||
ZoneIndex = zone?.ZoneIndex ?? -1,
|
||||
SourceEntityIndex = entity?.EntityIndex ?? -1,
|
||||
ItemKey = itemKey,
|
||||
Reason = reason
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 76ddd6e0004f0a247a16d1a393d768d1
|
||||
|
|
@ -1,526 +0,0 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Mesh;
|
||||
using MessagePack;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
internal static class AetheriaRuntimeCultCacheDocumentStore
|
||||
{
|
||||
public static void WriteEveCommand(string path, AetheriaRuntimeEveCommandDocument document)
|
||||
{
|
||||
WriteDocument(
|
||||
path,
|
||||
$"command:gamecult.eve.command.{document.CommandId}.v1",
|
||||
AetheriaRuntimeEveCommandDocument.SchemaId,
|
||||
"gamecult.eve.command",
|
||||
"gamecult.eve.command.v1",
|
||||
document.IssuedAtUtc,
|
||||
MessagePackSerializer.Serialize(document));
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeEveCommandDocument ReadEveCommand(string path)
|
||||
{
|
||||
return MessagePackSerializer.Deserialize<AetheriaRuntimeEveCommandDocument>(
|
||||
CultMesh.ReadSingleFileDocumentPayload(path, AetheriaRuntimeEveCommandDocument.SchemaId));
|
||||
}
|
||||
|
||||
public static void WriteDaemonCommand(string path, AetheriaRuntimeDaemonCommandDocument document)
|
||||
{
|
||||
WriteDocument(
|
||||
path,
|
||||
$"command:gamecult.aetheria.daemon_command.{document.CommandId}.v1",
|
||||
AetheriaRuntimeDaemonSchemas.Command,
|
||||
"gamecult.aetheria.daemon_command",
|
||||
"gamecult.aetheria.daemon_command.v1",
|
||||
document.IssuedAtUtc,
|
||||
MessagePackSerializer.Serialize(document));
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeDaemonCommandDocument ReadDaemonCommand(string path)
|
||||
{
|
||||
return MessagePackSerializer.Deserialize<AetheriaRuntimeDaemonCommandDocument>(
|
||||
CultMesh.ReadSingleFileDocumentPayload(path, AetheriaRuntimeDaemonSchemas.Command));
|
||||
}
|
||||
|
||||
private static void WriteDocument(
|
||||
string path,
|
||||
string key,
|
||||
string schemaId,
|
||||
string schemaName,
|
||||
string schemaVersion,
|
||||
string storedAt,
|
||||
byte[] payload)
|
||||
{
|
||||
CultMesh.WriteSingleFileDocumentPayload(
|
||||
path,
|
||||
new CultRecordKey(key),
|
||||
new CultMeshSingleFileDocumentSchema(schemaId, schemaName, schemaVersion),
|
||||
storedAt,
|
||||
payload);
|
||||
}
|
||||
|
||||
internal static byte[] WriteRuntimeSurfaceDocument(AetheriaRuntimeSurfaceDocument document)
|
||||
{
|
||||
var buffer = new ArrayBufferWriter<byte>();
|
||||
var writer = new MessagePackWriter(buffer);
|
||||
|
||||
writer.WriteArrayHeader(7);
|
||||
writer.Write(document.ProviderId ?? "");
|
||||
writer.Write(document.ProviderKind ?? "");
|
||||
writer.Write(document.Title ?? "");
|
||||
writer.Write(document.Version);
|
||||
writer.Write(document.UpdatedAtUtc ?? "");
|
||||
WriteSurfaceTree(ref writer, document.Surface);
|
||||
WriteSurfaceCommands(ref writer, document.Commands);
|
||||
writer.Flush();
|
||||
|
||||
return buffer.WrittenSpan.ToArray();
|
||||
}
|
||||
|
||||
private static void WriteSurfaceTree(ref MessagePackWriter writer, AetheriaRuntimeSurfaceTree tree)
|
||||
{
|
||||
tree ??= new AetheriaRuntimeSurfaceTree(
|
||||
"",
|
||||
new AetheriaRuntimeSurfaceComponent(
|
||||
"",
|
||||
"surface",
|
||||
new System.Collections.Generic.Dictionary<string, string>(StringComparer.Ordinal),
|
||||
Array.Empty<AetheriaRuntimeSurfaceComponent>()),
|
||||
Array.Empty<AetheriaRuntimeSurfaceStyleToken>());
|
||||
|
||||
writer.WriteArrayHeader(3);
|
||||
writer.Write(tree.Id ?? "");
|
||||
WriteSurfaceComponent(ref writer, tree.Root);
|
||||
writer.WriteArrayHeader(tree.Styles?.Count ?? 0);
|
||||
foreach (var style in tree.Styles ?? Array.Empty<AetheriaRuntimeSurfaceStyleToken>())
|
||||
{
|
||||
writer.WriteArrayHeader(2);
|
||||
writer.Write(style.Name ?? "");
|
||||
writer.Write(style.Value ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteSurfaceComponent(ref MessagePackWriter writer, AetheriaRuntimeSurfaceComponent component)
|
||||
{
|
||||
component ??= new AetheriaRuntimeSurfaceComponent(
|
||||
"",
|
||||
"",
|
||||
new System.Collections.Generic.Dictionary<string, string>(StringComparer.Ordinal),
|
||||
Array.Empty<AetheriaRuntimeSurfaceComponent>());
|
||||
|
||||
writer.WriteArrayHeader(8);
|
||||
writer.Write(component.Id ?? "");
|
||||
writer.Write(component.Kind ?? "");
|
||||
writer.WriteMapHeader(component.Props?.Count ?? 0);
|
||||
foreach (var prop in component.Props ?? new System.Collections.Generic.Dictionary<string, string>(StringComparer.Ordinal))
|
||||
{
|
||||
writer.Write(prop.Key ?? "");
|
||||
writer.Write(prop.Value ?? "");
|
||||
}
|
||||
|
||||
writer.WriteArrayHeader(component.Children?.Count ?? 0);
|
||||
foreach (var child in component.Children ?? Array.Empty<AetheriaRuntimeSurfaceComponent>())
|
||||
{
|
||||
WriteSurfaceComponent(ref writer, child);
|
||||
}
|
||||
|
||||
writer.WriteArrayHeader(component.StateBindings?.Count ?? 0);
|
||||
foreach (var binding in component.StateBindings ?? Array.Empty<CultMeshStateBindingDescriptor>())
|
||||
{
|
||||
var record = CultMesh.StateBindingRecord(binding);
|
||||
writer.WriteArrayHeader(6);
|
||||
writer.Write(record.TargetProp);
|
||||
writer.Write(record.PointerId);
|
||||
writer.Write(record.SourceId);
|
||||
writer.Write(record.SchemaId);
|
||||
writer.Write(record.RouteKind);
|
||||
writer.Write(record.RouteDescription);
|
||||
}
|
||||
|
||||
writer.WriteArrayHeader(component.EmbeddedDocuments?.Count ?? 0);
|
||||
foreach (var slot in component.EmbeddedDocuments ?? Array.Empty<AetheriaRuntimeEmbeddedDocumentSlot>())
|
||||
{
|
||||
var route = slot.RouteHint ?? CultMeshRouteHint.Automatic;
|
||||
writer.WriteArrayHeader(6);
|
||||
writer.Write(slot.SlotId ?? "");
|
||||
writer.Write(slot.DocumentId ?? "");
|
||||
writer.Write(slot.SchemaId ?? "");
|
||||
writer.Write(slot.PresentationKind ?? "");
|
||||
writer.Write(route.Kind.ToString());
|
||||
writer.Write(route.Description ?? "");
|
||||
}
|
||||
|
||||
WriteStringMap(ref writer, component.Layout);
|
||||
WriteStringMap(ref writer, component.Style);
|
||||
}
|
||||
|
||||
private static void WriteSurfaceCommands(
|
||||
ref MessagePackWriter writer,
|
||||
System.Collections.Generic.IReadOnlyList<AetheriaRuntimeSurfaceCommandTemplate> commands)
|
||||
{
|
||||
writer.WriteArrayHeader(commands?.Count ?? 0);
|
||||
foreach (var command in commands ?? Array.Empty<AetheriaRuntimeSurfaceCommandTemplate>())
|
||||
{
|
||||
var record = CultMesh.OperationBindingRecord(command.Operation);
|
||||
writer.WriteArrayHeader(5);
|
||||
writer.Write(record.OperationId);
|
||||
writer.Write(record.Label);
|
||||
writer.Write(record.SchemaId);
|
||||
writer.Write(record.RouteKind);
|
||||
writer.Write(record.RouteDescription);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteStringMap(
|
||||
ref MessagePackWriter writer,
|
||||
System.Collections.Generic.IReadOnlyDictionary<string, string> values)
|
||||
{
|
||||
writer.WriteMapHeader(values?.Count ?? 0);
|
||||
foreach (var value in values ?? new System.Collections.Generic.Dictionary<string, string>(StringComparer.Ordinal))
|
||||
{
|
||||
writer.Write(value.Key ?? "");
|
||||
writer.Write(value.Value ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
internal static AetheriaRuntimeSurfaceDocument ReadRuntimeSurfaceDocument(byte[] payload)
|
||||
{
|
||||
var reader = new MessagePackReader(payload);
|
||||
var fields = reader.ReadArrayHeader();
|
||||
var providerId = ReadFieldString(ref reader, fields, 0, "");
|
||||
var providerKind = ReadFieldString(ref reader, fields, 1, "");
|
||||
var title = ReadFieldString(ref reader, fields, 2, "");
|
||||
var version = ReadFieldInt64(ref reader, fields, 3, 0);
|
||||
var updatedAtUtc = ReadFieldString(ref reader, fields, 4, "");
|
||||
var surface = ReadFieldSurfaceTree(ref reader, fields, 5);
|
||||
var commands = ReadFieldSurfaceCommands(ref reader, fields, 6);
|
||||
for (var index = 7; index < fields; index++)
|
||||
{
|
||||
reader.Skip();
|
||||
}
|
||||
|
||||
return new AetheriaRuntimeSurfaceDocument(
|
||||
providerId,
|
||||
providerKind,
|
||||
title,
|
||||
version,
|
||||
updatedAtUtc,
|
||||
surface,
|
||||
commands);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceTree ReadFieldSurfaceTree(
|
||||
ref MessagePackReader reader,
|
||||
int fields,
|
||||
int index)
|
||||
{
|
||||
if (index >= fields || reader.NextMessagePackType == MessagePackType.Nil)
|
||||
{
|
||||
if (index < fields)
|
||||
reader.ReadNil();
|
||||
|
||||
return new AetheriaRuntimeSurfaceTree(
|
||||
"",
|
||||
new AetheriaRuntimeSurfaceComponent(
|
||||
"",
|
||||
"surface",
|
||||
new System.Collections.Generic.Dictionary<string, string>(StringComparer.Ordinal),
|
||||
Array.Empty<AetheriaRuntimeSurfaceComponent>()),
|
||||
Array.Empty<AetheriaRuntimeSurfaceStyleToken>());
|
||||
}
|
||||
|
||||
var treeFields = reader.ReadArrayHeader();
|
||||
var id = ReadFieldString(ref reader, treeFields, 0, "");
|
||||
var root = ReadFieldSurfaceComponent(ref reader, treeFields, 1);
|
||||
var styles = ReadFieldSurfaceStyles(ref reader, treeFields, 2);
|
||||
for (var field = 3; field < treeFields; field++)
|
||||
{
|
||||
reader.Skip();
|
||||
}
|
||||
|
||||
return new AetheriaRuntimeSurfaceTree(id, root, styles);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent ReadFieldSurfaceComponent(
|
||||
ref MessagePackReader reader,
|
||||
int fields,
|
||||
int index)
|
||||
{
|
||||
if (index >= fields || reader.NextMessagePackType == MessagePackType.Nil)
|
||||
{
|
||||
if (index < fields)
|
||||
reader.ReadNil();
|
||||
|
||||
return new AetheriaRuntimeSurfaceComponent(
|
||||
"",
|
||||
"",
|
||||
new System.Collections.Generic.Dictionary<string, string>(StringComparer.Ordinal),
|
||||
Array.Empty<AetheriaRuntimeSurfaceComponent>());
|
||||
}
|
||||
|
||||
var componentFields = reader.ReadArrayHeader();
|
||||
var id = ReadFieldString(ref reader, componentFields, 0, "");
|
||||
var kind = ReadFieldString(ref reader, componentFields, 1, "");
|
||||
var props = ReadFieldStringMap(ref reader, componentFields, 2);
|
||||
var children = ReadFieldSurfaceComponents(ref reader, componentFields, 3);
|
||||
var stateBindings = ReadFieldSurfaceStateBindings(ref reader, componentFields, 4);
|
||||
var embeddedDocuments = ReadFieldEmbeddedDocumentSlots(ref reader, componentFields, 5);
|
||||
var layout = ReadFieldStringMap(ref reader, componentFields, 6);
|
||||
var style = ReadFieldStringMap(ref reader, componentFields, 7);
|
||||
for (var field = 8; field < componentFields; field++)
|
||||
{
|
||||
reader.Skip();
|
||||
}
|
||||
|
||||
return new AetheriaRuntimeSurfaceComponent(id, kind, props, children, stateBindings, embeddedDocuments, layout, style);
|
||||
}
|
||||
|
||||
private static System.Collections.Generic.IReadOnlyList<AetheriaRuntimeEmbeddedDocumentSlot> ReadFieldEmbeddedDocumentSlots(
|
||||
ref MessagePackReader reader,
|
||||
int fields,
|
||||
int index)
|
||||
{
|
||||
if (index >= fields || reader.NextMessagePackType == MessagePackType.Nil)
|
||||
{
|
||||
if (index < fields)
|
||||
reader.ReadNil();
|
||||
|
||||
return Array.Empty<AetheriaRuntimeEmbeddedDocumentSlot>();
|
||||
}
|
||||
|
||||
var count = reader.ReadArrayHeader();
|
||||
var slots = new AetheriaRuntimeEmbeddedDocumentSlot[count];
|
||||
for (var item = 0; item < count; item++)
|
||||
{
|
||||
var slotFields = reader.ReadArrayHeader();
|
||||
var slotId = ReadFieldString(ref reader, slotFields, 0, "");
|
||||
var documentId = ReadFieldString(ref reader, slotFields, 1, "");
|
||||
var schemaId = ReadFieldString(ref reader, slotFields, 2, "");
|
||||
var presentationKind = ReadFieldString(ref reader, slotFields, 3, "");
|
||||
var routeKind = ReadFieldString(ref reader, slotFields, 4, nameof(CultMeshLocalityKind.Automatic));
|
||||
var routeDescription = ReadFieldString(ref reader, slotFields, 5, "");
|
||||
for (var field = 6; field < slotFields; field++)
|
||||
{
|
||||
reader.Skip();
|
||||
}
|
||||
|
||||
slots[item] = new AetheriaRuntimeEmbeddedDocumentSlot(
|
||||
slotId,
|
||||
documentId,
|
||||
schemaId,
|
||||
presentationKind,
|
||||
new CultMeshRouteHint(
|
||||
Enum.TryParse<CultMeshLocalityKind>(routeKind, true, out var locality)
|
||||
? locality
|
||||
: CultMeshLocalityKind.Automatic,
|
||||
routeDescription));
|
||||
}
|
||||
|
||||
return slots;
|
||||
}
|
||||
|
||||
private static System.Collections.Generic.IReadOnlyList<CultMeshStateBindingDescriptor> ReadFieldSurfaceStateBindings(
|
||||
ref MessagePackReader reader,
|
||||
int fields,
|
||||
int index)
|
||||
{
|
||||
if (index >= fields || reader.NextMessagePackType == MessagePackType.Nil)
|
||||
{
|
||||
if (index < fields)
|
||||
reader.ReadNil();
|
||||
|
||||
return Array.Empty<CultMeshStateBindingDescriptor>();
|
||||
}
|
||||
|
||||
var count = reader.ReadArrayHeader();
|
||||
var bindings = new CultMeshStateBindingDescriptor[count];
|
||||
for (var item = 0; item < count; item++)
|
||||
{
|
||||
var bindingFields = reader.ReadArrayHeader();
|
||||
var targetProp = ReadFieldString(ref reader, bindingFields, 0, "value");
|
||||
var pointerId = ReadFieldString(ref reader, bindingFields, 1, "");
|
||||
var sourceId = ReadFieldString(ref reader, bindingFields, 2, "");
|
||||
var schemaId = ReadFieldString(ref reader, bindingFields, 3, "");
|
||||
var routeKind = ReadFieldString(ref reader, bindingFields, 4, "");
|
||||
var routeDescription = ReadFieldString(ref reader, bindingFields, 5, "");
|
||||
for (var field = 6; field < bindingFields; field++)
|
||||
{
|
||||
reader.Skip();
|
||||
}
|
||||
|
||||
bindings[item] = CultMesh.StateBindingRecord(
|
||||
targetProp,
|
||||
pointerId,
|
||||
sourceId,
|
||||
schemaId,
|
||||
routeKind,
|
||||
routeDescription).ToBinding();
|
||||
}
|
||||
|
||||
return bindings;
|
||||
}
|
||||
|
||||
private static System.Collections.Generic.IReadOnlyList<AetheriaRuntimeSurfaceComponent> ReadFieldSurfaceComponents(
|
||||
ref MessagePackReader reader,
|
||||
int fields,
|
||||
int index)
|
||||
{
|
||||
if (index >= fields || reader.NextMessagePackType == MessagePackType.Nil)
|
||||
{
|
||||
if (index < fields)
|
||||
reader.ReadNil();
|
||||
|
||||
return Array.Empty<AetheriaRuntimeSurfaceComponent>();
|
||||
}
|
||||
|
||||
var count = reader.ReadArrayHeader();
|
||||
var children = new AetheriaRuntimeSurfaceComponent[count];
|
||||
for (var child = 0; child < count; child++)
|
||||
{
|
||||
children[child] = ReadFieldSurfaceComponent(ref reader, 1, 0);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
private static System.Collections.Generic.IReadOnlyDictionary<string, string> ReadFieldStringMap(
|
||||
ref MessagePackReader reader,
|
||||
int fields,
|
||||
int index)
|
||||
{
|
||||
var values = new System.Collections.Generic.Dictionary<string, string>(StringComparer.Ordinal);
|
||||
if (index >= fields || reader.NextMessagePackType == MessagePackType.Nil)
|
||||
{
|
||||
if (index < fields)
|
||||
reader.ReadNil();
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
var count = reader.ReadMapHeader();
|
||||
for (var item = 0; item < count; item++)
|
||||
{
|
||||
values[reader.ReadString() ?? ""] = reader.ReadString() ?? "";
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private static System.Collections.Generic.IReadOnlyList<AetheriaRuntimeSurfaceStyleToken> ReadFieldSurfaceStyles(
|
||||
ref MessagePackReader reader,
|
||||
int fields,
|
||||
int index)
|
||||
{
|
||||
if (index >= fields || reader.NextMessagePackType == MessagePackType.Nil)
|
||||
{
|
||||
if (index < fields)
|
||||
reader.ReadNil();
|
||||
|
||||
return Array.Empty<AetheriaRuntimeSurfaceStyleToken>();
|
||||
}
|
||||
|
||||
var count = reader.ReadArrayHeader();
|
||||
var styles = new AetheriaRuntimeSurfaceStyleToken[count];
|
||||
for (var style = 0; style < count; style++)
|
||||
{
|
||||
var styleFields = reader.ReadArrayHeader();
|
||||
var name = ReadFieldString(ref reader, styleFields, 0, "");
|
||||
var value = ReadFieldString(ref reader, styleFields, 1, "");
|
||||
for (var field = 2; field < styleFields; field++)
|
||||
{
|
||||
reader.Skip();
|
||||
}
|
||||
|
||||
styles[style] = new AetheriaRuntimeSurfaceStyleToken(name, value);
|
||||
}
|
||||
|
||||
return styles;
|
||||
}
|
||||
|
||||
private static System.Collections.Generic.IReadOnlyList<AetheriaRuntimeSurfaceCommandTemplate> ReadFieldSurfaceCommands(
|
||||
ref MessagePackReader reader,
|
||||
int fields,
|
||||
int index)
|
||||
{
|
||||
if (index >= fields || reader.NextMessagePackType == MessagePackType.Nil)
|
||||
{
|
||||
if (index < fields)
|
||||
reader.ReadNil();
|
||||
|
||||
return Array.Empty<AetheriaRuntimeSurfaceCommandTemplate>();
|
||||
}
|
||||
|
||||
var count = reader.ReadArrayHeader();
|
||||
var commands = new AetheriaRuntimeSurfaceCommandTemplate[count];
|
||||
for (var command = 0; command < count; command++)
|
||||
{
|
||||
var commandFields = reader.ReadArrayHeader();
|
||||
var name = ReadFieldString(ref reader, commandFields, 0, "");
|
||||
var label = ReadFieldString(ref reader, commandFields, 1, "");
|
||||
var schemaId = ReadFieldString(ref reader, commandFields, 2, "");
|
||||
var routeKind = commandFields > 3
|
||||
? ReadFieldString(ref reader, commandFields, 3, "")
|
||||
: nameof(CultMeshLocalityKind.Automatic);
|
||||
var routeDescription = commandFields > 4
|
||||
? ReadFieldString(ref reader, commandFields, 4, "")
|
||||
: "";
|
||||
for (var field = 5; field < commandFields; field++)
|
||||
{
|
||||
reader.Skip();
|
||||
}
|
||||
|
||||
commands[command] = new AetheriaRuntimeSurfaceCommandTemplate(
|
||||
CultMesh.OperationBindingRecord(
|
||||
name,
|
||||
label,
|
||||
schemaId,
|
||||
routeKind,
|
||||
routeDescription).ToBinding());
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
private static string ReadFieldString(ref MessagePackReader reader, int fields, int index, string fallback)
|
||||
{
|
||||
return index >= fields ? fallback : reader.ReadString() ?? fallback;
|
||||
}
|
||||
|
||||
private static int ReadFieldInt32(ref MessagePackReader reader, int fields, int index, int fallback)
|
||||
{
|
||||
if (index >= fields)
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (reader.NextMessagePackType == MessagePackType.Nil)
|
||||
{
|
||||
reader.ReadNil();
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return reader.ReadInt32();
|
||||
}
|
||||
|
||||
private static long ReadFieldInt64(ref MessagePackReader reader, int fields, int index, long fallback)
|
||||
{
|
||||
if (index >= fields)
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (reader.NextMessagePackType == MessagePackType.Nil)
|
||||
{
|
||||
reader.ReadNil();
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return reader.ReadInt64();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 730416bfa990947479bc2eb704361bd8
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 89b7a1cbc494a264ba4130e65485ee5b
|
||||
|
|
@ -1,214 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public static class AetheriaRuntimeDaemonEditorSurfaceBuilder
|
||||
{
|
||||
public const string SurfaceId = "aetheria.daemon.editor";
|
||||
public const string TuiSurfaceId = "aetheria.daemon.editor.tui";
|
||||
|
||||
public static AetheriaRuntimeSurfaceDocument Build(
|
||||
AetheriaRuntimeDaemonProviderAdvertisementDocument provider,
|
||||
AetheriaRuntimeDaemonHealthDocument health,
|
||||
AetheriaRuntimeDaemonCommandBoundaryDocument commandBoundary,
|
||||
IReadOnlyList<AetheriaRuntimeSurfaceDocument>? designerSurfaces = null)
|
||||
{
|
||||
provider ??= new AetheriaRuntimeDaemonProviderAdvertisementDocument();
|
||||
health ??= new AetheriaRuntimeDaemonHealthDocument();
|
||||
commandBoundary ??= AetheriaRuntimeDaemonCommandBoundaryDocument.Create(provider.DaemonId);
|
||||
designerSurfaces ??= Array.Empty<AetheriaRuntimeSurfaceDocument>();
|
||||
|
||||
return new AetheriaRuntimeSurfaceDocument(
|
||||
providerId: AetheriaRuntimeProviderIdentity.ProviderId,
|
||||
providerKind: "editor.daemon",
|
||||
title: "Aetheria Daemon Editor",
|
||||
version: health.FrameId,
|
||||
updatedAtUtc: string.IsNullOrWhiteSpace(health.PublishedAtUtc)
|
||||
? provider.PublishedAtUtc
|
||||
: health.PublishedAtUtc,
|
||||
surface: new AetheriaRuntimeSurfaceTree(
|
||||
SurfaceId,
|
||||
Node(
|
||||
"aetheria.daemon.editor.root",
|
||||
"surface",
|
||||
Array.Empty<(string Key, string Value)>(),
|
||||
Node(
|
||||
"aetheria.daemon.editor.provider",
|
||||
"card",
|
||||
new[] { ("title", "Verse Provider") },
|
||||
Metric("aetheria.daemon.editor.provider.verse", "Verse", provider.VerseId),
|
||||
Metric("aetheria.daemon.editor.provider.provider", "Provider", provider.ProviderId),
|
||||
Metric("aetheria.daemon.editor.provider.daemon", "Daemon", provider.DaemonId),
|
||||
Metric("aetheria.daemon.editor.provider.transport", "CultMesh", provider.CultMeshAddress),
|
||||
Metric("aetheria.daemon.editor.provider.state", "State", provider.StateRecordRef)),
|
||||
Node(
|
||||
"aetheria.daemon.editor.records",
|
||||
"card",
|
||||
new[] { ("title", "Managed Records") },
|
||||
Metric("aetheria.daemon.editor.records.frame", "Frame", provider.FrameRecordRef),
|
||||
Metric("aetheria.daemon.editor.records.soa", "SoA", provider.SoaViewRecordRef),
|
||||
Metric("aetheria.daemon.editor.records.health", "Health", provider.HealthRecordRef),
|
||||
Metric("aetheria.daemon.editor.records.commands", "Command Boundary", provider.CommandBoundaryRecordRef),
|
||||
Metric("aetheria.daemon.editor.records.game", "Game Surface", provider.EveGuiSurfaceRecordRef),
|
||||
Metric("aetheria.daemon.editor.records.editor", "Editor Surface", provider.EditorGuiSurfaceRecordRef)),
|
||||
Node(
|
||||
"aetheria.daemon.editor.health",
|
||||
"card",
|
||||
new[] { ("title", "Health") },
|
||||
Metric("aetheria.daemon.editor.health.status", "Status", health.Status),
|
||||
Metric("aetheria.daemon.editor.health.frame", "Frame", health.FrameId.ToString(CultureInfo.InvariantCulture)),
|
||||
Metric("aetheria.daemon.editor.health.observed_commands", "Observed Commands", health.ObservedCommandCount.ToString(CultureInfo.InvariantCulture)),
|
||||
Metric("aetheria.daemon.editor.health.applied", "Applied", health.AppliedCommandCount.ToString(CultureInfo.InvariantCulture)),
|
||||
Metric("aetheria.daemon.editor.health.rejected", "Rejected", health.RejectedCommandCount.ToString(CultureInfo.InvariantCulture)),
|
||||
Metric("aetheria.daemon.editor.health.source", "Source", health.PublicationSource),
|
||||
Metric("aetheria.daemon.editor.health.transport", "Transport", health.Transport)),
|
||||
Node(
|
||||
"aetheria.daemon.editor.schemas",
|
||||
"card",
|
||||
new[] { ("title", "Published Schemas") },
|
||||
Row(
|
||||
"aetheria.daemon.editor.schemas.rows",
|
||||
provider.PublishedSchemas
|
||||
.Select((schema, index) => Metric(
|
||||
$"aetheria.daemon.editor.schemas.{index}",
|
||||
"Schema",
|
||||
schema))
|
||||
.ToArray())),
|
||||
Node(
|
||||
"aetheria.daemon.editor.commands",
|
||||
"card",
|
||||
new[] { ("title", "Typed Commands") },
|
||||
Metric(
|
||||
"aetheria.daemon.editor.commands.boundary",
|
||||
"Boundary",
|
||||
commandBoundary.BoundaryId),
|
||||
Metric(
|
||||
"aetheria.daemon.editor.commands.count",
|
||||
"Command Count",
|
||||
commandBoundary.Commands.Count.ToString(CultureInfo.InvariantCulture)),
|
||||
Row(
|
||||
"aetheria.daemon.editor.commands.rows",
|
||||
commandBoundary.Commands
|
||||
.Take(16)
|
||||
.Select((entry, index) => CommandRow(index, entry))
|
||||
.ToArray())),
|
||||
Node(
|
||||
"aetheria.daemon.editor.designer_surfaces",
|
||||
"card",
|
||||
new[] { ("title", "Designer Surfaces") },
|
||||
Row(
|
||||
"aetheria.daemon.editor.designer_surfaces.rows",
|
||||
designerSurfaces
|
||||
.Where(surface => surface != null)
|
||||
.Select((surface, index) => DesignerSurfaceRow(index, surface))
|
||||
.ToArray())),
|
||||
Node(
|
||||
"aetheria.daemon.editor.eve_surfaces",
|
||||
"card",
|
||||
new[] { ("title", "Advertised Eve Surfaces") },
|
||||
Row(
|
||||
"aetheria.daemon.editor.eve_surfaces.rows",
|
||||
provider.EveSurfaces
|
||||
.Where(surface => surface != null)
|
||||
.Select((surface, index) => AdvertisedSurfaceRow(index, surface))
|
||||
.ToArray()))),
|
||||
Array.Empty<AetheriaRuntimeSurfaceStyleToken>()),
|
||||
commands: commandBoundary.Commands
|
||||
.Where(entry => AetheriaRuntimeDaemonSurfaceCommandCatalog.IsArgumentlessCommand(entry.Kind))
|
||||
.Select(entry => new AetheriaRuntimeSurfaceCommandTemplate(
|
||||
AetheriaRuntimeDaemonSurfaceCommandCatalog.CommandName(entry.Kind),
|
||||
AetheriaRuntimeDaemonSurfaceCommandCatalog.Label(entry.Kind),
|
||||
"cultmesh"))
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent DesignerSurfaceRow(
|
||||
int index,
|
||||
AetheriaRuntimeSurfaceDocument surface)
|
||||
{
|
||||
return Node(
|
||||
$"aetheria.daemon.editor.designer_surfaces.{index}",
|
||||
"inspector.kv",
|
||||
new[]
|
||||
{
|
||||
("title", surface.Title),
|
||||
("surfaceId", surface.Surface.Id),
|
||||
("provider", surface.ProviderId),
|
||||
("kind", surface.ProviderKind),
|
||||
("commands", surface.Commands.Count.ToString(CultureInfo.InvariantCulture))
|
||||
});
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent AdvertisedSurfaceRow(
|
||||
int index,
|
||||
AetheriaRuntimeEveSurfaceAdvertisement surface)
|
||||
{
|
||||
return Node(
|
||||
$"aetheria.daemon.editor.eve_surfaces.{index}",
|
||||
"inspector.kv",
|
||||
new[]
|
||||
{
|
||||
("title", surface.Title),
|
||||
("surfaceId", surface.SurfaceId),
|
||||
("record", surface.RecordRef),
|
||||
("kind", surface.SurfaceKind),
|
||||
("projection", surface.WorldInteraction?.ProjectionKind ?? ""),
|
||||
("commandBoundary", surface.WorldInteraction?.CommandBoundary ?? ""),
|
||||
("status", surface.Status),
|
||||
("audience", surface.Audience),
|
||||
("provider", surface.ProviderId)
|
||||
});
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent CommandRow(
|
||||
int index,
|
||||
AetheriaRuntimeDaemonCommandBoundaryEntry entry)
|
||||
{
|
||||
return Node(
|
||||
$"aetheria.daemon.editor.commands.{index}",
|
||||
"inspector.kv",
|
||||
new[]
|
||||
{
|
||||
("kind", entry.Kind.ToString()),
|
||||
("body", entry.CommandBody),
|
||||
("authority", entry.Authority),
|
||||
("receipt", entry.Receipt)
|
||||
});
|
||||
}
|
||||
|
||||
private static string CommandName(AetheriaRuntimeDaemonCommandKinds kind)
|
||||
{
|
||||
return AetheriaRuntimeDaemonSurfaceCommandCatalog.CommandName(kind);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent Metric(string id, string label, string value)
|
||||
{
|
||||
return Node(id, "metric", new[] { ("label", label), ("value", value ?? "") });
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent Row(
|
||||
string id,
|
||||
params AetheriaRuntimeSurfaceComponent[] children)
|
||||
{
|
||||
return Node(id, "row", Array.Empty<(string Key, string Value)>(), children);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent Node(
|
||||
string id,
|
||||
string kind,
|
||||
IEnumerable<(string Key, string Value)> props,
|
||||
params AetheriaRuntimeSurfaceComponent[] children)
|
||||
{
|
||||
return new AetheriaRuntimeSurfaceComponent(
|
||||
id,
|
||||
kind,
|
||||
props.ToDictionary(prop => prop.Key, prop => prop.Value ?? "", StringComparer.Ordinal),
|
||||
children ?? Array.Empty<AetheriaRuntimeSurfaceComponent>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 17918ab9d67f6304c96767532bdf3d11
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 09207ef539b22e348a582894cc9396ec
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public sealed class AetheriaRuntimeDaemonIntentState
|
||||
{
|
||||
public List<AetheriaRuntimeDaemonMovementIntent> Movements { get; } =
|
||||
new List<AetheriaRuntimeDaemonMovementIntent>();
|
||||
public List<AetheriaRuntimeDaemonWeaponGroupIntent> WeaponGroups { get; } =
|
||||
new List<AetheriaRuntimeDaemonWeaponGroupIntent>();
|
||||
public List<AetheriaRuntimeDaemonBehaviorIntent> Behaviors { get; } =
|
||||
new List<AetheriaRuntimeDaemonBehaviorIntent>();
|
||||
public List<AetheriaRuntimeDaemonConsumableIntent> Consumables { get; } =
|
||||
new List<AetheriaRuntimeDaemonConsumableIntent>();
|
||||
public List<AetheriaRuntimeDaemonDockingIntent> Docking { get; } =
|
||||
new List<AetheriaRuntimeDaemonDockingIntent>();
|
||||
public List<AetheriaRuntimeDaemonWormholeIntent> Wormholes { get; } =
|
||||
new List<AetheriaRuntimeDaemonWormholeIntent>();
|
||||
public List<AetheriaRuntimeDaemonSensorPingIntent> SensorPings { get; } =
|
||||
new List<AetheriaRuntimeDaemonSensorPingIntent>();
|
||||
|
||||
public bool HasAny =>
|
||||
Movements.Count > 0 ||
|
||||
SensorPings.Count > 0 ||
|
||||
WeaponGroups.Count > 0 ||
|
||||
Behaviors.Count > 0 ||
|
||||
Consumables.Count > 0 ||
|
||||
Docking.Count > 0 ||
|
||||
Wormholes.Count > 0;
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimeDaemonMovementIntent
|
||||
{
|
||||
public string ActorEntityKey { get; set; } = "";
|
||||
public double DirectionX { get; set; }
|
||||
public double DirectionY { get; set; }
|
||||
public double Magnitude { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimeDaemonWeaponGroupIntent
|
||||
{
|
||||
public string ActorEntityKey { get; set; } = "";
|
||||
public int WeaponGroup { get; set; } = -1;
|
||||
public bool Fire { get; set; }
|
||||
public bool Active { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimeDaemonSensorPingIntent
|
||||
{
|
||||
public string ActorEntityKey { get; set; } = "";
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimeDaemonBehaviorIntent
|
||||
{
|
||||
public string ActorEntityKey { get; set; } = "";
|
||||
public int EquipmentIndex { get; set; } = -1;
|
||||
public int BehaviorIndex { get; set; } = -1;
|
||||
public bool Active { get; set; }
|
||||
public string TargetBodyKey { get; set; } = "";
|
||||
public int TargetAsteroidIndex { get; set; } = -1;
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimeDaemonConsumableIntent
|
||||
{
|
||||
public string ActorEntityKey { get; set; } = "";
|
||||
public string ItemKey { get; set; } = "";
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimeDaemonDockingIntent
|
||||
{
|
||||
public string ActorEntityKey { get; set; } = "";
|
||||
public string TargetEntityKey { get; set; } = "";
|
||||
public bool Dock { get; set; }
|
||||
public bool Undock { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimeDaemonWormholeIntent
|
||||
{
|
||||
public string ActorEntityKey { get; set; } = "";
|
||||
public int SourceZoneIndex { get; set; } = -1;
|
||||
public int TargetZoneIndex { get; set; } = -1;
|
||||
public double EntryWormholeX { get; set; }
|
||||
public double EntryWormholeZ { get; set; }
|
||||
public double ExitWormholeX { get; set; }
|
||||
public double ExitWormholeZ { get; set; }
|
||||
public string CommandId { get; set; } = "";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f16b722f9a3746340af404d50b13cc66
|
||||
|
|
@ -1,366 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public readonly struct AetheriaRuntimeDaemonItemStatValue
|
||||
{
|
||||
public AetheriaRuntimeDaemonItemStatValue(
|
||||
string itemKey,
|
||||
string behaviorKind,
|
||||
int behaviorGroup,
|
||||
int fieldKey,
|
||||
string label,
|
||||
double value)
|
||||
{
|
||||
ItemKey = itemKey ?? "";
|
||||
BehaviorKind = behaviorKind ?? "";
|
||||
BehaviorGroup = behaviorGroup;
|
||||
FieldKey = fieldKey;
|
||||
Label = label ?? "";
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public string ItemKey { get; }
|
||||
public string BehaviorKind { get; }
|
||||
public int BehaviorGroup { get; }
|
||||
public int FieldKey { get; }
|
||||
public string Label { get; }
|
||||
public double Value { get; }
|
||||
public string ValueRef => AetheriaRuntimeDaemonItemStatQueries.ItemStatRef(
|
||||
ItemKey,
|
||||
BehaviorKind,
|
||||
BehaviorGroup,
|
||||
FieldKey);
|
||||
}
|
||||
|
||||
public static class AetheriaRuntimeDaemonItemStatQueries
|
||||
{
|
||||
public const string StateRefPrefix = "aetheria.state/items";
|
||||
|
||||
public static string ItemStatRef(
|
||||
string itemKey,
|
||||
string behaviorKind,
|
||||
int behaviorGroup,
|
||||
int fieldKey)
|
||||
{
|
||||
return $"{StateRefPrefix}/{Token(itemKey)}/behaviors/{Token(behaviorKind)}/{behaviorGroup}/stats/{fieldKey}";
|
||||
}
|
||||
|
||||
public static bool TryReadItemStatRef(
|
||||
string stateRef,
|
||||
out string itemKey,
|
||||
out string behaviorKind,
|
||||
out int behaviorGroup,
|
||||
out int fieldKey)
|
||||
{
|
||||
itemKey = "";
|
||||
behaviorKind = "";
|
||||
behaviorGroup = -1;
|
||||
fieldKey = -1;
|
||||
|
||||
var parts = (stateRef ?? "").Split('/');
|
||||
if (parts.Length != 8 ||
|
||||
!string.Equals(parts[0], "aetheria.state", StringComparison.Ordinal) ||
|
||||
!string.Equals(parts[1], "items", StringComparison.Ordinal) ||
|
||||
!string.Equals(parts[3], "behaviors", StringComparison.Ordinal) ||
|
||||
!string.Equals(parts[6], "stats", StringComparison.Ordinal) ||
|
||||
!int.TryParse(parts[5], NumberStyles.Integer, CultureInfo.InvariantCulture, out behaviorGroup) ||
|
||||
!int.TryParse(parts[7], NumberStyles.Integer, CultureInfo.InvariantCulture, out fieldKey))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
itemKey = DecodeToken(parts[2]);
|
||||
behaviorKind = DecodeToken(parts[4]);
|
||||
return !string.IsNullOrWhiteSpace(itemKey) && !string.IsNullOrWhiteSpace(behaviorKind);
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeStatRecipePreviewState ConditionsFor(
|
||||
AetheriaRuntimeLoadoutItemCommit? item,
|
||||
double heat = 0,
|
||||
double charge = 1,
|
||||
double ammo = 1,
|
||||
double range = 1,
|
||||
double integrity = 1,
|
||||
double pilotSkill = 0,
|
||||
double environment = 0)
|
||||
{
|
||||
return new AetheriaRuntimeStatRecipePreviewState(
|
||||
quality: Clamp01(item?.Quality ?? 1),
|
||||
durability: Clamp01(item?.Durability ?? 1),
|
||||
heat: Clamp01(heat),
|
||||
charge: Clamp01(charge),
|
||||
ammo: Clamp01(ammo),
|
||||
range: Clamp01(range),
|
||||
integrity: Clamp01(integrity),
|
||||
pilotSkill: Clamp01(pilotSkill),
|
||||
environment: Clamp01(environment));
|
||||
}
|
||||
|
||||
public static double EvaluatePerformanceStat(
|
||||
AetheriaRuntimeBehaviorValue? value,
|
||||
AetheriaRuntimeLoadoutItemCommit? item,
|
||||
double heat = 0)
|
||||
{
|
||||
return EvaluatePerformanceStat(
|
||||
ReadPerformanceStat(value),
|
||||
ConditionsFor(item, heat));
|
||||
}
|
||||
|
||||
public static double EvaluatePerformanceStat(
|
||||
AetheriaRuntimeBehaviorValue? value,
|
||||
AetheriaRuntimeStatRecipePreviewState conditions)
|
||||
{
|
||||
return EvaluatePerformanceStat(ReadPerformanceStat(value), conditions);
|
||||
}
|
||||
|
||||
public static double EvaluateConsumablePerformanceStat(
|
||||
AetheriaRuntimeBehaviorValue? value,
|
||||
double quality,
|
||||
double effectiveness)
|
||||
{
|
||||
var stat = ReadPerformanceStat(value);
|
||||
var weightedQuality = stat.QualityExponent == 0
|
||||
? 1
|
||||
: Math.Pow(Clamp01(quality), stat.QualityExponent);
|
||||
var result = Lerp(stat.Min, stat.Max, Clamp01(effectiveness) * weightedQuality);
|
||||
return double.IsNaN(result) ? stat.Min : result;
|
||||
}
|
||||
|
||||
public static double SampleCurve(IReadOnlyList<AetheriaRuntimeCurveKey>? keys, double value)
|
||||
{
|
||||
if (keys == null || keys.Count == 0)
|
||||
return Clamp01(value);
|
||||
|
||||
var ordered = keys.OrderBy(key => key.Time).ToArray();
|
||||
if (value <= ordered[0].Time)
|
||||
return Clamp01(ordered[0].Value);
|
||||
|
||||
for (var index = 1; index < ordered.Length; index++)
|
||||
{
|
||||
var next = ordered[index];
|
||||
var previous = ordered[index - 1];
|
||||
if (value > next.Time)
|
||||
continue;
|
||||
|
||||
var span = next.Time - previous.Time;
|
||||
var t = span <= double.Epsilon ? 1 : Clamp01((value - previous.Time) / span);
|
||||
var t2 = t * t;
|
||||
var t3 = t2 * t;
|
||||
var h00 = 2 * t3 - 3 * t2 + 1;
|
||||
var h10 = t3 - 2 * t2 + t;
|
||||
var h01 = -2 * t3 + 3 * t2;
|
||||
var h11 = t3 - t2;
|
||||
return Clamp01(
|
||||
h00 * previous.Value +
|
||||
h10 * span * previous.OutTangent +
|
||||
h01 * next.Value +
|
||||
h11 * span * next.InTangent);
|
||||
}
|
||||
|
||||
return Clamp01(ordered[ordered.Length - 1].Value);
|
||||
}
|
||||
|
||||
public static double EvaluatePerformanceStat(
|
||||
AetheriaRuntimePerformanceStat? stat,
|
||||
AetheriaRuntimeStatRecipePreviewState conditions)
|
||||
{
|
||||
if (stat == null)
|
||||
return 0;
|
||||
|
||||
conditions ??= AetheriaRuntimeStatRecipePreviewState.Default;
|
||||
if (stat.Recipe != null)
|
||||
return EvaluateRecipe(stat.Recipe, conditions);
|
||||
|
||||
var heat = stat.HeatExponentMultiplier == 0
|
||||
? 1
|
||||
: Math.Pow(Clamp01(conditions.Heat), stat.HeatExponentMultiplier);
|
||||
var durability = stat.DurabilityExponentMultiplier == 0
|
||||
? 1
|
||||
: Math.Pow(Clamp01(conditions.Durability), stat.DurabilityExponentMultiplier);
|
||||
var quality = stat.QualityExponent == 0
|
||||
? 1
|
||||
: Math.Pow(Clamp01(conditions.Quality), stat.QualityExponent);
|
||||
|
||||
return Lerp(stat.Min, stat.Max, durability * quality * heat);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<AetheriaRuntimeDaemonItemStatValue> QueryPerformanceStats(
|
||||
AetheriaRuntimeCatalogItem? catalogItem,
|
||||
AetheriaRuntimeLoadoutItemCommit? item,
|
||||
double heat = 0)
|
||||
{
|
||||
if (catalogItem?.BehaviorPayloads == null || catalogItem.BehaviorPayloads.Count == 0)
|
||||
return Array.Empty<AetheriaRuntimeDaemonItemStatValue>();
|
||||
|
||||
var conditions = ConditionsFor(item, heat);
|
||||
var stats = new List<AetheriaRuntimeDaemonItemStatValue>();
|
||||
foreach (var behavior in catalogItem.BehaviorPayloads)
|
||||
{
|
||||
var metadata = AetheriaRuntimeBehaviorMetadataCatalog.Get(behavior.Kind);
|
||||
foreach (var field in behavior.Fields ?? Array.Empty<AetheriaRuntimeBehaviorField>())
|
||||
{
|
||||
var fieldMetadata = metadata?.DisplayFields.FirstOrDefault(candidate => candidate.Key == field.Key);
|
||||
if (fieldMetadata?.ValueKind != AetheriaRuntimeBehaviorFieldValueKind.PerformanceStat)
|
||||
continue;
|
||||
|
||||
stats.Add(new AetheriaRuntimeDaemonItemStatValue(
|
||||
item?.ItemKey ?? catalogItem.ItemKey,
|
||||
behavior.Kind,
|
||||
behavior.Group,
|
||||
field.Key,
|
||||
fieldMetadata.Name,
|
||||
EvaluatePerformanceStat(ReadPerformanceStat(field.Value), conditions)));
|
||||
}
|
||||
}
|
||||
|
||||
return stats.Count == 0 ? Array.Empty<AetheriaRuntimeDaemonItemStatValue>() : stats;
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeLoadoutItemCommit ItemCommit(
|
||||
string itemKey,
|
||||
double quality,
|
||||
double durability,
|
||||
int quantity = 1,
|
||||
bool enabled = true,
|
||||
bool overrideShutdown = false,
|
||||
double temperature = 0)
|
||||
{
|
||||
return new AetheriaRuntimeLoadoutItemCommit
|
||||
{
|
||||
ItemKey = itemKey ?? "",
|
||||
Quality = quality,
|
||||
Durability = durability,
|
||||
Quantity = quantity,
|
||||
Enabled = enabled,
|
||||
OverrideShutdown = overrideShutdown,
|
||||
Temperature = temperature
|
||||
};
|
||||
}
|
||||
|
||||
private static AetheriaRuntimePerformanceStat ReadPerformanceStat(AetheriaRuntimeBehaviorValue? value)
|
||||
{
|
||||
return new AetheriaRuntimePerformanceStat(
|
||||
ChildNumber(value, 0),
|
||||
ChildNumber(value, 1),
|
||||
ChildNumber(value, 2),
|
||||
ChildNumber(value, 3),
|
||||
ChildNumber(value, 4),
|
||||
ReadStatRecipe(ChildValue(value, 5)));
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeStatRecipe? ReadStatRecipe(AetheriaRuntimeBehaviorValue? value)
|
||||
{
|
||||
if (value == null || value.Children.Count == 0)
|
||||
return null;
|
||||
|
||||
var modifiers = ChildValue(value, 1)?.Children
|
||||
.Select(ReadStatRecipeModifier)
|
||||
.Where(modifier => modifier != null)
|
||||
.Cast<AetheriaRuntimeStatRecipeModifier>()
|
||||
.ToArray() ?? Array.Empty<AetheriaRuntimeStatRecipeModifier>();
|
||||
|
||||
return new AetheriaRuntimeStatRecipe(ChildNumber(value, 0), modifiers);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeStatRecipeModifier? ReadStatRecipeModifier(AetheriaRuntimeBehaviorValue? value)
|
||||
{
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
return new AetheriaRuntimeStatRecipeModifier(
|
||||
ChildString(value, 0),
|
||||
ChildString(value, 1),
|
||||
ChildNumber(value, 2),
|
||||
ReadCurveKeys(ChildValue(value, 3)),
|
||||
value.Children.Count <= 4 || ChildValue(value, 4)?.BoolValue == true);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AetheriaRuntimeCurveKey> ReadCurveKeys(AetheriaRuntimeBehaviorValue? value)
|
||||
{
|
||||
if (value?.Children == null || value.Children.Count == 0)
|
||||
return Array.Empty<AetheriaRuntimeCurveKey>();
|
||||
|
||||
return value.Children
|
||||
.Where(key => key.Children.Count >= 4)
|
||||
.Select(key => new AetheriaRuntimeCurveKey(
|
||||
ChildNumber(key, 0),
|
||||
ChildNumber(key, 1),
|
||||
ChildNumber(key, 2),
|
||||
ChildNumber(key, 3)))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static double EvaluateRecipe(
|
||||
AetheriaRuntimeStatRecipe recipe,
|
||||
AetheriaRuntimeStatRecipePreviewState conditions)
|
||||
{
|
||||
var value = recipe.BaseValue;
|
||||
foreach (var modifier in recipe.Modifiers ?? Array.Empty<AetheriaRuntimeStatRecipeModifier>())
|
||||
{
|
||||
if (modifier == null || !modifier.Enabled || string.IsNullOrWhiteSpace(modifier.Condition))
|
||||
continue;
|
||||
|
||||
var sample = SampleCurve(modifier.CurveKeys, conditions.GetConditionValue(modifier.Condition));
|
||||
switch (modifier.Operation ?? "")
|
||||
{
|
||||
case AetheriaRuntimeStatRecipeOperations.Multiply:
|
||||
value *= 1 + ((modifier.Amount - 1) * sample);
|
||||
break;
|
||||
case AetheriaRuntimeStatRecipeOperations.Override:
|
||||
value = Lerp(value, modifier.Amount, sample);
|
||||
break;
|
||||
default:
|
||||
value += modifier.Amount * sample;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static double ChildNumber(AetheriaRuntimeBehaviorValue? value, int index)
|
||||
{
|
||||
return ChildValue(value, index)?.NumberValue ?? 0;
|
||||
}
|
||||
|
||||
private static string ChildString(AetheriaRuntimeBehaviorValue? value, int index)
|
||||
{
|
||||
return ChildValue(value, index)?.StringValue ?? "";
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorValue? ChildValue(AetheriaRuntimeBehaviorValue? value, int index)
|
||||
{
|
||||
return value != null && value.Children.Count > index ? value.Children[index] : null;
|
||||
}
|
||||
|
||||
private static double Clamp01(double value)
|
||||
{
|
||||
if (value < 0)
|
||||
return 0;
|
||||
return value > 1 ? 1 : value;
|
||||
}
|
||||
|
||||
private static double Lerp(double from, double to, double t)
|
||||
{
|
||||
return from + ((to - from) * t);
|
||||
}
|
||||
|
||||
private static string Token(string value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value)
|
||||
? "_"
|
||||
: value.Trim().Replace("/", "%2F");
|
||||
}
|
||||
|
||||
private static string DecodeToken(string value)
|
||||
{
|
||||
return (value ?? "").Replace("%2F", "/", StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 740ed3acb051bb549b5f82a57eb31e3a
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
using System;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public sealed class AetheriaRuntimeDaemonObservationResult
|
||||
{
|
||||
public AetheriaRuntimeDaemonObservationResult(
|
||||
bool observed,
|
||||
bool frameChanged,
|
||||
bool soaViewChanged,
|
||||
long frameId,
|
||||
long soaGeneration,
|
||||
bool isAuthoritative)
|
||||
{
|
||||
Observed = observed;
|
||||
FrameChanged = frameChanged;
|
||||
SoaViewChanged = soaViewChanged;
|
||||
FrameId = frameId;
|
||||
SoaGeneration = soaGeneration;
|
||||
IsAuthoritative = isAuthoritative;
|
||||
}
|
||||
|
||||
public bool Observed { get; }
|
||||
public bool FrameChanged { get; }
|
||||
public bool SoaViewChanged { get; }
|
||||
public bool Changed => FrameChanged || SoaViewChanged;
|
||||
public long FrameId { get; }
|
||||
public long SoaGeneration { get; }
|
||||
public bool IsAuthoritative { get; }
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimeDaemonObservationCursor
|
||||
{
|
||||
public long LastFrameId { get; private set; } = -1;
|
||||
public long LastSoaGeneration { get; private set; } = -1;
|
||||
public string LastDaemonId { get; private set; } = "";
|
||||
public string LastSessionId { get; private set; } = "";
|
||||
|
||||
public AetheriaRuntimeDaemonObservationResult Observe(AetheriaRuntimeDaemonRenderView? observed)
|
||||
{
|
||||
if (observed == null)
|
||||
{
|
||||
return new AetheriaRuntimeDaemonObservationResult(
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
LastFrameId,
|
||||
LastSoaGeneration,
|
||||
false);
|
||||
}
|
||||
|
||||
var frame = observed.Frame;
|
||||
var soaGeneration = observed.SoaView?.Generation ?? -1;
|
||||
var frameChanged = frame.FrameId != LastFrameId ||
|
||||
!string.Equals(frame.DaemonId, LastDaemonId, StringComparison.Ordinal) ||
|
||||
!string.Equals(frame.SessionId, LastSessionId, StringComparison.Ordinal);
|
||||
var soaViewChanged = soaGeneration != LastSoaGeneration;
|
||||
|
||||
LastFrameId = frame.FrameId;
|
||||
LastSoaGeneration = soaGeneration;
|
||||
LastDaemonId = frame.DaemonId ?? "";
|
||||
LastSessionId = frame.SessionId ?? "";
|
||||
|
||||
return new AetheriaRuntimeDaemonObservationResult(
|
||||
true,
|
||||
frameChanged,
|
||||
soaViewChanged,
|
||||
frame.FrameId,
|
||||
soaGeneration,
|
||||
frame.IsAuthoritative);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
LastFrameId = -1;
|
||||
LastSoaGeneration = -1;
|
||||
LastDaemonId = "";
|
||||
LastSessionId = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bd46bb16aafbc684fa18d590767c9f9f
|
||||
|
|
@ -1,600 +0,0 @@
|
|||
using System;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
internal sealed class AetheriaRuntimeDaemonOperationClient
|
||||
{
|
||||
public const string DefaultClientId = "aetheria-daemon-client";
|
||||
private readonly Func<AetheriaRuntimeDaemonCommandDocument, AetheriaRuntimeDaemonCommandEnvelope>? _submit;
|
||||
|
||||
internal AetheriaRuntimeDaemonOperationClient(
|
||||
string stateFilePath,
|
||||
string clientId = DefaultClientId,
|
||||
string sessionId = "local")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(stateFilePath))
|
||||
throw new ArgumentException("State file path must be non-empty.", nameof(stateFilePath));
|
||||
|
||||
StateFilePath = stateFilePath;
|
||||
ClientId = string.IsNullOrWhiteSpace(clientId) ? DefaultClientId : clientId;
|
||||
SessionId = string.IsNullOrWhiteSpace(sessionId) ? "local" : sessionId;
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonOperationClient(
|
||||
string stateFilePath,
|
||||
string clientId,
|
||||
string sessionId,
|
||||
Func<AetheriaRuntimeDaemonCommandDocument, AetheriaRuntimeDaemonCommandEnvelope> submit)
|
||||
: this(stateFilePath, clientId, sessionId)
|
||||
{
|
||||
_submit = submit ?? throw new ArgumentNullException(nameof(submit));
|
||||
}
|
||||
|
||||
public string StateFilePath { get; }
|
||||
public string ClientId { get; }
|
||||
public string SessionId { get; }
|
||||
|
||||
private AetheriaRuntimeDaemonCommandEnvelope Send(AetheriaRuntimeDaemonCommandDocument command)
|
||||
{
|
||||
if (_submit != null)
|
||||
return _submit(command);
|
||||
|
||||
if (!TrySend(command, out var envelope, out var error))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to submit Aetheria daemon operation {command.Kind}: {error}");
|
||||
}
|
||||
|
||||
return envelope!;
|
||||
}
|
||||
|
||||
private bool TrySend(
|
||||
AetheriaRuntimeDaemonCommandDocument command,
|
||||
out AetheriaRuntimeDaemonCommandEnvelope? envelope,
|
||||
out string error)
|
||||
{
|
||||
envelope = null;
|
||||
error = "";
|
||||
|
||||
try
|
||||
{
|
||||
using var client = AetheriaClient
|
||||
.OpenAsync(StateFilePath, ClientId, startServer: false, pullOnOpen: true)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
envelope = client
|
||||
.SubmitDaemonCommandDocument(command);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.ToString();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private AetheriaRuntimeDaemonCommandDocument Create(
|
||||
AetheriaRuntimeDaemonCommandKinds kind,
|
||||
AetheriaRuntimeDaemonFrameDocument? frame)
|
||||
{
|
||||
return AetheriaRuntimeDaemonCommandDocument.Create(
|
||||
kind,
|
||||
ClientId,
|
||||
frame?.SessionId ?? SessionId,
|
||||
frame?.FrameId ?? -1,
|
||||
frame?.Run.CurrentEntityKey ?? "");
|
||||
}
|
||||
|
||||
internal static AetheriaRuntimeDaemonCommandEnvelope ToEnvelope(AetheriaRuntimeDaemonCommandDocument command)
|
||||
{
|
||||
return new AetheriaRuntimeDaemonCommandEnvelope(
|
||||
command.Schema ?? "",
|
||||
command.CommandId ?? "",
|
||||
command.ClientId ?? "",
|
||||
command.IssuedAtUtc ?? "",
|
||||
command.SessionId ?? "",
|
||||
command.ObservedFrameId,
|
||||
command.Kind,
|
||||
command.ActorEntityKey ?? "",
|
||||
"");
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope SetTarget(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
string targetEntityKey)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.SetTarget, frame);
|
||||
command.TargetEntityKey = targetEntityKey ?? "";
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope ClearTarget(AetheriaRuntimeDaemonFrameDocument? frame)
|
||||
{
|
||||
return Send(Create(AetheriaRuntimeDaemonCommandKinds.ClearTarget, frame));
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope TargetNearest(AetheriaRuntimeDaemonFrameDocument? frame)
|
||||
{
|
||||
return Send(Create(AetheriaRuntimeDaemonCommandKinds.TargetNearest, frame));
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope TargetNext(AetheriaRuntimeDaemonFrameDocument? frame)
|
||||
{
|
||||
return Send(Create(AetheriaRuntimeDaemonCommandKinds.TargetNext, frame));
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope TargetPrevious(AetheriaRuntimeDaemonFrameDocument? frame)
|
||||
{
|
||||
return Send(Create(AetheriaRuntimeDaemonCommandKinds.TargetPrevious, frame));
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope TargetReticle(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
double directionX,
|
||||
double directionY,
|
||||
double directionZ)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.TargetReticle, frame);
|
||||
command.DirectionX = directionX;
|
||||
command.DirectionY = directionY;
|
||||
command.PositionZ = directionZ;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope SetMoveVector(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
double directionX,
|
||||
double directionY,
|
||||
double scalarValue = 1.0)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.SetMoveVector, frame);
|
||||
command.DirectionX = directionX;
|
||||
command.DirectionY = directionY;
|
||||
command.ScalarValue = scalarValue;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope SetSimulationRate(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
double rate)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.SetSimulationRate, frame);
|
||||
command.ScalarValue = rate;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope AdvanceSimulationStep(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame)
|
||||
{
|
||||
return Send(Create(AetheriaRuntimeDaemonCommandKinds.AdvanceSimulationStep, frame));
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope SetLookDirection(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
double directionX,
|
||||
double directionY,
|
||||
double directionZ)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.SetLookDirection, frame);
|
||||
command.DirectionX = directionX;
|
||||
command.DirectionY = directionY;
|
||||
command.PositionZ = directionZ;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope SetTractorPower(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
double power)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.SetTractorPower, frame);
|
||||
command.ScalarValue = power;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope FireWeaponGroup(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
int weaponGroup)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.FireWeaponGroup, frame);
|
||||
command.WeaponGroup = weaponGroup;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope SetWeaponGroupActive(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
int weaponGroup,
|
||||
bool active)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.SetWeaponGroupActive, frame);
|
||||
command.WeaponGroup = weaponGroup;
|
||||
command.ScalarValue = active ? 1.0 : 0.0;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope SetWeaponGroupMembership(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
string targetEntityKey,
|
||||
int equipmentIndex,
|
||||
int weaponGroup,
|
||||
bool assigned)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.SetWeaponGroupMembership, frame);
|
||||
command.TargetEntityKey = targetEntityKey ?? "";
|
||||
command.EquipmentIndex = equipmentIndex;
|
||||
command.WeaponGroup = weaponGroup;
|
||||
command.ScalarValue = assigned ? 1.0 : 0.0;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope SetBehaviorActive(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
int equipmentIndex,
|
||||
int behaviorIndex,
|
||||
bool active)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.SetBehaviorActive, frame);
|
||||
command.EquipmentIndex = equipmentIndex;
|
||||
command.BehaviorIndex = behaviorIndex;
|
||||
command.ScalarValue = active ? 1.0 : 0.0;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope ActivateConsumable(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
string itemKey)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.ActivateConsumable, frame);
|
||||
command.TextValue = itemKey ?? "";
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope SensorPing(AetheriaRuntimeDaemonFrameDocument? frame)
|
||||
{
|
||||
return Send(Create(AetheriaRuntimeDaemonCommandKinds.SensorPing, frame));
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope SetHeatsinksEnabled(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
bool enabled)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.SetHeatsinksEnabled, frame);
|
||||
command.ScalarValue = enabled ? 1.0 : 0.0;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope SetOverrideShutdown(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
bool enabled)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.SetOverrideShutdown, frame);
|
||||
command.ScalarValue = enabled ? 1.0 : 0.0;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope SetOverrideShutdown(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
string targetEntityKey,
|
||||
bool enabled)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.SetOverrideShutdown, frame);
|
||||
command.TargetEntityKey = targetEntityKey ?? "";
|
||||
command.ScalarValue = enabled ? 1.0 : 0.0;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope SetItemEnabled(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
int equipmentIndex,
|
||||
bool enabled)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.SetItemEnabled, frame);
|
||||
command.EquipmentIndex = equipmentIndex;
|
||||
command.ScalarValue = enabled ? 1.0 : 0.0;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope ToggleShieldEnabled(AetheriaRuntimeDaemonFrameDocument? frame)
|
||||
{
|
||||
return Send(Create(AetheriaRuntimeDaemonCommandKinds.ToggleShieldEnabled, frame));
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope SetItemOverrideShutdown(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
string targetEntityKey,
|
||||
int equipmentIndex,
|
||||
bool enabled)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.SetItemOverrideShutdown, frame);
|
||||
command.TargetEntityKey = targetEntityKey ?? "";
|
||||
command.EquipmentIndex = equipmentIndex;
|
||||
command.ScalarValue = enabled ? 1.0 : 0.0;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope SetThermotoggleTargetTemperature(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
string targetEntityKey,
|
||||
int equipmentIndex,
|
||||
int behaviorIndex,
|
||||
double targetTemperature)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.SetThermotoggleTargetTemperature, frame);
|
||||
command.TargetEntityKey = targetEntityKey ?? "";
|
||||
command.EquipmentIndex = equipmentIndex;
|
||||
command.BehaviorIndex = behaviorIndex;
|
||||
command.ScalarValue = targetTemperature;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope SetShutdownPerformance(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
string targetEntityKey,
|
||||
double shutdownPerformance)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.SetShutdownPerformance, frame);
|
||||
command.TargetEntityKey = targetEntityKey ?? "";
|
||||
command.ScalarValue = shutdownPerformance;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope ToggleHullConductivity(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
string targetEntityKey,
|
||||
int x,
|
||||
int y,
|
||||
int axis)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.ToggleHullConductivity, frame);
|
||||
command.TargetEntityKey = targetEntityKey ?? "";
|
||||
command.PositionX = x;
|
||||
command.PositionY = y;
|
||||
command.ScalarValue = axis;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope SetEntityName(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
string targetEntityKey,
|
||||
string name)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.SetEntityName, frame);
|
||||
command.TargetEntityKey = targetEntityKey ?? "";
|
||||
command.TextValue = name ?? "";
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope Dock(AetheriaRuntimeDaemonFrameDocument? frame, string targetEntityKey)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.Dock, frame);
|
||||
command.TargetEntityKey = targetEntityKey ?? "";
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope DockNearest(AetheriaRuntimeDaemonFrameDocument? frame)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.DockNearest, frame);
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope Undock(AetheriaRuntimeDaemonFrameDocument? frame)
|
||||
{
|
||||
return Send(Create(AetheriaRuntimeDaemonCommandKinds.Undock, frame));
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope Interact(AetheriaRuntimeDaemonFrameDocument? frame)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.Interact, frame);
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope SetDockedCurrentShip(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
string targetEntityKey)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.SetDockedCurrentShip, frame);
|
||||
command.TargetEntityKey = targetEntityKey ?? "";
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope EnterWormhole(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
int targetZoneIndex,
|
||||
double positionX,
|
||||
double positionY)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.EnterWormhole, frame);
|
||||
command.TargetZoneIndex = targetZoneIndex;
|
||||
command.PositionX = positionX;
|
||||
command.PositionY = positionY;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope TowToStation(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
string stationEntityKey)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.TowToStation, frame);
|
||||
command.TargetEntityKey = stationEntityKey ?? "";
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope TransferCargoItem(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
string originEntityKey,
|
||||
int originCargoIndex,
|
||||
string destinationEntityKey,
|
||||
int destinationCargoIndex,
|
||||
string itemKey,
|
||||
int quantity,
|
||||
int sourceX,
|
||||
int sourceY,
|
||||
int destinationX,
|
||||
int destinationY,
|
||||
bool hasDestinationPosition)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.TransferCargoItem, frame);
|
||||
command.TargetEntityKey = destinationEntityKey ?? "";
|
||||
command.EquipmentIndex = destinationCargoIndex;
|
||||
command.PositionX = destinationX;
|
||||
command.PositionY = destinationY;
|
||||
command.ScalarValue = quantity;
|
||||
command.TextValue = itemKey ?? "";
|
||||
command.CargoTransfer.OriginEntityKey = originEntityKey ?? "";
|
||||
command.CargoTransfer.OriginCargoIndex = originCargoIndex;
|
||||
command.CargoTransfer.DestinationEntityKey = destinationEntityKey ?? "";
|
||||
command.CargoTransfer.DestinationCargoIndex = destinationCargoIndex;
|
||||
command.CargoTransfer.SourceX = sourceX;
|
||||
command.CargoTransfer.SourceY = sourceY;
|
||||
command.CargoTransfer.DestinationX = destinationX;
|
||||
command.CargoTransfer.DestinationY = destinationY;
|
||||
command.CargoTransfer.HasDestinationPosition = hasDestinationPosition;
|
||||
command.CargoTransfer.Quantity = quantity;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope TradePurchase(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
string itemKey,
|
||||
int quantity,
|
||||
int stationCargoIndex,
|
||||
int targetCargoIndex,
|
||||
int sourceX,
|
||||
int sourceY)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.TradePurchase, frame);
|
||||
command.EquipmentIndex = targetCargoIndex;
|
||||
command.PositionX = sourceX;
|
||||
command.PositionY = sourceY;
|
||||
command.TextValue = itemKey ?? "";
|
||||
command.TradePurchase.ItemKey = itemKey ?? "";
|
||||
command.TradePurchase.Quantity = quantity;
|
||||
command.TradePurchase.StationCargoIndex = stationCargoIndex;
|
||||
command.TradePurchase.TargetCargoIndex = targetCargoIndex;
|
||||
command.TradePurchase.SourceX = sourceX;
|
||||
command.TradePurchase.SourceY = sourceY;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope TradeSale(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
string itemKey,
|
||||
int quantity,
|
||||
int sourceCargoIndex,
|
||||
int sourceX,
|
||||
int sourceY)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.TradeSale, frame);
|
||||
command.TextValue = itemKey ?? "";
|
||||
command.EquipmentIndex = sourceCargoIndex;
|
||||
command.PositionX = sourceX;
|
||||
command.PositionY = sourceY;
|
||||
command.ScalarValue = quantity;
|
||||
command.TradeSale.ItemKey = itemKey ?? "";
|
||||
command.TradeSale.Quantity = quantity;
|
||||
command.TradeSale.SourceCargoIndex = sourceCargoIndex;
|
||||
command.TradeSale.SourceX = sourceX;
|
||||
command.TradeSale.SourceY = sourceY;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope RestoreLoadout(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
string dockedEntityKey,
|
||||
string templateName,
|
||||
int price)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.RestoreLoadout, frame);
|
||||
command.TargetEntityKey = dockedEntityKey ?? "";
|
||||
command.TextValue = templateName ?? "";
|
||||
command.ScalarValue = price;
|
||||
command.LoadoutRestore.DockedEntityKey = dockedEntityKey ?? "";
|
||||
command.LoadoutRestore.TemplateName = templateName ?? "";
|
||||
command.LoadoutRestore.Price = price;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope EquipItem(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
string sourceKind,
|
||||
string originEntityKey,
|
||||
int originIndex,
|
||||
string destinationEntityKey,
|
||||
string itemKey,
|
||||
int sourceX,
|
||||
int sourceY,
|
||||
int destinationX,
|
||||
int destinationY,
|
||||
bool hasDestinationPosition)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.EquipItem, frame);
|
||||
command.TargetEntityKey = destinationEntityKey ?? "";
|
||||
command.PositionX = destinationX;
|
||||
command.PositionY = destinationY;
|
||||
command.TextValue = itemKey ?? "";
|
||||
command.EquipmentTransfer.SourceKind = sourceKind ?? "";
|
||||
command.EquipmentTransfer.OriginEntityKey = originEntityKey ?? "";
|
||||
command.EquipmentTransfer.OriginIndex = originIndex;
|
||||
command.EquipmentTransfer.DestinationEntityKey = destinationEntityKey ?? "";
|
||||
command.EquipmentTransfer.SourceX = sourceX;
|
||||
command.EquipmentTransfer.SourceY = sourceY;
|
||||
command.EquipmentTransfer.DestinationX = destinationX;
|
||||
command.EquipmentTransfer.DestinationY = destinationY;
|
||||
command.EquipmentTransfer.HasDestinationPosition = hasDestinationPosition;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope StoreItem(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
string originEntityKey,
|
||||
int sourceEquipmentIndex,
|
||||
string destinationEntityKey,
|
||||
int destinationCargoIndex,
|
||||
string itemKey,
|
||||
int destinationX,
|
||||
int destinationY,
|
||||
bool hasDestinationPosition,
|
||||
string sourceKind = AetheriaRuntimeRefitSourceKinds.Equipment)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.StoreItem, frame);
|
||||
command.TargetEntityKey = destinationEntityKey ?? "";
|
||||
command.EquipmentIndex = sourceEquipmentIndex;
|
||||
command.PositionX = destinationX;
|
||||
command.PositionY = destinationY;
|
||||
command.TextValue = itemKey ?? "";
|
||||
command.StoreItem.OriginEntityKey = originEntityKey ?? "";
|
||||
command.StoreItem.SourceEquipmentIndex = sourceEquipmentIndex;
|
||||
command.StoreItem.DestinationEntityKey = destinationEntityKey ?? "";
|
||||
command.StoreItem.DestinationCargoIndex = destinationCargoIndex;
|
||||
command.StoreItem.DestinationX = destinationX;
|
||||
command.StoreItem.DestinationY = destinationY;
|
||||
command.StoreItem.HasDestinationPosition = hasDestinationPosition;
|
||||
command.StoreItem.SourceKind = sourceKind ?? AetheriaRuntimeRefitSourceKinds.Equipment;
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope IssueAgentTask(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
AetheriaRuntimeAgentTaskCommand task)
|
||||
{
|
||||
task ??= new AetheriaRuntimeAgentTaskCommand();
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.IssueAgentTask, frame);
|
||||
command.AgentTask = task;
|
||||
command.SubjectKey = string.IsNullOrWhiteSpace(task.CorporationKey)
|
||||
? "aetheria.tasks"
|
||||
: $"aetheria.tasks.{task.CorporationKey}";
|
||||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope CancelAgentTask(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
string taskId)
|
||||
{
|
||||
var command = Create(AetheriaRuntimeDaemonCommandKinds.CancelAgentTask, frame);
|
||||
command.AgentTask.TaskId = taskId ?? "";
|
||||
command.SubjectKey = "aetheria.tasks";
|
||||
return Send(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ce9502e0eb3bd5749b54057cb266a717
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e5491470c689e0b40a098462d4ab1fe5
|
||||
|
|
@ -1,645 +0,0 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using GameCult.Eve.Surface;
|
||||
using GameCult.Mesh;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public sealed class AetheriaRuntimeDaemonOperationsClient
|
||||
{
|
||||
private readonly Func<Func<AetheriaRuntimeDaemonOperationClient, AetheriaRuntimeDaemonFrameDocument?, AetheriaRuntimeDaemonCommandEnvelope>, AetheriaRuntimeDaemonCommandEnvelope> _submit;
|
||||
|
||||
internal AetheriaRuntimeDaemonOperationsClient(
|
||||
Func<Func<AetheriaRuntimeDaemonOperationClient, AetheriaRuntimeDaemonFrameDocument?, AetheriaRuntimeDaemonCommandEnvelope>, AetheriaRuntimeDaemonCommandEnvelope> submit)
|
||||
{
|
||||
_submit = submit ?? throw new ArgumentNullException(nameof(submit));
|
||||
}
|
||||
|
||||
public static bool TryCreateSurfaceCommandDocument(
|
||||
EveSurfaceCommandRequest request,
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
string stateFilePath,
|
||||
string clientId,
|
||||
string sessionId,
|
||||
out AetheriaRuntimeDaemonCommandDocument? command)
|
||||
{
|
||||
command = null;
|
||||
if (request == null)
|
||||
return false;
|
||||
|
||||
AetheriaRuntimeDaemonCommandDocument? translated = null;
|
||||
var operations = new AetheriaRuntimeDaemonOperationsClient(submit =>
|
||||
{
|
||||
var client = new AetheriaRuntimeDaemonOperationClient(
|
||||
string.IsNullOrWhiteSpace(stateFilePath) ? "." : stateFilePath,
|
||||
clientId,
|
||||
sessionId,
|
||||
document =>
|
||||
{
|
||||
translated = document;
|
||||
return AetheriaRuntimeDaemonOperationClient.ToEnvelope(document);
|
||||
});
|
||||
return submit(client, frame);
|
||||
});
|
||||
var accepted = operations.TrySubmitSurfaceCommand(request, out _);
|
||||
var actorEntityKey = ReadPayloadString(
|
||||
request,
|
||||
"entityId",
|
||||
ReadPayloadString(request, "sourceEntityId", ""));
|
||||
if (translated != null && !string.IsNullOrWhiteSpace(actorEntityKey))
|
||||
translated.ActorEntityKey = actorEntityKey;
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("AETHERIA_TRACE_EVE_SNAPSHOTS"), "1", StringComparison.Ordinal))
|
||||
Console.WriteLine(
|
||||
$"Eve command translated command={request.Command} actor={translated?.ActorEntityKey ?? "missing"} " +
|
||||
$"direction={translated?.DirectionX},{translated?.DirectionY} scalar={translated?.ScalarValue}");
|
||||
command = translated;
|
||||
return accepted && command != null;
|
||||
}
|
||||
|
||||
public AetheriaRuntimeDaemonCommandEnvelope SetTarget(string targetEntityKey)
|
||||
{
|
||||
return Submit((client, frame) => client.SetTarget(frame, targetEntityKey));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt ClearTarget()
|
||||
{
|
||||
return Send((client, frame) => client.ClearTarget(frame));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt TargetNearest()
|
||||
{
|
||||
return Send((client, frame) => client.TargetNearest(frame));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt TargetNext()
|
||||
{
|
||||
return Send((client, frame) => client.TargetNext(frame));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt TargetPrevious()
|
||||
{
|
||||
return Send((client, frame) => client.TargetPrevious(frame));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt TargetReticle(double directionX, double directionY, double directionZ)
|
||||
{
|
||||
return Send((client, frame) => client.TargetReticle(frame, directionX, directionY, directionZ));
|
||||
}
|
||||
|
||||
public AetheriaRuntimeDaemonCommandEnvelope SetMoveVector(
|
||||
double directionX,
|
||||
double directionY,
|
||||
double scalarValue = 1.0)
|
||||
{
|
||||
return Submit((client, frame) => client.SetMoveVector(frame, directionX, directionY, scalarValue));
|
||||
}
|
||||
|
||||
public AetheriaRuntimeDaemonCommandEnvelope IssueAgentTask(AetheriaRuntimeAgentTaskCommand task)
|
||||
{
|
||||
return Submit((client, frame) => client.IssueAgentTask(frame, task));
|
||||
}
|
||||
|
||||
public AetheriaRuntimeDaemonCommandEnvelope CancelAgentTask(string taskId)
|
||||
{
|
||||
return Submit((client, frame) => client.CancelAgentTask(frame, taskId));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt SetLookDirection(double directionX, double directionY, double directionZ)
|
||||
{
|
||||
return Send((client, frame) => client.SetLookDirection(frame, directionX, directionY, directionZ));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt SetTractorPower(double power)
|
||||
{
|
||||
return Send((client, frame) => client.SetTractorPower(frame, power));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt FireWeaponGroup(int weaponGroup)
|
||||
{
|
||||
return Send((client, frame) => client.FireWeaponGroup(frame, weaponGroup));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt SetWeaponGroupActive(int weaponGroup, bool active)
|
||||
{
|
||||
return Send((client, frame) => client.SetWeaponGroupActive(frame, weaponGroup, active));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt SetWeaponGroupMembership(
|
||||
string targetEntityKey,
|
||||
int equipmentIndex,
|
||||
int weaponGroup,
|
||||
bool assigned)
|
||||
{
|
||||
return Send((client, frame) => client.SetWeaponGroupMembership(
|
||||
frame,
|
||||
targetEntityKey,
|
||||
equipmentIndex,
|
||||
weaponGroup,
|
||||
assigned));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt SetBehaviorActive(int equipmentIndex, int behaviorIndex, bool active)
|
||||
{
|
||||
return Send((client, frame) => client.SetBehaviorActive(frame, equipmentIndex, behaviorIndex, active));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt ActivateConsumable(string itemKey)
|
||||
{
|
||||
return Send((client, frame) => client.ActivateConsumable(frame, itemKey));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt SensorPing()
|
||||
{
|
||||
return Send((client, frame) => client.SensorPing(frame));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt SetHeatsinksEnabled(bool enabled)
|
||||
{
|
||||
return Send((client, frame) => client.SetHeatsinksEnabled(frame, enabled));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt SetOverrideShutdown(bool enabled)
|
||||
{
|
||||
return Send((client, frame) => client.SetOverrideShutdown(frame, enabled));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt SetEntityOverrideShutdown(string targetEntityKey, bool enabled)
|
||||
{
|
||||
return Send((client, frame) => client.SetOverrideShutdown(frame, targetEntityKey, enabled));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt SetItemEnabled(int equipmentIndex, bool enabled)
|
||||
{
|
||||
return Send((client, frame) => client.SetItemEnabled(frame, equipmentIndex, enabled));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt ToggleShieldEnabled()
|
||||
{
|
||||
return Send((client, frame) => client.ToggleShieldEnabled(frame));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt SetItemOverrideShutdown(
|
||||
string targetEntityKey,
|
||||
int equipmentIndex,
|
||||
bool enabled)
|
||||
{
|
||||
return Send((client, frame) => client.SetItemOverrideShutdown(
|
||||
frame,
|
||||
targetEntityKey,
|
||||
equipmentIndex,
|
||||
enabled));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt SetThermotoggleTargetTemperature(
|
||||
string targetEntityKey,
|
||||
int equipmentIndex,
|
||||
int behaviorIndex,
|
||||
double targetTemperature)
|
||||
{
|
||||
return Send((client, frame) => client.SetThermotoggleTargetTemperature(
|
||||
frame,
|
||||
targetEntityKey,
|
||||
equipmentIndex,
|
||||
behaviorIndex,
|
||||
targetTemperature));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt SetShutdownPerformance(
|
||||
string targetEntityKey,
|
||||
double shutdownPerformance)
|
||||
{
|
||||
return Send((client, frame) => client.SetShutdownPerformance(
|
||||
frame,
|
||||
targetEntityKey,
|
||||
shutdownPerformance));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt ToggleHullConductivity(
|
||||
string targetEntityKey,
|
||||
int x,
|
||||
int y,
|
||||
int axis)
|
||||
{
|
||||
return Send((client, frame) => client.ToggleHullConductivity(frame, targetEntityKey, x, y, axis));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt SetEntityName(string targetEntityKey, string name)
|
||||
{
|
||||
return Send((client, frame) => client.SetEntityName(frame, targetEntityKey, name));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt Dock(string targetEntityKey)
|
||||
{
|
||||
return Send((client, frame) => client.Dock(frame, targetEntityKey));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt DockNearest()
|
||||
{
|
||||
return Send((client, frame) => client.DockNearest(frame));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt Undock()
|
||||
{
|
||||
return Send((client, frame) => client.Undock(frame));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt Interact()
|
||||
{
|
||||
return Send((client, frame) => client.Interact(frame));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt SetDockedCurrentShip(string targetEntityKey)
|
||||
{
|
||||
return Send((client, frame) => client.SetDockedCurrentShip(frame, targetEntityKey));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt EnterWormhole(
|
||||
int targetZoneIndex,
|
||||
double positionX,
|
||||
double positionY)
|
||||
{
|
||||
return Send((client, frame) => client.EnterWormhole(frame, targetZoneIndex, positionX, positionY));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt TowToStation(string stationEntityKey)
|
||||
{
|
||||
return Send((client, frame) => client.TowToStation(frame, stationEntityKey));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt TransferCargoItem(
|
||||
string originEntityKey,
|
||||
int originCargoIndex,
|
||||
string destinationEntityKey,
|
||||
int destinationCargoIndex,
|
||||
string itemKey,
|
||||
int quantity,
|
||||
int sourceX,
|
||||
int sourceY,
|
||||
int destinationX,
|
||||
int destinationY,
|
||||
bool hasDestinationPosition)
|
||||
{
|
||||
return Send((client, frame) => client.TransferCargoItem(
|
||||
frame,
|
||||
originEntityKey,
|
||||
originCargoIndex,
|
||||
destinationEntityKey,
|
||||
destinationCargoIndex,
|
||||
itemKey,
|
||||
quantity,
|
||||
sourceX,
|
||||
sourceY,
|
||||
destinationX,
|
||||
destinationY,
|
||||
hasDestinationPosition));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt TradePurchase(
|
||||
string itemKey,
|
||||
int quantity,
|
||||
int stationCargoIndex,
|
||||
int targetCargoIndex,
|
||||
int sourceX,
|
||||
int sourceY)
|
||||
{
|
||||
return Send((client, frame) => client.TradePurchase(
|
||||
frame,
|
||||
itemKey,
|
||||
quantity,
|
||||
stationCargoIndex,
|
||||
targetCargoIndex,
|
||||
sourceX,
|
||||
sourceY));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt TradeSale(
|
||||
string itemKey,
|
||||
int quantity,
|
||||
int sourceCargoIndex,
|
||||
int sourceX,
|
||||
int sourceY)
|
||||
{
|
||||
return Send((client, frame) => client.TradeSale(
|
||||
frame,
|
||||
itemKey,
|
||||
quantity,
|
||||
sourceCargoIndex,
|
||||
sourceX,
|
||||
sourceY));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt RestoreLoadout(
|
||||
string dockedEntityKey,
|
||||
string templateName,
|
||||
int price)
|
||||
{
|
||||
return Send((client, frame) => client.RestoreLoadout(frame, dockedEntityKey, templateName, price));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt EquipItem(
|
||||
string sourceKind,
|
||||
string originEntityKey,
|
||||
int originIndex,
|
||||
string destinationEntityKey,
|
||||
string itemKey,
|
||||
int sourceX,
|
||||
int sourceY,
|
||||
int destinationX,
|
||||
int destinationY,
|
||||
bool hasDestinationPosition)
|
||||
{
|
||||
return Send((client, frame) => client.EquipItem(
|
||||
frame,
|
||||
sourceKind,
|
||||
originEntityKey,
|
||||
originIndex,
|
||||
destinationEntityKey,
|
||||
itemKey,
|
||||
sourceX,
|
||||
sourceY,
|
||||
destinationX,
|
||||
destinationY,
|
||||
hasDestinationPosition));
|
||||
}
|
||||
|
||||
public CultMeshOperationReceipt StoreItem(
|
||||
string originEntityKey,
|
||||
int sourceEquipmentIndex,
|
||||
string destinationEntityKey,
|
||||
int destinationCargoIndex,
|
||||
string itemKey,
|
||||
int destinationX,
|
||||
int destinationY,
|
||||
bool hasDestinationPosition,
|
||||
string sourceKind = AetheriaRuntimeRefitSourceKinds.Equipment)
|
||||
{
|
||||
return Send((client, frame) => client.StoreItem(
|
||||
frame,
|
||||
originEntityKey,
|
||||
sourceEquipmentIndex,
|
||||
destinationEntityKey,
|
||||
destinationCargoIndex,
|
||||
itemKey,
|
||||
destinationX,
|
||||
destinationY,
|
||||
hasDestinationPosition,
|
||||
sourceKind));
|
||||
}
|
||||
|
||||
public bool TrySubmitSurfaceCommand(
|
||||
EveSurfaceCommandRequest request,
|
||||
out AetheriaRuntimeDaemonCommandEnvelope? envelope)
|
||||
{
|
||||
envelope = null;
|
||||
if (request == null ||
|
||||
!string.Equals(request.ProviderId, AetheriaRuntimeProviderIdentity.ProviderId, StringComparison.Ordinal) ||
|
||||
!TryResolveSurfaceCommandKind(request, out var kind))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
envelope = Submit((client, frame) =>
|
||||
TrySubmitSurfaceCommand(client, frame, kind, request, out var submitted)
|
||||
? submitted!
|
||||
: throw new UnsupportedSurfaceCommandException());
|
||||
return true;
|
||||
}
|
||||
catch (UnsupportedSurfaceCommandException)
|
||||
{
|
||||
envelope = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private CultMeshOperationReceipt Send(
|
||||
Func<AetheriaRuntimeDaemonOperationClient, AetheriaRuntimeDaemonFrameDocument?, AetheriaRuntimeDaemonCommandEnvelope> submit)
|
||||
{
|
||||
return Submit(submit);
|
||||
}
|
||||
|
||||
private AetheriaRuntimeDaemonCommandEnvelope Submit(
|
||||
Func<AetheriaRuntimeDaemonOperationClient, AetheriaRuntimeDaemonFrameDocument?, AetheriaRuntimeDaemonCommandEnvelope> submit)
|
||||
{
|
||||
return _submit(submit);
|
||||
}
|
||||
|
||||
private static bool TryResolveSurfaceCommandKind(
|
||||
EveSurfaceCommandRequest request,
|
||||
out AetheriaRuntimeDaemonCommandKinds kind)
|
||||
{
|
||||
kind = AetheriaRuntimeDaemonCommandKinds.None;
|
||||
var command = NormalizeSurfaceCommandName(request.Operation?.OperationId ?? "");
|
||||
if (!Enum.TryParse(command, ignoreCase: false, out kind) ||
|
||||
kind == AetheriaRuntimeDaemonCommandKinds.None)
|
||||
{
|
||||
command = NormalizeSurfaceCommandName(ReadPayloadString(request, "commandId", ""));
|
||||
}
|
||||
|
||||
return Enum.TryParse(command, ignoreCase: false, out kind) &&
|
||||
kind != AetheriaRuntimeDaemonCommandKinds.None;
|
||||
}
|
||||
|
||||
private static string NormalizeSurfaceCommandName(string command)
|
||||
{
|
||||
if (command.StartsWith(AetheriaRuntimeDaemonSurfaceCommandCatalog.CommandPrefix, StringComparison.Ordinal))
|
||||
command = command.Substring(AetheriaRuntimeDaemonSurfaceCommandCatalog.CommandPrefix.Length);
|
||||
return command;
|
||||
}
|
||||
|
||||
private static bool TrySubmitSurfaceCommand(
|
||||
AetheriaRuntimeDaemonOperationClient client,
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
AetheriaRuntimeDaemonCommandKinds kind,
|
||||
EveSurfaceCommandRequest request,
|
||||
out AetheriaRuntimeDaemonCommandEnvelope? envelope)
|
||||
{
|
||||
envelope = null;
|
||||
if (client == null)
|
||||
return false;
|
||||
|
||||
envelope = kind switch
|
||||
{
|
||||
AetheriaRuntimeDaemonCommandKinds.SetMoveVector => client.SetMoveVector(
|
||||
frame,
|
||||
ReadPayloadDouble(request, "directionX", 0.0),
|
||||
ReadPayloadDouble(request, "directionY", 0.0),
|
||||
ReadPayloadDouble(request, "scalarValue", 1.0)),
|
||||
AetheriaRuntimeDaemonCommandKinds.SetSimulationRate => client.SetSimulationRate(
|
||||
frame,
|
||||
ReadPayloadDouble(request, "scalarValue", 0.0)),
|
||||
AetheriaRuntimeDaemonCommandKinds.AdvanceSimulationStep => client.AdvanceSimulationStep(frame),
|
||||
AetheriaRuntimeDaemonCommandKinds.SetLookDirection => client.SetLookDirection(
|
||||
frame,
|
||||
ReadPayloadDouble(request, "directionX", 0.0),
|
||||
ReadPayloadDouble(request, "directionY", 1.0),
|
||||
ReadPayloadDouble(request, "directionZ", 0.0)),
|
||||
AetheriaRuntimeDaemonCommandKinds.TargetReticle => client.TargetReticle(
|
||||
frame,
|
||||
ReadPayloadDouble(request, "directionX", 0.0),
|
||||
ReadPayloadDouble(request, "directionY", 0.0),
|
||||
ReadPayloadDouble(request, "directionZ", 0.0)),
|
||||
AetheriaRuntimeDaemonCommandKinds.SetTractorPower => client.SetTractorPower(
|
||||
frame,
|
||||
ReadPayloadDouble(request, "scalarValue", 1.0)),
|
||||
AetheriaRuntimeDaemonCommandKinds.FireWeaponGroup => client.FireWeaponGroup(
|
||||
frame,
|
||||
ReadWeaponGroup(request)),
|
||||
AetheriaRuntimeDaemonCommandKinds.SetWeaponGroupActive => client.SetWeaponGroupActive(
|
||||
frame,
|
||||
ReadWeaponGroup(request),
|
||||
ReadPayloadBool(request, "active", true)),
|
||||
AetheriaRuntimeDaemonCommandKinds.SetBehaviorActive => client.SetBehaviorActive(
|
||||
frame,
|
||||
(int)ReadPayloadDouble(request, "equipmentIndex", -1),
|
||||
(int)ReadPayloadDouble(request, "behaviorIndex", -1),
|
||||
ReadPayloadBool(request, "active", true)),
|
||||
AetheriaRuntimeDaemonCommandKinds.SetThermotoggleTargetTemperature =>
|
||||
client.SetThermotoggleTargetTemperature(
|
||||
frame,
|
||||
ReadPayloadString(request, "targetEntityKey",
|
||||
ReadPayloadString(request, "entityId", "")),
|
||||
(int)ReadPayloadDouble(request, "equipmentIndex", -1),
|
||||
(int)ReadPayloadDouble(request, "behaviorIndex", -1),
|
||||
ReadPayloadDouble(request, "scalarValue", double.NaN)),
|
||||
AetheriaRuntimeDaemonCommandKinds.SetTarget => client.SetTarget(
|
||||
frame,
|
||||
ReadPayloadString(request, "targetEntityId", ReadPayloadString(request, "entityId", ""))),
|
||||
AetheriaRuntimeDaemonCommandKinds.IssueAgentTask => client.IssueAgentTask(
|
||||
frame,
|
||||
new AetheriaRuntimeAgentTaskCommand
|
||||
{
|
||||
TaskId = ReadPayloadString(request, "taskId", ""),
|
||||
CorporationKey = ReadPayloadString(request, "corporation", ""),
|
||||
TaskType = ReadPayloadString(request, "taskType", ""),
|
||||
Priority = (int)ReadPayloadDouble(request, "priority", 0),
|
||||
ZoneIndex = (int)ReadPayloadDouble(request, "zoneIndex", -1),
|
||||
TargetEntityIndex = (int)ReadPayloadDouble(request, "targetEntityIndex", -1),
|
||||
TargetPositionX = ReadPayloadDouble(request, "targetPositionX", 0),
|
||||
TargetPositionZ = ReadPayloadDouble(request, "targetPositionZ", 0),
|
||||
CompletionRadius = ReadPayloadDouble(request, "completionRadius", 10),
|
||||
WeaponGroup = (int)ReadPayloadDouble(request, "weaponGroup", 0),
|
||||
OriginEntityIndex = (int)ReadPayloadDouble(request, "originEntityIndex", -1),
|
||||
ItemKey = ReadPayloadString(request, "itemKey", ""),
|
||||
Quantity = (int)ReadPayloadDouble(request, "quantity", 0),
|
||||
TargetBodyKeys = ReadPayloadString(request, "targetBodyKeys", "")
|
||||
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(key => key.Trim())
|
||||
.Where(key => !string.IsNullOrWhiteSpace(key))
|
||||
.ToArray(),
|
||||
OrbitParentKey = ReadPayloadString(request, "orbitParentKey", ""),
|
||||
OrbitDistance = ReadPayloadDouble(request, "orbitDistance", 0)
|
||||
}),
|
||||
AetheriaRuntimeDaemonCommandKinds.CancelAgentTask => client.CancelAgentTask(
|
||||
frame,
|
||||
ReadPayloadString(request, "taskId", "")),
|
||||
AetheriaRuntimeDaemonCommandKinds.TradePurchase => client.TradePurchase(
|
||||
frame,
|
||||
ReadPayloadString(request, "itemKey", ""),
|
||||
(int)ReadPayloadDouble(request, "quantity", 1),
|
||||
(int)ReadPayloadDouble(request, "stationCargoIndex", -1),
|
||||
(int)ReadPayloadDouble(request, "targetCargoIndex", -1),
|
||||
(int)ReadPayloadDouble(request, "sourceX", int.MinValue),
|
||||
(int)ReadPayloadDouble(request, "sourceY", int.MinValue)),
|
||||
AetheriaRuntimeDaemonCommandKinds.TradeSale => client.TradeSale(
|
||||
frame,
|
||||
ReadPayloadString(request, "itemKey", ""),
|
||||
(int)ReadPayloadDouble(request, "quantity", 1),
|
||||
(int)ReadPayloadDouble(request, "sourceCargoIndex", -1),
|
||||
(int)ReadPayloadDouble(request, "sourceX", int.MinValue),
|
||||
(int)ReadPayloadDouble(request, "sourceY", int.MinValue)),
|
||||
AetheriaRuntimeDaemonCommandKinds.TransferCargoItem => client.TransferCargoItem(
|
||||
frame,
|
||||
ReadPayloadString(request, "originEntityKey", ""),
|
||||
(int)ReadPayloadDouble(request, "originCargoIndex", -1),
|
||||
ReadPayloadString(request, "destinationEntityKey", ""),
|
||||
(int)ReadPayloadDouble(request, "destinationCargoIndex", -1),
|
||||
ReadPayloadString(request, "itemKey", ""),
|
||||
(int)ReadPayloadDouble(request, "quantity", 1),
|
||||
(int)ReadPayloadDouble(request, "sourceX", int.MinValue),
|
||||
(int)ReadPayloadDouble(request, "sourceY", int.MinValue),
|
||||
(int)ReadPayloadDouble(request, "destinationX", int.MinValue),
|
||||
(int)ReadPayloadDouble(request, "destinationY", int.MinValue),
|
||||
ReadPayloadBool(request, "hasDestinationPosition", false)),
|
||||
AetheriaRuntimeDaemonCommandKinds.EquipItem => client.EquipItem(
|
||||
frame,
|
||||
ReadPayloadString(request, "sourceKind", AetheriaRuntimeRefitSourceKinds.Cargo),
|
||||
ReadPayloadString(request, "originEntityKey", ""),
|
||||
(int)ReadPayloadDouble(request, "originIndex", -1),
|
||||
ReadPayloadString(request, "destinationEntityKey", ""),
|
||||
ReadPayloadString(request, "itemKey", ""),
|
||||
(int)ReadPayloadDouble(request, "sourceX", int.MinValue),
|
||||
(int)ReadPayloadDouble(request, "sourceY", int.MinValue),
|
||||
(int)ReadPayloadDouble(request, "destinationX", int.MinValue),
|
||||
(int)ReadPayloadDouble(request, "destinationY", int.MinValue),
|
||||
ReadPayloadBool(request, "hasDestinationPosition", false)),
|
||||
AetheriaRuntimeDaemonCommandKinds.StoreItem => client.StoreItem(
|
||||
frame,
|
||||
ReadPayloadString(request, "originEntityKey", ""),
|
||||
(int)ReadPayloadDouble(request, "originIndex", -1),
|
||||
ReadPayloadString(request, "destinationEntityKey", ""),
|
||||
(int)ReadPayloadDouble(request, "destinationCargoIndex", -1),
|
||||
ReadPayloadString(request, "itemKey", ""),
|
||||
(int)ReadPayloadDouble(request, "destinationX", int.MinValue),
|
||||
(int)ReadPayloadDouble(request, "destinationY", int.MinValue),
|
||||
ReadPayloadBool(request, "hasDestinationPosition", false),
|
||||
ReadPayloadString(request, "sourceKind", AetheriaRuntimeRefitSourceKinds.Equipment)),
|
||||
_ => AetheriaRuntimeDaemonSurfaceCommandCatalog.TrySubmitArgumentless(
|
||||
client,
|
||||
frame,
|
||||
kind,
|
||||
out var submitted)
|
||||
? submitted
|
||||
: null
|
||||
};
|
||||
|
||||
return envelope != null;
|
||||
}
|
||||
|
||||
private static string ReadPayloadString(
|
||||
EveSurfaceCommandRequest request,
|
||||
string key,
|
||||
string defaultValue)
|
||||
{
|
||||
var value = request.Payload.GetString(key);
|
||||
return string.IsNullOrWhiteSpace(value) ? defaultValue : value;
|
||||
}
|
||||
|
||||
private static double ReadPayloadDouble(
|
||||
EveSurfaceCommandRequest request,
|
||||
string key,
|
||||
double defaultValue)
|
||||
{
|
||||
return request.Payload.GetDouble(key, defaultValue);
|
||||
}
|
||||
|
||||
private static bool ReadPayloadBool(
|
||||
EveSurfaceCommandRequest request,
|
||||
string key,
|
||||
bool defaultValue)
|
||||
{
|
||||
var value = request.Payload.GetString(key);
|
||||
return string.IsNullOrWhiteSpace(value)
|
||||
? defaultValue
|
||||
: string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(value, "1", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static int ReadWeaponGroup(EveSurfaceCommandRequest request)
|
||||
{
|
||||
var value = request.Payload.GetString("weaponGroup");
|
||||
if (int.TryParse(value, out var parsed))
|
||||
return parsed;
|
||||
|
||||
value = request.Payload.GetString("actionId");
|
||||
return int.TryParse(value, out parsed) ? parsed : 0;
|
||||
}
|
||||
|
||||
private sealed class UnsupportedSurfaceCommandException : Exception
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 28d1fca8a55115f4db34e4bf2be8c072
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8d4ea4a3ec7b5e14aa476dbceb212451
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
using System;
|
||||
using GameCult.Mesh;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public sealed class AetheriaRuntimeDaemonRenderView
|
||||
{
|
||||
public AetheriaRuntimeDaemonRenderView(
|
||||
AetheriaRuntimeDaemonFrameDocument frame,
|
||||
AetheriaRuntimeDaemonSoaViewDocument? soaView,
|
||||
AetheriaRuntimeZoneRenderDocument zoneRender)
|
||||
{
|
||||
Frame = frame ?? throw new ArgumentNullException(nameof(frame));
|
||||
SoaView = soaView;
|
||||
SoaIndex = AetheriaRuntimeDaemonSoaViewIndex.Build(soaView);
|
||||
ZoneRender = zoneRender ?? throw new ArgumentNullException(nameof(zoneRender));
|
||||
}
|
||||
|
||||
public AetheriaRuntimeDaemonFrameDocument Frame { get; }
|
||||
public AetheriaRuntimeDaemonSoaViewDocument? SoaView { get; }
|
||||
public AetheriaRuntimeDaemonSoaViewIndex SoaIndex { get; }
|
||||
public AetheriaRuntimeZoneRenderDocument ZoneRender { get; }
|
||||
public bool HasSoaView => SoaView != null && SoaIndex.IsValid;
|
||||
public bool IsAuthoritative => Frame.IsAuthoritative;
|
||||
public AetheriaRuntimeRunCheckpointCommit Run => Frame.Run;
|
||||
|
||||
public static bool TryCreateCurrent(
|
||||
CultMeshReactiveDocument<AetheriaRuntimeDaemonFrameDocument> frame,
|
||||
CultMeshReactiveDocument<AetheriaRuntimeDaemonSoaViewDocument>? soaView,
|
||||
CultMeshReactiveDocument<AetheriaRuntimeZoneRenderDocument> zoneRender,
|
||||
out AetheriaRuntimeDaemonRenderView? observed)
|
||||
{
|
||||
observed = null;
|
||||
try
|
||||
{
|
||||
var currentFrame = frame?.Current;
|
||||
var currentZoneRender = zoneRender?.Current;
|
||||
if (currentFrame == null || currentZoneRender == null)
|
||||
return false;
|
||||
|
||||
var currentSoaView = soaView?.Current;
|
||||
if (currentSoaView == null ||
|
||||
!string.Equals(currentSoaView.Schema, AetheriaRuntimeDaemonSchemas.SoaView, StringComparison.Ordinal))
|
||||
{
|
||||
currentSoaView = null;
|
||||
}
|
||||
|
||||
observed = new AetheriaRuntimeDaemonRenderView(currentFrame, currentSoaView, currentZoneRender);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: faf4bdd4c3ff79548932e1e85d115eed
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 479d0ef759b333b4dae8052bad70eca7
|
||||
|
|
@ -1,303 +0,0 @@
|
|||
using MessagePack;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
[MessagePackObject(true)]
|
||||
public readonly struct AetheriaRuntimeDaemonSimulationSettings
|
||||
{
|
||||
public static AetheriaRuntimeDaemonSimulationSettings AetheriaDefault { get; } =
|
||||
new AetheriaRuntimeDaemonSimulationSettings(
|
||||
pawnSpeed: 84.0,
|
||||
raiderSpeed: 68.0,
|
||||
attackRange: 145.0,
|
||||
attackHoldRatio: 0.82,
|
||||
pawnProjectileDamage: 18.0,
|
||||
raiderProjectileDamage: 12.0,
|
||||
weaponCooldownSeconds: 0.55,
|
||||
projectileSpeed: 330.0,
|
||||
projectileRadius: 18.0,
|
||||
projectileLifetimeSeconds: 2.2,
|
||||
projectileSpawnOffset: 18.0,
|
||||
projectileHeatScale: 0.18,
|
||||
heatDissipationPerSecond: 8.0,
|
||||
stationSensorRange: 720.0,
|
||||
entitySensorRange: 520.0,
|
||||
playerStationHull: 420.0,
|
||||
hostileStationHull: 240.0,
|
||||
playerEntityHull: 120.0,
|
||||
raiderEntityHull: 80.0,
|
||||
stationShield: 120.0,
|
||||
entityShield: 45.0,
|
||||
weaponLockSpeed: 2.0,
|
||||
weaponLockSensorImpact: 1.0,
|
||||
weaponLockAngleDegrees: 45.0,
|
||||
weaponLockDirectionImpact: 1.0,
|
||||
weaponLockDecayPerSecond: 1.0,
|
||||
lootDropProbability: 0.25,
|
||||
lootDropVelocity: 25.0,
|
||||
pickupLifetimeSeconds: 30.0,
|
||||
severeThermalRiskThreshold: 0.25,
|
||||
heatstrokeTemperature: 330.0,
|
||||
heatstrokeMultiplier: 0.00001,
|
||||
heatstrokeExponent: 2.0,
|
||||
heatstrokeRecoveryPerSecond: 0.2,
|
||||
hypothermiaTemperature: 273.0,
|
||||
hypothermiaMultiplier: 0.00001,
|
||||
hypothermiaExponent: 2.0,
|
||||
hypothermiaRecoveryPerSecond: 0.2,
|
||||
agentRangeExponent: 0.25,
|
||||
agentForwardLerp: 0.5,
|
||||
agentMaxForwardDistance: 50,
|
||||
agentDpsSampleCount: 32,
|
||||
wormholeAnimationDuration: 4,
|
||||
wormholeExitCurveStart: 0.8,
|
||||
wormholeExitVelocity: 20,
|
||||
wormholeExitRadius: 50,
|
||||
wormholeDepth: 1000);
|
||||
|
||||
public AetheriaRuntimeDaemonSimulationSettings(
|
||||
double pawnSpeed,
|
||||
double raiderSpeed,
|
||||
double attackRange,
|
||||
double attackHoldRatio,
|
||||
double pawnProjectileDamage,
|
||||
double raiderProjectileDamage,
|
||||
double weaponCooldownSeconds,
|
||||
double projectileSpeed,
|
||||
double projectileRadius,
|
||||
double projectileLifetimeSeconds,
|
||||
double projectileSpawnOffset,
|
||||
double projectileHeatScale,
|
||||
double heatDissipationPerSecond,
|
||||
double stationSensorRange,
|
||||
double entitySensorRange,
|
||||
double playerStationHull,
|
||||
double hostileStationHull,
|
||||
double playerEntityHull,
|
||||
double raiderEntityHull,
|
||||
double stationShield,
|
||||
double entityShield,
|
||||
double weaponLockSpeed = AetheriaDefaultRaw.WeaponLockSpeed,
|
||||
double weaponLockSensorImpact = AetheriaDefaultRaw.WeaponLockSensorImpact,
|
||||
double weaponLockAngleDegrees = AetheriaDefaultRaw.WeaponLockAngleDegrees,
|
||||
double weaponLockDirectionImpact = AetheriaDefaultRaw.WeaponLockDirectionImpact,
|
||||
double weaponLockDecayPerSecond = AetheriaDefaultRaw.WeaponLockDecayPerSecond,
|
||||
double lootDropProbability = AetheriaDefaultRaw.LootDropProbability,
|
||||
double lootDropVelocity = AetheriaDefaultRaw.LootDropVelocity,
|
||||
double pickupLifetimeSeconds = AetheriaDefaultRaw.PickupLifetimeSeconds,
|
||||
double severeThermalRiskThreshold = AetheriaDefaultRaw.SevereThermalRiskThreshold,
|
||||
double heatstrokeTemperature = AetheriaDefaultRaw.HeatstrokeTemperature,
|
||||
double heatstrokeMultiplier = AetheriaDefaultRaw.HeatstrokeMultiplier,
|
||||
double heatstrokeExponent = AetheriaDefaultRaw.HeatstrokeExponent,
|
||||
double heatstrokeRecoveryPerSecond = AetheriaDefaultRaw.HeatstrokeRecoveryPerSecond,
|
||||
double hypothermiaTemperature = AetheriaDefaultRaw.HypothermiaTemperature,
|
||||
double hypothermiaMultiplier = AetheriaDefaultRaw.HypothermiaMultiplier,
|
||||
double hypothermiaExponent = AetheriaDefaultRaw.HypothermiaExponent,
|
||||
double hypothermiaRecoveryPerSecond = AetheriaDefaultRaw.HypothermiaRecoveryPerSecond,
|
||||
double aetherTorqueMultiplier = AetheriaDefaultRaw.AetherTorqueMultiplier,
|
||||
double aetherHeatMultiplier = AetheriaDefaultRaw.AetherHeatMultiplier,
|
||||
double torqueFloor = AetheriaDefaultRaw.TorqueFloor,
|
||||
double torqueMultiplier = AetheriaDefaultRaw.TorqueMultiplier,
|
||||
double agentRangeExponent = AetheriaDefaultRaw.AgentRangeExponent,
|
||||
double agentForwardLerp = AetheriaDefaultRaw.AgentForwardLerp,
|
||||
double agentMaxForwardDistance = AetheriaDefaultRaw.AgentMaxForwardDistance,
|
||||
int agentDpsSampleCount = AetheriaDefaultRaw.AgentDpsSampleCount,
|
||||
double wormholeAnimationDuration = AetheriaDefaultRaw.WormholeAnimationDuration,
|
||||
double wormholeExitCurveStart = AetheriaDefaultRaw.WormholeExitCurveStart,
|
||||
double wormholeExitVelocity = AetheriaDefaultRaw.WormholeExitVelocity,
|
||||
double wormholeExitRadius = AetheriaDefaultRaw.WormholeExitRadius,
|
||||
double wormholeDepth = AetheriaDefaultRaw.WormholeDepth,
|
||||
double visibilityDecay = AetheriaDefaultRaw.VisibilityDecay,
|
||||
double targetInfoDecay = AetheriaDefaultRaw.TargetInfoDecay,
|
||||
double secureAreaRadiusMultiplier = AetheriaDefaultRaw.SecureAreaRadiusMultiplier)
|
||||
{
|
||||
PawnSpeed = PositiveOr(pawnSpeed, AetheriaDefaultRaw.PawnSpeed);
|
||||
RaiderSpeed = PositiveOr(raiderSpeed, AetheriaDefaultRaw.RaiderSpeed);
|
||||
AttackRange = PositiveOr(attackRange, AetheriaDefaultRaw.AttackRange);
|
||||
AttackHoldRatio = Clamp01Or(attackHoldRatio, AetheriaDefaultRaw.AttackHoldRatio);
|
||||
PawnProjectileDamage = PositiveOr(pawnProjectileDamage, AetheriaDefaultRaw.PawnProjectileDamage);
|
||||
RaiderProjectileDamage = PositiveOr(raiderProjectileDamage, AetheriaDefaultRaw.RaiderProjectileDamage);
|
||||
WeaponCooldownSeconds = PositiveOr(weaponCooldownSeconds, AetheriaDefaultRaw.WeaponCooldownSeconds);
|
||||
ProjectileSpeed = PositiveOr(projectileSpeed, AetheriaDefaultRaw.ProjectileSpeed);
|
||||
ProjectileRadius = PositiveOr(projectileRadius, AetheriaDefaultRaw.ProjectileRadius);
|
||||
ProjectileLifetimeSeconds = PositiveOr(projectileLifetimeSeconds, AetheriaDefaultRaw.ProjectileLifetimeSeconds);
|
||||
ProjectileSpawnOffset = PositiveOr(projectileSpawnOffset, AetheriaDefaultRaw.ProjectileSpawnOffset);
|
||||
ProjectileHeatScale = NonNegativeOr(projectileHeatScale, AetheriaDefaultRaw.ProjectileHeatScale);
|
||||
HeatDissipationPerSecond = NonNegativeOr(heatDissipationPerSecond, AetheriaDefaultRaw.HeatDissipationPerSecond);
|
||||
StationSensorRange = PositiveOr(stationSensorRange, AetheriaDefaultRaw.StationSensorRange);
|
||||
EntitySensorRange = PositiveOr(entitySensorRange, AetheriaDefaultRaw.EntitySensorRange);
|
||||
PlayerStationHull = PositiveOr(playerStationHull, AetheriaDefaultRaw.PlayerStationHull);
|
||||
HostileStationHull = PositiveOr(hostileStationHull, AetheriaDefaultRaw.HostileStationHull);
|
||||
PlayerEntityHull = PositiveOr(playerEntityHull, AetheriaDefaultRaw.PlayerEntityHull);
|
||||
RaiderEntityHull = PositiveOr(raiderEntityHull, AetheriaDefaultRaw.RaiderEntityHull);
|
||||
StationShield = NonNegativeOr(stationShield, AetheriaDefaultRaw.StationShield);
|
||||
EntityShield = NonNegativeOr(entityShield, AetheriaDefaultRaw.EntityShield);
|
||||
WeaponLockSpeed = PositiveOr(weaponLockSpeed, AetheriaDefaultRaw.WeaponLockSpeed);
|
||||
WeaponLockSensorImpact = NonNegativeOr(weaponLockSensorImpact, AetheriaDefaultRaw.WeaponLockSensorImpact);
|
||||
WeaponLockAngleDegrees = PositiveOr(weaponLockAngleDegrees, AetheriaDefaultRaw.WeaponLockAngleDegrees);
|
||||
WeaponLockDirectionImpact = NonNegativeOr(weaponLockDirectionImpact, AetheriaDefaultRaw.WeaponLockDirectionImpact);
|
||||
WeaponLockDecayPerSecond = NonNegativeOr(weaponLockDecayPerSecond, AetheriaDefaultRaw.WeaponLockDecayPerSecond);
|
||||
LootDropProbability = Clamp01Or(lootDropProbability, AetheriaDefaultRaw.LootDropProbability);
|
||||
LootDropVelocity = NonNegativeOr(lootDropVelocity, AetheriaDefaultRaw.LootDropVelocity);
|
||||
PickupLifetimeSeconds = PositiveOr(pickupLifetimeSeconds, AetheriaDefaultRaw.PickupLifetimeSeconds);
|
||||
SevereThermalRiskThreshold = Clamp01Or(severeThermalRiskThreshold, AetheriaDefaultRaw.SevereThermalRiskThreshold);
|
||||
HeatstrokeTemperature = PositiveOr(heatstrokeTemperature, AetheriaDefaultRaw.HeatstrokeTemperature);
|
||||
HeatstrokeMultiplier = NonNegativeOr(heatstrokeMultiplier, AetheriaDefaultRaw.HeatstrokeMultiplier);
|
||||
HeatstrokeExponent = PositiveOr(heatstrokeExponent, AetheriaDefaultRaw.HeatstrokeExponent);
|
||||
HeatstrokeRecoveryPerSecond = NonNegativeOr(heatstrokeRecoveryPerSecond, AetheriaDefaultRaw.HeatstrokeRecoveryPerSecond);
|
||||
HypothermiaTemperature = PositiveOr(hypothermiaTemperature, AetheriaDefaultRaw.HypothermiaTemperature);
|
||||
HypothermiaMultiplier = NonNegativeOr(hypothermiaMultiplier, AetheriaDefaultRaw.HypothermiaMultiplier);
|
||||
HypothermiaExponent = PositiveOr(hypothermiaExponent, AetheriaDefaultRaw.HypothermiaExponent);
|
||||
HypothermiaRecoveryPerSecond = NonNegativeOr(hypothermiaRecoveryPerSecond, AetheriaDefaultRaw.HypothermiaRecoveryPerSecond);
|
||||
AetherTorqueMultiplier = NonNegativeOr(aetherTorqueMultiplier, AetheriaDefaultRaw.AetherTorqueMultiplier);
|
||||
AetherHeatMultiplier = NonNegativeOr(aetherHeatMultiplier, AetheriaDefaultRaw.AetherHeatMultiplier);
|
||||
TorqueFloor = NonNegativeOr(torqueFloor, AetheriaDefaultRaw.TorqueFloor);
|
||||
TorqueMultiplier = NonNegativeOr(torqueMultiplier, AetheriaDefaultRaw.TorqueMultiplier);
|
||||
AgentRangeExponent = NonNegativeOr(agentRangeExponent, AetheriaDefaultRaw.AgentRangeExponent);
|
||||
AgentForwardLerp = Clamp01Or(agentForwardLerp, AetheriaDefaultRaw.AgentForwardLerp);
|
||||
AgentMaxForwardDistance = PositiveOr(agentMaxForwardDistance, AetheriaDefaultRaw.AgentMaxForwardDistance);
|
||||
AgentDpsSampleCount = agentDpsSampleCount > 0 ? agentDpsSampleCount : AetheriaDefaultRaw.AgentDpsSampleCount;
|
||||
WormholeAnimationDuration = PositiveOr(wormholeAnimationDuration, AetheriaDefaultRaw.WormholeAnimationDuration);
|
||||
WormholeExitCurveStart = Clamp01Or(wormholeExitCurveStart, AetheriaDefaultRaw.WormholeExitCurveStart);
|
||||
WormholeExitVelocity = PositiveOr(wormholeExitVelocity, AetheriaDefaultRaw.WormholeExitVelocity);
|
||||
WormholeExitRadius = PositiveOr(wormholeExitRadius, AetheriaDefaultRaw.WormholeExitRadius);
|
||||
WormholeDepth = PositiveOr(wormholeDepth, AetheriaDefaultRaw.WormholeDepth);
|
||||
VisibilityDecay = NonNegativeOr(visibilityDecay, AetheriaDefaultRaw.VisibilityDecay);
|
||||
TargetInfoDecay = NonNegativeOr(targetInfoDecay, AetheriaDefaultRaw.TargetInfoDecay);
|
||||
SecureAreaRadiusMultiplier = PositiveOr(
|
||||
secureAreaRadiusMultiplier, AetheriaDefaultRaw.SecureAreaRadiusMultiplier);
|
||||
}
|
||||
|
||||
public double PawnSpeed { get; }
|
||||
public double RaiderSpeed { get; }
|
||||
public double AttackRange { get; }
|
||||
public double AttackHoldRatio { get; }
|
||||
public double PawnProjectileDamage { get; }
|
||||
public double RaiderProjectileDamage { get; }
|
||||
public double WeaponCooldownSeconds { get; }
|
||||
public double ProjectileSpeed { get; }
|
||||
public double ProjectileRadius { get; }
|
||||
public double ProjectileLifetimeSeconds { get; }
|
||||
public double ProjectileSpawnOffset { get; }
|
||||
public double ProjectileHeatScale { get; }
|
||||
public double HeatDissipationPerSecond { get; }
|
||||
[System.Obsolete("Sensor reach derives from installed equipment. Retained only for snapshot compatibility.")]
|
||||
public double StationSensorRange { get; }
|
||||
public double FallbackSensorRange => EntitySensorRange;
|
||||
public double EntitySensorRange { get; }
|
||||
public double PlayerStationHull { get; }
|
||||
public double HostileStationHull { get; }
|
||||
public double PlayerEntityHull { get; }
|
||||
public double RaiderEntityHull { get; }
|
||||
public double StationShield { get; }
|
||||
public double EntityShield { get; }
|
||||
public double WeaponLockSpeed { get; }
|
||||
public double WeaponLockSensorImpact { get; }
|
||||
public double WeaponLockAngleDegrees { get; }
|
||||
public double WeaponLockDirectionImpact { get; }
|
||||
public double WeaponLockDecayPerSecond { get; }
|
||||
public double LootDropProbability { get; }
|
||||
public double LootDropVelocity { get; }
|
||||
public double PickupLifetimeSeconds { get; }
|
||||
public double SevereThermalRiskThreshold { get; }
|
||||
public double HeatstrokeTemperature { get; }
|
||||
public double HeatstrokeMultiplier { get; }
|
||||
public double HeatstrokeExponent { get; }
|
||||
public double HeatstrokeRecoveryPerSecond { get; }
|
||||
public double HypothermiaTemperature { get; }
|
||||
public double HypothermiaMultiplier { get; }
|
||||
public double HypothermiaExponent { get; }
|
||||
public double HypothermiaRecoveryPerSecond { get; }
|
||||
public double AetherTorqueMultiplier { get; }
|
||||
public double AetherHeatMultiplier { get; }
|
||||
public double TorqueFloor { get; }
|
||||
public double TorqueMultiplier { get; }
|
||||
public double AgentRangeExponent { get; }
|
||||
public double AgentForwardLerp { get; }
|
||||
public double AgentMaxForwardDistance { get; }
|
||||
public int AgentDpsSampleCount { get; }
|
||||
public double WormholeAnimationDuration { get; }
|
||||
public double WormholeExitCurveStart { get; }
|
||||
public double WormholeExitVelocity { get; }
|
||||
public double WormholeExitRadius { get; }
|
||||
public double WormholeDepth { get; }
|
||||
public double VisibilityDecay { get; }
|
||||
public double TargetInfoDecay { get; }
|
||||
public double SecureAreaRadiusMultiplier { get; }
|
||||
|
||||
private static double PositiveOr(double value, double fallback)
|
||||
{
|
||||
return double.IsFinite(value) && value > 0 ? value : fallback;
|
||||
}
|
||||
|
||||
private static double NonNegativeOr(double value, double fallback)
|
||||
{
|
||||
return double.IsFinite(value) && value >= 0 ? value : fallback;
|
||||
}
|
||||
|
||||
private static double Clamp01Or(double value, double fallback)
|
||||
{
|
||||
return double.IsFinite(value) ? value < 0 ? 0 : value > 1 ? 1 : value : fallback;
|
||||
}
|
||||
|
||||
private static class AetheriaDefaultRaw
|
||||
{
|
||||
public const double PawnSpeed = 84.0;
|
||||
public const double RaiderSpeed = 68.0;
|
||||
public const double AttackRange = 145.0;
|
||||
public const double AttackHoldRatio = 0.82;
|
||||
public const double PawnProjectileDamage = 18.0;
|
||||
public const double RaiderProjectileDamage = 12.0;
|
||||
public const double WeaponCooldownSeconds = 0.55;
|
||||
public const double ProjectileSpeed = 330.0;
|
||||
public const double ProjectileRadius = 18.0;
|
||||
public const double ProjectileLifetimeSeconds = 2.2;
|
||||
public const double ProjectileSpawnOffset = 18.0;
|
||||
public const double ProjectileHeatScale = 0.18;
|
||||
public const double HeatDissipationPerSecond = 8.0;
|
||||
public const double StationSensorRange = 720.0;
|
||||
public const double EntitySensorRange = 520.0;
|
||||
public const double PlayerStationHull = 420.0;
|
||||
public const double HostileStationHull = 240.0;
|
||||
public const double PlayerEntityHull = 120.0;
|
||||
public const double RaiderEntityHull = 80.0;
|
||||
public const double StationShield = 120.0;
|
||||
public const double EntityShield = 45.0;
|
||||
public const double WeaponLockSpeed = 2.0;
|
||||
public const double WeaponLockSensorImpact = 1.0;
|
||||
public const double WeaponLockAngleDegrees = 45.0;
|
||||
public const double WeaponLockDirectionImpact = 1.0;
|
||||
public const double WeaponLockDecayPerSecond = 1.0;
|
||||
public const double LootDropProbability = 0.25;
|
||||
public const double LootDropVelocity = 25.0;
|
||||
public const double PickupLifetimeSeconds = 30.0;
|
||||
public const double SevereThermalRiskThreshold = 0.25;
|
||||
public const double HeatstrokeTemperature = 330.0;
|
||||
public const double HeatstrokeMultiplier = 0.00001;
|
||||
public const double HeatstrokeExponent = 2.0;
|
||||
public const double HeatstrokeRecoveryPerSecond = 0.2;
|
||||
public const double HypothermiaTemperature = 273.0;
|
||||
public const double HypothermiaMultiplier = 0.00001;
|
||||
public const double HypothermiaExponent = 2.0;
|
||||
public const double HypothermiaRecoveryPerSecond = 0.2;
|
||||
public const double AetherTorqueMultiplier = 0.1;
|
||||
public const double AetherHeatMultiplier = 0.25;
|
||||
public const double TorqueFloor = 0.5;
|
||||
public const double TorqueMultiplier = 0.1;
|
||||
public const double AgentRangeExponent = 0.25;
|
||||
public const double AgentForwardLerp = 0.5;
|
||||
public const double AgentMaxForwardDistance = 50.0;
|
||||
public const int AgentDpsSampleCount = 32;
|
||||
public const double WormholeAnimationDuration = 4.0;
|
||||
public const double WormholeExitCurveStart = 0.8;
|
||||
public const double WormholeExitVelocity = 20.0;
|
||||
public const double WormholeExitRadius = 50.0;
|
||||
public const double WormholeDepth = 1000.0;
|
||||
public const double VisibilityDecay = 0.5;
|
||||
public const double TargetInfoDecay = 0.5;
|
||||
public const double SecureAreaRadiusMultiplier = 0.425;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7a04293019e69804188e0fd89165928e
|
||||
|
|
@ -1,298 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GameCult.Caching;
|
||||
using MessagePack;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public static class AetheriaRuntimeDaemonSoaBackends
|
||||
{
|
||||
public const string CultCache = "cultcache";
|
||||
public const string CultMesh = "cultmesh";
|
||||
public const string MemoryMappedFile = "memory_mapped_file";
|
||||
public const string SharedNativeMemory = "shared_native_memory";
|
||||
}
|
||||
|
||||
public static class AetheriaRuntimeDaemonSoaSynchronizationModes
|
||||
{
|
||||
public const string ImmutableFrame = "immutable_frame";
|
||||
public const string DoubleBuffered = "double_buffered";
|
||||
public const string TripleBuffered = "triple_buffered";
|
||||
}
|
||||
|
||||
public static class AetheriaRuntimeDaemonSoaColumnKinds
|
||||
{
|
||||
public const string EntityIndex = "entity.index";
|
||||
public const string EntityKey = "entity.key";
|
||||
public const string ZoneIndex = "entity.zone_index";
|
||||
public const string Position = "transform.position";
|
||||
public const string RotationRadians = "transform.rotation.radians";
|
||||
public const string Velocity = "motion.velocity";
|
||||
public const string PhysicsBodyRadius = "physics.body.radius";
|
||||
public const string PhysicsBodyMass = "physics.body.mass";
|
||||
public const string PhysicsBodyInverseMass = "physics.body.inverse_mass";
|
||||
public const string Hull = "stats.hull";
|
||||
public const string Heat = "stats.heat";
|
||||
public const string Quality = "stats.quality";
|
||||
public const string Durability = "stats.durability";
|
||||
public const string RenderGroupId = "render.group.id";
|
||||
public const string RenderScale = "render.scale";
|
||||
public const string RenderVisibility = "render.visibility";
|
||||
public const string RenderLod = "render.lod";
|
||||
public const string CargoQuantity = "inventory.cargo.quantity";
|
||||
public const string BeamPower = "effect.beam.power";
|
||||
}
|
||||
|
||||
public static class AetheriaRuntimeDaemonRenderShadowModes
|
||||
{
|
||||
public const string Off = "off";
|
||||
public const string On = "on";
|
||||
public const string TwoSided = "two_sided";
|
||||
public const string ShadowsOnly = "shadows_only";
|
||||
}
|
||||
|
||||
[CultDocument("gamecult.aetheria.daemon_soa_view", "gamecult.aetheria.daemon_soa_view.v1")]
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaRuntimeDaemonSoaViewDocument
|
||||
{
|
||||
[Key(0)]
|
||||
public string Schema { get; set; } = AetheriaRuntimeDaemonSchemas.SoaView;
|
||||
|
||||
[Key(1)]
|
||||
public string DaemonId { get; set; } = "local";
|
||||
|
||||
[Key(2)]
|
||||
public string SessionId { get; set; } = "local";
|
||||
|
||||
[Key(3)]
|
||||
public long FrameId { get; set; }
|
||||
|
||||
[Key(4)]
|
||||
public long Generation { get; set; }
|
||||
|
||||
[Key(5)]
|
||||
public string PublishedAtUtc { get; set; } = "";
|
||||
|
||||
[Key(6)]
|
||||
public bool IsAuthoritative { get; set; } = true;
|
||||
|
||||
[Key(7)]
|
||||
public string Backend { get; set; } = AetheriaRuntimeDaemonSoaBackends.CultCache;
|
||||
|
||||
[Key(8)]
|
||||
public string SynchronizationMode { get; set; } = AetheriaRuntimeDaemonSoaSynchronizationModes.ImmutableFrame;
|
||||
|
||||
[Key(9)]
|
||||
public IReadOnlyList<AetheriaRuntimeDaemonSoaBufferDocument> Buffers { get; set; } =
|
||||
Array.Empty<AetheriaRuntimeDaemonSoaBufferDocument>();
|
||||
|
||||
[Key(10)]
|
||||
public IReadOnlyList<AetheriaRuntimeDaemonSoaColumnDocument> Columns { get; set; } =
|
||||
Array.Empty<AetheriaRuntimeDaemonSoaColumnDocument>();
|
||||
|
||||
[Key(11)]
|
||||
public IReadOnlyList<AetheriaRuntimeDaemonSoaDirtyRangeDocument> DirtyRanges { get; set; } =
|
||||
Array.Empty<AetheriaRuntimeDaemonSoaDirtyRangeDocument>();
|
||||
|
||||
[Key(12)]
|
||||
public IReadOnlyList<AetheriaRuntimeDaemonRenderGroupDocument> RenderGroups { get; set; } =
|
||||
Array.Empty<AetheriaRuntimeDaemonRenderGroupDocument>();
|
||||
|
||||
[Key(13)]
|
||||
public IReadOnlyList<AetheriaRuntimeDaemonSoaIdentityDocument> Identities { get; set; } =
|
||||
Array.Empty<AetheriaRuntimeDaemonSoaIdentityDocument>();
|
||||
|
||||
public static AetheriaRuntimeDaemonSoaViewDocument Create(
|
||||
string daemonId,
|
||||
string sessionId,
|
||||
long frameId,
|
||||
long generation,
|
||||
IReadOnlyList<AetheriaRuntimeDaemonSoaBufferDocument>? buffers,
|
||||
IReadOnlyList<AetheriaRuntimeDaemonSoaColumnDocument>? columns,
|
||||
IReadOnlyList<AetheriaRuntimeDaemonSoaDirtyRangeDocument>? dirtyRanges = null,
|
||||
string backend = AetheriaRuntimeDaemonSoaBackends.CultCache,
|
||||
string synchronizationMode = AetheriaRuntimeDaemonSoaSynchronizationModes.ImmutableFrame,
|
||||
bool isAuthoritative = true,
|
||||
IReadOnlyList<AetheriaRuntimeDaemonRenderGroupDocument>? renderGroups = null,
|
||||
IReadOnlyList<AetheriaRuntimeDaemonSoaIdentityDocument>? identities = null)
|
||||
{
|
||||
return new AetheriaRuntimeDaemonSoaViewDocument
|
||||
{
|
||||
DaemonId = string.IsNullOrWhiteSpace(daemonId) ? "local" : daemonId,
|
||||
SessionId = string.IsNullOrWhiteSpace(sessionId) ? "local" : sessionId,
|
||||
FrameId = frameId,
|
||||
Generation = generation,
|
||||
PublishedAtUtc = DateTime.UtcNow.ToString("O"),
|
||||
IsAuthoritative = isAuthoritative,
|
||||
Backend = string.IsNullOrWhiteSpace(backend) ? AetheriaRuntimeDaemonSoaBackends.CultCache : backend,
|
||||
SynchronizationMode = string.IsNullOrWhiteSpace(synchronizationMode)
|
||||
? AetheriaRuntimeDaemonSoaSynchronizationModes.ImmutableFrame
|
||||
: synchronizationMode,
|
||||
Buffers = buffers ?? Array.Empty<AetheriaRuntimeDaemonSoaBufferDocument>(),
|
||||
Columns = columns ?? Array.Empty<AetheriaRuntimeDaemonSoaColumnDocument>(),
|
||||
DirtyRanges = dirtyRanges ?? Array.Empty<AetheriaRuntimeDaemonSoaDirtyRangeDocument>(),
|
||||
RenderGroups = renderGroups ?? Array.Empty<AetheriaRuntimeDaemonRenderGroupDocument>(),
|
||||
Identities = identities ?? Array.Empty<AetheriaRuntimeDaemonSoaIdentityDocument>()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaRuntimeDaemonSoaIdentityDocument
|
||||
{
|
||||
[Key(0)] public int EntityIndex { get; set; } = -1;
|
||||
[Key(1)] public string EntityId { get; set; } = "";
|
||||
[Key(2)] public string Kind { get; set; } = "";
|
||||
[Key(3)] public string Label { get; set; } = "";
|
||||
[Key(4)] public string Faction { get; set; } = "";
|
||||
[Key(5)] public bool Selectable { get; set; } = true;
|
||||
[Key(6)] public bool Controllable { get; set; }
|
||||
[Key(7)] public string AssetRef { get; set; } = "";
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaRuntimeDaemonRenderGroupDocument
|
||||
{
|
||||
[Key(0)]
|
||||
public int GroupId { get; set; }
|
||||
|
||||
[Key(1)]
|
||||
public string MeshKey { get; set; } = "";
|
||||
|
||||
[Key(2)]
|
||||
public string MaterialKey { get; set; } = "";
|
||||
|
||||
[Key(3)]
|
||||
public int SubMeshIndex { get; set; }
|
||||
|
||||
[Key(4)]
|
||||
public int Layer { get; set; }
|
||||
|
||||
[Key(5)]
|
||||
public string ShaderKey { get; set; } = "";
|
||||
|
||||
[Key(6)]
|
||||
public string DisplayName { get; set; } = "";
|
||||
|
||||
[Key(7)]
|
||||
public int InstanceCount { get; set; } = -1;
|
||||
|
||||
[Key(8)]
|
||||
public float BoundsCenterX { get; set; }
|
||||
|
||||
[Key(9)]
|
||||
public float BoundsCenterY { get; set; }
|
||||
|
||||
[Key(10)]
|
||||
public float BoundsCenterZ { get; set; }
|
||||
|
||||
[Key(11)]
|
||||
public float BoundsSizeX { get; set; } = -1.0f;
|
||||
|
||||
[Key(12)]
|
||||
public float BoundsSizeY { get; set; } = -1.0f;
|
||||
|
||||
[Key(13)]
|
||||
public float BoundsSizeZ { get; set; } = -1.0f;
|
||||
|
||||
[Key(14)]
|
||||
public string ShadowMode { get; set; } = AetheriaRuntimeDaemonRenderShadowModes.On;
|
||||
|
||||
[Key(15)]
|
||||
public bool ReceiveShadows { get; set; } = true;
|
||||
|
||||
[Key(16)]
|
||||
public float DefaultScale { get; set; } = 1.0f;
|
||||
|
||||
[Key(17)]
|
||||
public int Lod { get; set; } = -1;
|
||||
|
||||
[Key(18)]
|
||||
public AetheriaRuntimeAssetRef MeshAsset { get; set; } =
|
||||
AetheriaRuntimeAssetRef.Empty(AetheriaRuntimeAssetKinds.Mesh);
|
||||
|
||||
[Key(19)]
|
||||
public AetheriaRuntimeAssetRef MaterialAsset { get; set; } =
|
||||
AetheriaRuntimeAssetRef.Empty(AetheriaRuntimeAssetKinds.Material);
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaRuntimeDaemonSoaBufferDocument
|
||||
{
|
||||
[Key(0)]
|
||||
public string BufferId { get; set; } = "";
|
||||
|
||||
[Key(1)]
|
||||
public string DisplayName { get; set; } = "";
|
||||
|
||||
[Key(2)]
|
||||
public string Backend { get; set; } = AetheriaRuntimeDaemonSoaBackends.CultCache;
|
||||
|
||||
[Key(3)]
|
||||
public string Location { get; set; } = "";
|
||||
|
||||
[Key(4)]
|
||||
public long ByteOffset { get; set; }
|
||||
|
||||
[Key(5)]
|
||||
public long ByteLength { get; set; }
|
||||
|
||||
[Key(6)]
|
||||
public long Generation { get; set; }
|
||||
|
||||
[Key(7)]
|
||||
public bool DaemonWritable { get; set; } = true;
|
||||
|
||||
[Key(8)]
|
||||
public bool ObserverWritable { get; set; }
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaRuntimeDaemonSoaColumnDocument
|
||||
{
|
||||
[Key(0)]
|
||||
public string ColumnId { get; set; } = "";
|
||||
|
||||
[Key(1)]
|
||||
public string Kind { 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 AetheriaRuntimeDaemonSoaDirtyRangeDocument
|
||||
{
|
||||
[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 Generation { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: aaba35482c606b241adcf479cbfa4298
|
||||
|
|
@ -1,953 +0,0 @@
|
|||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using GameCult.Eve.Surface;
|
||||
using GameCult.Mesh;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public sealed class AetheriaRuntimeDaemonSoaFrame : IDisposable
|
||||
{
|
||||
private CultMeshFrameBodyWriteLease? _write;
|
||||
|
||||
public AetheriaRuntimeDaemonSoaFrame(
|
||||
AetheriaRuntimeDaemonSoaViewDocument view,
|
||||
CultMeshFrameBodyWriteLease write,
|
||||
int byteLength)
|
||||
{
|
||||
View = view ?? throw new ArgumentNullException(nameof(view));
|
||||
_write = write ?? throw new ArgumentNullException(nameof(write));
|
||||
ByteLength = byteLength;
|
||||
}
|
||||
|
||||
public AetheriaRuntimeDaemonSoaViewDocument View { get; }
|
||||
public int ByteLength { get; }
|
||||
|
||||
public CultMeshBodyDescriptor Commit(DateTimeOffset nowUtc)
|
||||
{
|
||||
var write = _write ?? throw new ObjectDisposedException(nameof(AetheriaRuntimeDaemonSoaFrame));
|
||||
var descriptor = write.Commit(ByteLength, nowUtc);
|
||||
_write = null;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_write?.Dispose();
|
||||
_write = null;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimeDaemonSoaPublication
|
||||
{
|
||||
public AetheriaRuntimeDaemonSoaPublication(
|
||||
CultMeshBodyPublicationDocument body,
|
||||
EveEntitySoaViewDocument view,
|
||||
ReadOnlyMemory<byte> realtimePayload)
|
||||
{
|
||||
Body = body ?? throw new ArgumentNullException(nameof(body));
|
||||
View = view ?? throw new ArgumentNullException(nameof(view));
|
||||
RealtimePayload = realtimePayload;
|
||||
}
|
||||
|
||||
public CultMeshBodyPublicationDocument Body { get; }
|
||||
public EveEntitySoaViewDocument View { get; }
|
||||
public ReadOnlyMemory<byte> RealtimePayload { get; }
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimeDaemonSoaFramePublisher : IDisposable
|
||||
{
|
||||
public const string BodyId = "eve:entity-soa:aetheria.daemon:pilot";
|
||||
public const string ProducerId = "aetheria.daemon";
|
||||
public const string BodySchemaId = "gamecult.eve.entity_soa.body.v2";
|
||||
public const int LayoutVersion = 3;
|
||||
private const int Capacity = 4096;
|
||||
private const int EntityRenderGroupId = 1;
|
||||
private const int FloatStride = 4;
|
||||
private const int Float3Stride = 12;
|
||||
private const int IntStride = 4;
|
||||
private const int ByteStride = 1;
|
||||
private const string EntityProxyMeshAssetKey = "daemon.entity_proxy.mesh";
|
||||
private const string EntityProxyMaterialAssetKey = "daemon.entity_proxy.material";
|
||||
private const string EntityProxyMeshUri = "cultmesh://aetheria/assets/daemon/entity_proxy/mesh";
|
||||
private const string EntityProxyMaterialUri = "cultmesh://aetheria/assets/daemon/entity_proxy/material";
|
||||
|
||||
private readonly CultMeshFrameBodyPublisher _localPublisher;
|
||||
private readonly CultMeshBodyDemandTracker? _demand;
|
||||
private readonly Dictionary<string, int> _syntheticEntityIndices =
|
||||
new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
private int _nextSyntheticEntityIndex = -2;
|
||||
|
||||
public AetheriaRuntimeDaemonSoaFramePublisher(
|
||||
long producerEpoch,
|
||||
CultMeshBodyDemandTracker? demand = null)
|
||||
{
|
||||
_localPublisher = new CultMeshFrameBodyPublisher(
|
||||
BodyId, BodySchemaId, LayoutVersion, Capacity, producerEpoch,
|
||||
checked((int)EntityHotSlabLayout.Create(Capacity).TotalByteLength),
|
||||
TimeSpan.FromSeconds(30));
|
||||
_demand = demand;
|
||||
}
|
||||
|
||||
public CultMeshBodyDemandPlan? CurrentDemandPlan() => _demand?.Plan(BodyId);
|
||||
|
||||
public AetheriaRuntimeDaemonSoaFrame? BuildCurrentZoneEntities(
|
||||
AetheriaRuntimeDaemonFrameDocument frame,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog = null,
|
||||
bool realtimeDemand = false)
|
||||
{
|
||||
if (frame == null)
|
||||
throw new ArgumentNullException(nameof(frame));
|
||||
var demand = _demand?.Plan(BodyId);
|
||||
if (demand != null &&
|
||||
(!demand.HasConsumers ||
|
||||
(!demand.RequiresSharedMemory && !demand.RequiresSharedFileMapping && !realtimeDemand)))
|
||||
return null;
|
||||
|
||||
var run = frame.Run ?? new AetheriaRuntimeRunCheckpointCommit();
|
||||
var zone = (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
|
||||
.FirstOrDefault(candidate => candidate != null && candidate.ZoneIndex == run.CurrentZoneIndex);
|
||||
AetheriaRuntimeRunCheckpointCommit.TryParseEntityKey(
|
||||
run.CurrentEntityKey,
|
||||
out var controlledZoneIndex,
|
||||
out var controlledEntityIndex);
|
||||
var controlled = controlledZoneIndex == run.CurrentZoneIndex
|
||||
? (zone?.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
|
||||
.FirstOrDefault(entity => entity != null && entity.EntityIndex == controlledEntityIndex)
|
||||
: null;
|
||||
var dockParent = (zone?.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
|
||||
.FirstOrDefault(entity => entity != null && controlled != null &&
|
||||
(entity.DockingBayAssignments ?? Array.Empty<int>()).Contains(controlled.EntityIndex));
|
||||
var visibleEntityIndices = AetheriaRuntimeDaemonRenderQueries
|
||||
.QueryEffectiveContacts(zone, controlled?.EntityIndex ?? -1)
|
||||
.Where(contact => contact.Contact.Visible)
|
||||
.Select(contact => contact.Contact.TargetEntityIndex)
|
||||
.Append(controlled?.EntityIndex ?? -1)
|
||||
.ToHashSet();
|
||||
if (dockParent != null)
|
||||
visibleEntityIndices.Add(dockParent.EntityIndex);
|
||||
var fieldContributorIndices = (zone?.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
|
||||
.Where(AetheriaRuntimeGameDocuments.HasActiveFogFieldEmitter)
|
||||
.Select(entity => entity.EntityIndex)
|
||||
.ToHashSet();
|
||||
var entities = (zone?.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
|
||||
.Where(entity => entity != null &&
|
||||
(visibleEntityIndices.Contains(entity.EntityIndex) ||
|
||||
fieldContributorIndices.Contains(entity.EntityIndex)))
|
||||
.OrderBy(entity => entity.EntityIndex)
|
||||
.ToArray();
|
||||
var pickups = (zone?.DroppedPickups ?? Array.Empty<AetheriaRuntimeDroppedPickupCommit>())
|
||||
.Where(pickup => pickup != null)
|
||||
.OrderBy(pickup => pickup.PickupIndex)
|
||||
.ToArray();
|
||||
var payloads = (zone?.PhysicalPayloads ?? Array.Empty<AetheriaRuntimePhysicalPayloadCommit>())
|
||||
.Where(payload => payload != null && payload.Active)
|
||||
.OrderBy(payload => payload.PayloadId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
var celestialBodies = BuildCelestialBodies(frame, zone);
|
||||
var asteroidInstances = BuildAsteroidInstances(frame, zone);
|
||||
var unsupportedPayload = payloads.FirstOrDefault(payload =>
|
||||
!string.Equals(payload.PayloadKind, "mine", StringComparison.Ordinal));
|
||||
if (unsupportedPayload != null)
|
||||
throw new InvalidOperationException(
|
||||
$"Physical payload '{unsupportedPayload.PayloadId}' uses unsupported provider asset kind '{unsupportedPayload.PayloadKind}'.");
|
||||
var pickupEntityIds = pickups
|
||||
.Select(pickup => $"pickup:{run.CurrentZoneIndex}:{pickup.PickupIndex}")
|
||||
.ToArray();
|
||||
var payloadEntityIds = payloads
|
||||
.Select(payload => $"{run.RunId}:zone:{run.CurrentZoneIndex}:physical-payload:{payload.PayloadId}")
|
||||
.ToArray();
|
||||
var syntheticEntityIds = pickupEntityIds
|
||||
.Concat(payloadEntityIds)
|
||||
.Concat(celestialBodies.Select(value => value.EntityId))
|
||||
.Concat(asteroidInstances.Select(value => value.EntityId))
|
||||
.ToArray();
|
||||
if (syntheticEntityIds.Distinct(StringComparer.Ordinal).Count() != syntheticEntityIds.Length)
|
||||
throw new InvalidOperationException("Current-zone synthetic SoA entity identities are not unique.");
|
||||
var syntheticEntityIndices = syntheticEntityIds.ToDictionary(
|
||||
entityId => entityId,
|
||||
GetOrAllocateSyntheticEntityIndex,
|
||||
StringComparer.Ordinal);
|
||||
var count = entities.Length + pickups.Length + payloads.Length +
|
||||
celestialBodies.Count + asteroidInstances.Count;
|
||||
if (count > Capacity)
|
||||
throw new InvalidOperationException($"Aetheria entity SoA capacity {Capacity} was exceeded by {count} rows.");
|
||||
var generation = Math.Max(frame.FrameId, 0);
|
||||
// LayoutVersion owns byte interpretation. Keep offsets and body extent fixed for the
|
||||
// publisher capacity so a mapped frame may advance before reactive row metadata
|
||||
// without causing the same layout version to reinterpret its bytes.
|
||||
var layout = EntityHotSlabLayout.Create(Capacity);
|
||||
if (!_localPublisher.TryAcquireWrite(out var write))
|
||||
throw new InvalidOperationException("CultMesh has no unleased frame slot for the Aetheria SoA generation.");
|
||||
try
|
||||
{
|
||||
var bytes = write.Span[..checked((int)layout.TotalByteLength)];
|
||||
bytes.Clear();
|
||||
WriteEntities(bytes, layout, entities, visibleEntityIndices);
|
||||
WritePickups(bytes, layout, pickups, pickupEntityIds, syntheticEntityIndices, entities.Length);
|
||||
WritePayloads(bytes, layout, payloads, payloadEntityIds, syntheticEntityIndices, entities.Length + pickups.Length);
|
||||
WriteCelestialBodies(
|
||||
bytes,
|
||||
layout,
|
||||
celestialBodies,
|
||||
syntheticEntityIndices,
|
||||
entities.Length + pickups.Length + payloads.Length);
|
||||
WriteAsteroidInstances(
|
||||
bytes,
|
||||
layout,
|
||||
asteroidInstances,
|
||||
syntheticEntityIndices,
|
||||
entities.Length + pickups.Length + payloads.Length + celestialBodies.Count);
|
||||
|
||||
var view = AetheriaRuntimeDaemonSoaViewDocument.Create(
|
||||
string.IsNullOrWhiteSpace(frame.DaemonId) ? "aetheria-daemon" : frame.DaemonId,
|
||||
string.IsNullOrWhiteSpace(frame.SessionId) ? "local" : frame.SessionId,
|
||||
frame.FrameId,
|
||||
generation,
|
||||
new[]
|
||||
{
|
||||
new AetheriaRuntimeDaemonSoaBufferDocument
|
||||
{
|
||||
BufferId = BodyId,
|
||||
DisplayName = "Current zone daemon entity hot slab",
|
||||
ByteOffset = 0,
|
||||
ByteLength = layout.TotalByteLength,
|
||||
Generation = generation,
|
||||
DaemonWritable = true,
|
||||
ObserverWritable = false
|
||||
}
|
||||
},
|
||||
layout.CreateColumns(count),
|
||||
layout.CreateDirtyRanges(count, generation),
|
||||
backend: AetheriaRuntimeDaemonSoaBackends.CultMesh,
|
||||
synchronizationMode: AetheriaRuntimeDaemonSoaSynchronizationModes.ImmutableFrame,
|
||||
renderGroups: CreateRenderGroups(entities, pickups, payloads, celestialBodies, asteroidInstances),
|
||||
identities: entities.Select(entity => new AetheriaRuntimeDaemonSoaIdentityDocument
|
||||
{
|
||||
EntityIndex = entity.EntityIndex,
|
||||
EntityId = entity.EntityId,
|
||||
Kind = entity.Kind,
|
||||
Label = entity.Name,
|
||||
Faction = entity.FactionKey,
|
||||
Selectable = visibleEntityIndices.Contains(entity.EntityIndex) &&
|
||||
entity.EntityIndex != controlled?.EntityIndex,
|
||||
Controllable = visibleEntityIndices.Contains(entity.EntityIndex) &&
|
||||
entity.EntityIndex == controlled?.EntityIndex,
|
||||
AssetRef = AetheriaRuntimeAssets.ResolveEntityPrefabAssetRef(entity, catalog)
|
||||
})
|
||||
.Concat(pickups.Select((pickup, pickupRow) => new AetheriaRuntimeDaemonSoaIdentityDocument
|
||||
{
|
||||
EntityIndex = syntheticEntityIndices[pickupEntityIds[pickupRow]],
|
||||
EntityId = pickupEntityIds[pickupRow],
|
||||
Kind = "pickup",
|
||||
Label = string.IsNullOrWhiteSpace(pickup.Item?.ItemKey) ? "Pickup" : pickup.Item.ItemKey,
|
||||
Faction = "",
|
||||
Selectable = true,
|
||||
Controllable = false,
|
||||
AssetRef = "prefab.entity.pickup"
|
||||
}))
|
||||
.Concat(payloads.Select((payload, payloadRow) => new AetheriaRuntimeDaemonSoaIdentityDocument
|
||||
{
|
||||
EntityIndex = syntheticEntityIndices[payloadEntityIds[payloadRow]],
|
||||
EntityId = payloadEntityIds[payloadRow],
|
||||
Kind = "physical-payload",
|
||||
Label = string.IsNullOrWhiteSpace(payload.PayloadKind) ? "Physical payload" : payload.PayloadKind,
|
||||
Faction = payload.FactionKey ?? "",
|
||||
Selectable = false,
|
||||
Controllable = false,
|
||||
AssetRef = "prefab.entity.mine"
|
||||
}))
|
||||
.Concat(celestialBodies.Select(value => new AetheriaRuntimeDaemonSoaIdentityDocument
|
||||
{
|
||||
EntityIndex = syntheticEntityIndices[value.EntityId],
|
||||
EntityId = value.EntityId,
|
||||
Kind = value.EntityKind,
|
||||
Label = value.Label,
|
||||
Faction = "",
|
||||
Selectable = false,
|
||||
Controllable = false,
|
||||
AssetRef = value.AssetRef
|
||||
}))
|
||||
.Concat(asteroidInstances.Select(value => new AetheriaRuntimeDaemonSoaIdentityDocument
|
||||
{
|
||||
EntityIndex = syntheticEntityIndices[value.EntityId],
|
||||
EntityId = value.EntityId,
|
||||
Kind = "celestial.asteroid",
|
||||
Label = "Asteroid",
|
||||
Faction = "",
|
||||
Selectable = false,
|
||||
Controllable = false,
|
||||
AssetRef = "prefab.body.asteroid"
|
||||
}))
|
||||
.ToArray());
|
||||
|
||||
return new AetheriaRuntimeDaemonSoaFrame(
|
||||
view,
|
||||
write,
|
||||
checked((int)layout.TotalByteLength));
|
||||
}
|
||||
catch
|
||||
{
|
||||
write.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<AetheriaRuntimeDaemonSoaPublication> PublishAsync(
|
||||
AetheriaRuntimeDaemonSoaFrame frame,
|
||||
bool includeRealtimePayload = false)
|
||||
{
|
||||
if (frame == null) throw new ArgumentNullException(nameof(frame));
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var local = frame.Commit(now);
|
||||
var generation = new CultMeshBodyGeneration
|
||||
{
|
||||
BodyId = BodyId,
|
||||
ProducerId = ProducerId,
|
||||
SchemaId = BodySchemaId,
|
||||
LayoutVersion = LayoutVersion,
|
||||
Capacity = Capacity,
|
||||
ProducerEpoch = local.ProducerEpoch,
|
||||
Sequence = local.Sequence,
|
||||
Synchronization = local.Synchronization,
|
||||
LeaseExpiresAtUnixMs = local.LeaseExpiresAtUnixMs
|
||||
};
|
||||
var publication = new CultMeshBodyPublicationDocument
|
||||
{
|
||||
BodyId = BodyId,
|
||||
ProducerId = ProducerId,
|
||||
SchemaId = BodySchemaId,
|
||||
LayoutVersion = LayoutVersion,
|
||||
ByteSize = local.ByteSize,
|
||||
Capacity = Capacity,
|
||||
ProducerEpoch = local.ProducerEpoch,
|
||||
Sequence = local.Sequence,
|
||||
Synchronization = local.Synchronization,
|
||||
LivenessExpiresAtUnixMs = local.LeaseExpiresAtUnixMs,
|
||||
Representations = new[] { local }
|
||||
};
|
||||
new CultMeshBodyPublicationHandle(BodyId, publication.ProducerEpoch, publication.Sequence)
|
||||
.Validate(publication);
|
||||
var view = AetheriaRuntimeEveEntitySoaProjection.Project(frame.View, generation);
|
||||
if (view.Buffers.Length != 1 || !string.Equals(view.Buffers[0].BufferId, publication.BodyId, StringComparison.Ordinal) ||
|
||||
view.Columns.Any(column => !string.Equals(column.BufferId, publication.BodyId, StringComparison.Ordinal)))
|
||||
throw new InvalidOperationException("Aetheria Eve SoA layout does not reference its published logical body identity.");
|
||||
var realtimePayload = includeRealtimePayload
|
||||
? CopyRealtimePayload(local, now)
|
||||
: ReadOnlyMemory<byte>.Empty;
|
||||
return Task.FromResult(new AetheriaRuntimeDaemonSoaPublication(publication, view, realtimePayload));
|
||||
}
|
||||
|
||||
private static ReadOnlyMemory<byte> CopyRealtimePayload(CultMeshBodyDescriptor descriptor, DateTimeOffset now)
|
||||
{
|
||||
var bytes = new byte[checked((int)descriptor.ByteSize)];
|
||||
using var lease = new CultMeshSharedMemoryBodyAdapter().OpenReadOnly(
|
||||
descriptor,
|
||||
new CultMeshBodyValidationRequest
|
||||
{
|
||||
BodyId = descriptor.BodyId,
|
||||
SchemaId = descriptor.SchemaId,
|
||||
LayoutVersion = descriptor.LayoutVersion,
|
||||
ProducerEpoch = descriptor.ProducerEpoch,
|
||||
Sequence = descriptor.Sequence,
|
||||
Capacity = descriptor.Capacity,
|
||||
AccessMode = CultMeshBodyAccessMode.ReadOnly,
|
||||
NowUtc = now
|
||||
});
|
||||
var copied = lease.CopyTo(0, bytes, 0, bytes.Length);
|
||||
if (copied != bytes.Length)
|
||||
throw new EndOfStreamException($"CultMesh copied {copied} of {bytes.Length} realtime SoA bytes.");
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public void Dispose() => _localPublisher.Dispose();
|
||||
|
||||
private static IReadOnlyList<AetheriaRuntimeDaemonRenderGroupDocument> CreateRenderGroups(
|
||||
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
|
||||
IReadOnlyList<AetheriaRuntimeDroppedPickupCommit> pickups,
|
||||
IReadOnlyList<AetheriaRuntimePhysicalPayloadCommit> payloads,
|
||||
IReadOnlyList<CelestialBodyPresentation> celestialBodies,
|
||||
IReadOnlyList<AsteroidPresentation> asteroidInstances)
|
||||
{
|
||||
if (entities.Count == 0 && pickups.Count == 0 && payloads.Count == 0 &&
|
||||
celestialBodies.Count == 0 && asteroidInstances.Count == 0)
|
||||
return Array.Empty<AetheriaRuntimeDaemonRenderGroupDocument>();
|
||||
|
||||
var positions = entities.Select(entity => (entity.PositionX, entity.PositionY, entity.PositionZ))
|
||||
.Concat(pickups.Select(pickup => (pickup.PositionX, pickup.PositionY, pickup.PositionZ)))
|
||||
.Concat(payloads.Select(payload => (payload.PositionX, PositionY: payload.PositionY, payload.PositionZ)))
|
||||
.Concat(celestialBodies.Select(value => (value.PositionX, value.PositionY, value.PositionZ)))
|
||||
.Concat(asteroidInstances.Select(value => (value.PositionX, value.PositionY, value.PositionZ)))
|
||||
.ToArray();
|
||||
var minX = positions.Min(position => (float)position.PositionX);
|
||||
var minY = positions.Min(position => (float)position.PositionY);
|
||||
var minZ = positions.Min(position => (float)position.PositionZ);
|
||||
var maxX = positions.Max(position => (float)position.PositionX);
|
||||
var maxY = positions.Max(position => (float)position.PositionY);
|
||||
var maxZ = positions.Max(position => (float)position.PositionZ);
|
||||
const float boundsCell = 1024.0f;
|
||||
var boundsMinX = MathF.Floor((minX - 16.0f) / boundsCell) * boundsCell;
|
||||
var boundsMinY = MathF.Floor((minY - 16.0f) / boundsCell) * boundsCell;
|
||||
var boundsMinZ = MathF.Floor((minZ - 16.0f) / boundsCell) * boundsCell;
|
||||
var boundsMaxX = MathF.Ceiling((maxX + 16.0f) / boundsCell) * boundsCell;
|
||||
var boundsMaxY = MathF.Ceiling((maxY + 16.0f) / boundsCell) * boundsCell;
|
||||
var boundsMaxZ = MathF.Ceiling((maxZ + 16.0f) / boundsCell) * boundsCell;
|
||||
|
||||
return new[]
|
||||
{
|
||||
new AetheriaRuntimeDaemonRenderGroupDocument
|
||||
{
|
||||
GroupId = EntityRenderGroupId,
|
||||
MeshKey = EntityProxyMeshUri,
|
||||
MaterialKey = EntityProxyMaterialUri,
|
||||
MeshAsset = AetheriaRuntimeAssetRef.FromKey(
|
||||
EntityProxyMeshAssetKey,
|
||||
AetheriaRuntimeAssetKinds.Mesh,
|
||||
EntityProxyMeshUri,
|
||||
AetheriaRuntimeAssetTransports.CultMesh),
|
||||
MaterialAsset = AetheriaRuntimeAssetRef.FromKey(
|
||||
EntityProxyMaterialAssetKey,
|
||||
AetheriaRuntimeAssetKinds.Material,
|
||||
EntityProxyMaterialUri,
|
||||
AetheriaRuntimeAssetTransports.CultMesh),
|
||||
SubMeshIndex = 0,
|
||||
Layer = 0,
|
||||
ShaderKey = "aetheria.daemon.entity-proxy",
|
||||
DisplayName = "Daemon current-zone entities",
|
||||
InstanceCount = entities.Count + pickups.Count + payloads.Count +
|
||||
celestialBodies.Count + asteroidInstances.Count,
|
||||
BoundsCenterX = (boundsMinX + boundsMaxX) * 0.5f,
|
||||
BoundsCenterY = (boundsMinY + boundsMaxY) * 0.5f,
|
||||
BoundsCenterZ = (boundsMinZ + boundsMaxZ) * 0.5f,
|
||||
BoundsSizeX = Math.Max(boundsMaxX - boundsMinX, boundsCell),
|
||||
BoundsSizeY = Math.Max(boundsMaxY - boundsMinY, boundsCell),
|
||||
BoundsSizeZ = Math.Max(boundsMaxZ - boundsMinZ, boundsCell),
|
||||
ShadowMode = AetheriaRuntimeDaemonRenderShadowModes.On,
|
||||
ReceiveShadows = true,
|
||||
DefaultScale = 1.0f,
|
||||
Lod = -1
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void WriteEntities(
|
||||
Span<byte> bytes,
|
||||
EntityHotSlabLayout layout,
|
||||
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
|
||||
ISet<int> visibleEntityIndices)
|
||||
{
|
||||
for (var index = 0; index < entities.Count; index++)
|
||||
{
|
||||
var entity = entities[index];
|
||||
WriteInt32(bytes, layout.EntityIndex + index * IntStride, entity.EntityIndex);
|
||||
WriteInt32(bytes, layout.CargoQuantity + index * IntStride, AetheriaRuntimeCargoCapacityQueries.Quantity(entity));
|
||||
WriteFloat(bytes, layout.BeamPower, index, entity.TractorPower);
|
||||
WriteFloat3(bytes, layout.Position, index, entity.PositionX, entity.PositionY, entity.PositionZ);
|
||||
WriteFloat(bytes, layout.RotationRadians, index, Math.Atan2(entity.DirectionX, entity.DirectionY));
|
||||
WriteFloat3(bytes, layout.Velocity, index, entity.VelocityX, 0.0, entity.VelocityY);
|
||||
WriteFloat(bytes, layout.PhysicsBodyRadius, index, 1.0);
|
||||
WriteFloat(bytes, layout.PhysicsBodyMass, index, 1.0);
|
||||
WriteFloat(bytes, layout.PhysicsBodyInverseMass, index, 1.0);
|
||||
WriteFloat(bytes, layout.RenderScale, index, 1.0);
|
||||
bytes[checked((int)(layout.RenderVisibility + index * ByteStride))] =
|
||||
(byte)(entity.IsActive && visibleEntityIndices.Contains(entity.EntityIndex) ? 1 : 0);
|
||||
WriteInt32(bytes, layout.RenderLod + index * IntStride, 0);
|
||||
WriteUInt32(bytes, layout.RenderGroupId + index * IntStride, EntityRenderGroupId);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WritePickups(
|
||||
Span<byte> bytes,
|
||||
EntityHotSlabLayout layout,
|
||||
IReadOnlyList<AetheriaRuntimeDroppedPickupCommit> pickups,
|
||||
IReadOnlyList<string> pickupEntityIds,
|
||||
IReadOnlyDictionary<string, int> syntheticEntityIndices,
|
||||
int rowOffset)
|
||||
{
|
||||
for (var pickupRow = 0; pickupRow < pickups.Count; pickupRow++)
|
||||
{
|
||||
var pickup = pickups[pickupRow];
|
||||
var row = rowOffset + pickupRow;
|
||||
WriteInt32(bytes, layout.EntityIndex + row * IntStride, syntheticEntityIndices[pickupEntityIds[pickupRow]]);
|
||||
WriteInt32(bytes, layout.CargoQuantity + row * IntStride, 0);
|
||||
WriteFloat3(bytes, layout.Position, row, pickup.PositionX, pickup.PositionY, pickup.PositionZ);
|
||||
WriteFloat(bytes, layout.RotationRadians, row, 0.0);
|
||||
WriteFloat3(bytes, layout.Velocity, row, pickup.VelocityX, pickup.VelocityY, pickup.VelocityZ);
|
||||
WriteFloat(bytes, layout.PhysicsBodyRadius, row, 0.5);
|
||||
WriteFloat(bytes, layout.PhysicsBodyMass, row, 0.25);
|
||||
WriteFloat(bytes, layout.PhysicsBodyInverseMass, row, 4.0);
|
||||
WriteFloat(bytes, layout.RenderScale, row, 1.0);
|
||||
bytes[checked((int)(layout.RenderVisibility + row * ByteStride))] = 1;
|
||||
WriteInt32(bytes, layout.RenderLod + row * IntStride, 0);
|
||||
WriteUInt32(bytes, layout.RenderGroupId + row * IntStride, EntityRenderGroupId);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WritePayloads(
|
||||
Span<byte> bytes,
|
||||
EntityHotSlabLayout layout,
|
||||
IReadOnlyList<AetheriaRuntimePhysicalPayloadCommit> payloads,
|
||||
IReadOnlyList<string> payloadEntityIds,
|
||||
IReadOnlyDictionary<string, int> syntheticEntityIndices,
|
||||
int rowOffset)
|
||||
{
|
||||
for (var payloadRow = 0; payloadRow < payloads.Count; payloadRow++)
|
||||
{
|
||||
var payload = payloads[payloadRow];
|
||||
var row = rowOffset + payloadRow;
|
||||
WriteInt32(bytes, layout.EntityIndex + row * IntStride, syntheticEntityIndices[payloadEntityIds[payloadRow]]);
|
||||
WriteInt32(bytes, layout.CargoQuantity + row * IntStride, 0);
|
||||
WriteFloat3(bytes, layout.Position, row, payload.PositionX, payload.PositionY, payload.PositionZ);
|
||||
WriteFloat(bytes, layout.RotationRadians, row, Math.Atan2(payload.DirectionX, payload.DirectionY));
|
||||
WriteFloat3(bytes, layout.Velocity, row, payload.VelocityX, 0.0, payload.VelocityY);
|
||||
WriteFloat(bytes, layout.PhysicsBodyRadius, row, Math.Max(0.01, payload.Radius));
|
||||
WriteFloat(bytes, layout.PhysicsBodyMass, row, payload.Stationary ? 0.0 : 1.0);
|
||||
WriteFloat(bytes, layout.PhysicsBodyInverseMass, row, payload.Stationary ? 0.0 : 1.0);
|
||||
WriteFloat(bytes, layout.RenderScale, row, 1.0);
|
||||
bytes[checked((int)(layout.RenderVisibility + row * ByteStride))] = 1;
|
||||
WriteInt32(bytes, layout.RenderLod + row * IntStride, 0);
|
||||
WriteUInt32(bytes, layout.RenderGroupId + row * IntStride, EntityRenderGroupId);
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<CelestialBodyPresentation> BuildCelestialBodies(
|
||||
AetheriaRuntimeDaemonFrameDocument frame,
|
||||
AetheriaRuntimeZoneSnapshotCommit? zone)
|
||||
{
|
||||
if (zone == null)
|
||||
return Array.Empty<CelestialBodyPresentation>();
|
||||
|
||||
var poses = AetheriaRuntimeDaemonRenderQueries.QueryBodyPoses(zone)
|
||||
.ToDictionary(pose => pose.BodyKey, StringComparer.Ordinal);
|
||||
var values = new List<CelestialBodyPresentation>();
|
||||
foreach (var body in zone.Bodies ?? Array.Empty<AetheriaRuntimeBodySnapshotCommit>())
|
||||
{
|
||||
if (body == null ||
|
||||
string.Equals(body.Kind, "asteroid_belt", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.IsNullOrWhiteSpace(body.BodyKey) ||
|
||||
!poses.TryGetValue(body.BodyKey, out var pose))
|
||||
continue;
|
||||
|
||||
var scale = Math.Max(
|
||||
0.1,
|
||||
frame.RenderSettings.ResolveBodyRadius(body.Mass) * Math.Max(0, body.BodyRadiusMultiplier));
|
||||
var terrainHeight = AetheriaRuntimeDaemonRenderQueries.EvaluateGravityTerrainHeight(
|
||||
zone,
|
||||
pose.CenterX,
|
||||
pose.CenterZ,
|
||||
frame.SimulationTimeSeconds);
|
||||
var kind = NormalizeCelestialKind(body.Kind);
|
||||
values.Add(new CelestialBodyPresentation(
|
||||
$"{frame.Run.RunId}:zone:{zone.ZoneIndex}:body:{body.BodyKey}",
|
||||
kind,
|
||||
string.IsNullOrWhiteSpace(body.Name) ? body.BodyKey : body.Name,
|
||||
ResolveCelestialAssetRef(kind),
|
||||
pose.CenterX,
|
||||
terrainHeight + scale * 2.0,
|
||||
pose.CenterZ,
|
||||
frame.SimulationTimeSeconds * frame.RenderSettings.PlanetRotationSpeed * Math.PI / 180.0,
|
||||
scale));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AsteroidPresentation> BuildAsteroidInstances(
|
||||
AetheriaRuntimeDaemonFrameDocument frame,
|
||||
AetheriaRuntimeZoneSnapshotCommit? zone)
|
||||
{
|
||||
if (zone == null)
|
||||
return Array.Empty<AsteroidPresentation>();
|
||||
|
||||
var values = new List<AsteroidPresentation>();
|
||||
foreach (var belt in zone.Bodies ?? Array.Empty<AetheriaRuntimeBodySnapshotCommit>())
|
||||
{
|
||||
if (belt == null ||
|
||||
!string.Equals(belt.Kind, "asteroid_belt", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.IsNullOrWhiteSpace(belt.BodyKey))
|
||||
continue;
|
||||
|
||||
foreach (var pose in AetheriaRuntimeDaemonRenderQueries.QueryAsteroidInstancePoses(
|
||||
zone,
|
||||
belt.BodyKey,
|
||||
frame.SimulationTimeSeconds))
|
||||
{
|
||||
if (pose.Size <= 0)
|
||||
continue;
|
||||
var terrainHeight = AetheriaRuntimeDaemonRenderQueries.EvaluateGravityTerrainHeight(
|
||||
zone,
|
||||
pose.PositionX,
|
||||
pose.PositionZ,
|
||||
frame.SimulationTimeSeconds);
|
||||
values.Add(new AsteroidPresentation(
|
||||
$"{frame.Run.RunId}:zone:{zone.ZoneIndex}:asteroid:{belt.BodyKey}:{pose.AsteroidIndex}",
|
||||
pose.PositionX,
|
||||
terrainHeight + frame.RenderSettings.AsteroidVerticalOffset,
|
||||
pose.PositionZ,
|
||||
pose.Rotation,
|
||||
pose.Size));
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private static string NormalizeCelestialKind(string? kind)
|
||||
{
|
||||
var normalized = (kind ?? "").Trim().ToLowerInvariant().Replace('_', '-');
|
||||
return normalized switch
|
||||
{
|
||||
"sun" => "sun",
|
||||
"gas-giant" => "gas-giant",
|
||||
_ => "planet"
|
||||
};
|
||||
}
|
||||
|
||||
private static string ResolveCelestialAssetRef(string kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
"sun" => "prefab.body.sun",
|
||||
"gas-giant" => "prefab.body.gas-giant",
|
||||
_ => "prefab.body.planet"
|
||||
};
|
||||
}
|
||||
|
||||
private static void WriteCelestialBodies(
|
||||
Span<byte> bytes,
|
||||
EntityHotSlabLayout layout,
|
||||
IReadOnlyList<CelestialBodyPresentation> values,
|
||||
IReadOnlyDictionary<string, int> syntheticEntityIndices,
|
||||
int rowOffset)
|
||||
{
|
||||
for (var valueIndex = 0; valueIndex < values.Count; valueIndex++)
|
||||
{
|
||||
var value = values[valueIndex];
|
||||
var row = rowOffset + valueIndex;
|
||||
WritePresentationRow(
|
||||
bytes,
|
||||
layout,
|
||||
row,
|
||||
syntheticEntityIndices[value.EntityId],
|
||||
value.PositionX,
|
||||
value.PositionY,
|
||||
value.PositionZ,
|
||||
value.RotationRadians,
|
||||
value.Scale);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteAsteroidInstances(
|
||||
Span<byte> bytes,
|
||||
EntityHotSlabLayout layout,
|
||||
IReadOnlyList<AsteroidPresentation> values,
|
||||
IReadOnlyDictionary<string, int> syntheticEntityIndices,
|
||||
int rowOffset)
|
||||
{
|
||||
for (var valueIndex = 0; valueIndex < values.Count; valueIndex++)
|
||||
{
|
||||
var value = values[valueIndex];
|
||||
var row = rowOffset + valueIndex;
|
||||
WritePresentationRow(
|
||||
bytes,
|
||||
layout,
|
||||
row,
|
||||
syntheticEntityIndices[value.EntityId],
|
||||
value.PositionX,
|
||||
value.PositionY,
|
||||
value.PositionZ,
|
||||
value.RotationRadians,
|
||||
value.Scale);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WritePresentationRow(
|
||||
Span<byte> bytes,
|
||||
EntityHotSlabLayout layout,
|
||||
int row,
|
||||
int entityIndex,
|
||||
double positionX,
|
||||
double positionY,
|
||||
double positionZ,
|
||||
double rotationRadians,
|
||||
double scale)
|
||||
{
|
||||
WriteInt32(bytes, layout.EntityIndex + row * IntStride, entityIndex);
|
||||
WriteInt32(bytes, layout.CargoQuantity + row * IntStride, 0);
|
||||
WriteFloat3(bytes, layout.Position, row, positionX, positionY, positionZ);
|
||||
WriteFloat(bytes, layout.RotationRadians, row, rotationRadians);
|
||||
WriteFloat3(bytes, layout.Velocity, row, 0, 0, 0);
|
||||
WriteFloat(bytes, layout.PhysicsBodyRadius, row, Math.Max(0.01, scale));
|
||||
WriteFloat(bytes, layout.PhysicsBodyMass, row, 0);
|
||||
WriteFloat(bytes, layout.PhysicsBodyInverseMass, row, 0);
|
||||
WriteFloat(bytes, layout.RenderScale, row, Math.Max(0.01, scale));
|
||||
bytes[checked((int)(layout.RenderVisibility + row * ByteStride))] = 1;
|
||||
WriteInt32(bytes, layout.RenderLod + row * IntStride, 0);
|
||||
WriteUInt32(bytes, layout.RenderGroupId + row * IntStride, EntityRenderGroupId);
|
||||
}
|
||||
|
||||
private int GetOrAllocateSyntheticEntityIndex(string entityId)
|
||||
{
|
||||
if (_syntheticEntityIndices.TryGetValue(entityId, out var existing))
|
||||
return existing;
|
||||
if (_nextSyntheticEntityIndex == int.MinValue)
|
||||
throw new InvalidOperationException("Aetheria SoA synthetic entity index space is exhausted.");
|
||||
|
||||
var allocated = _nextSyntheticEntityIndex--;
|
||||
_syntheticEntityIndices.Add(entityId, allocated);
|
||||
return allocated;
|
||||
}
|
||||
|
||||
private sealed class CelestialBodyPresentation
|
||||
{
|
||||
public CelestialBodyPresentation(
|
||||
string entityId,
|
||||
string kind,
|
||||
string label,
|
||||
string assetRef,
|
||||
double positionX,
|
||||
double positionY,
|
||||
double positionZ,
|
||||
double rotationRadians,
|
||||
double scale)
|
||||
{
|
||||
EntityId = entityId;
|
||||
EntityKind = "celestial." + kind;
|
||||
Label = label;
|
||||
AssetRef = assetRef;
|
||||
PositionX = positionX;
|
||||
PositionY = positionY;
|
||||
PositionZ = positionZ;
|
||||
RotationRadians = rotationRadians;
|
||||
Scale = scale;
|
||||
}
|
||||
|
||||
public string EntityId { get; }
|
||||
public string EntityKind { get; }
|
||||
public string Label { get; }
|
||||
public string AssetRef { get; }
|
||||
public double PositionX { get; }
|
||||
public double PositionY { get; }
|
||||
public double PositionZ { get; }
|
||||
public double RotationRadians { get; }
|
||||
public double Scale { get; }
|
||||
}
|
||||
|
||||
private sealed class AsteroidPresentation
|
||||
{
|
||||
public AsteroidPresentation(
|
||||
string entityId,
|
||||
double positionX,
|
||||
double positionY,
|
||||
double positionZ,
|
||||
double rotationRadians,
|
||||
double scale)
|
||||
{
|
||||
EntityId = entityId;
|
||||
PositionX = positionX;
|
||||
PositionY = positionY;
|
||||
PositionZ = positionZ;
|
||||
RotationRadians = rotationRadians;
|
||||
Scale = scale;
|
||||
}
|
||||
|
||||
public string EntityId { get; }
|
||||
public double PositionX { get; }
|
||||
public double PositionY { get; }
|
||||
public double PositionZ { get; }
|
||||
public double RotationRadians { get; }
|
||||
public double Scale { get; }
|
||||
}
|
||||
|
||||
private static void WriteFloat(Span<byte> bytes, long byteOffset, int index, double value)
|
||||
{
|
||||
var scalar = IsFinite(value) ? (float)value : 0.0f;
|
||||
BinaryPrimitives.WriteInt32LittleEndian(
|
||||
bytes.Slice(checked((int)(byteOffset + index * FloatStride)), FloatStride),
|
||||
BitConverter.SingleToInt32Bits(scalar));
|
||||
}
|
||||
|
||||
private static void WriteFloat3(
|
||||
Span<byte> bytes,
|
||||
long byteOffset,
|
||||
int index,
|
||||
double x,
|
||||
double y,
|
||||
double z)
|
||||
{
|
||||
var elementOffset = byteOffset + index * Float3Stride;
|
||||
WriteFloat(bytes, elementOffset, 0, x);
|
||||
WriteFloat(bytes, elementOffset + FloatStride, 0, y);
|
||||
WriteFloat(bytes, elementOffset + FloatStride * 2, 0, z);
|
||||
}
|
||||
|
||||
private static void WriteInt32(Span<byte> bytes, long offset, int value) =>
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes.Slice(checked((int)offset), IntStride), value);
|
||||
|
||||
private static void WriteUInt32(Span<byte> bytes, long offset, int value) =>
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.Slice(checked((int)offset), IntStride), (uint)value);
|
||||
|
||||
private static bool IsFinite(double value)
|
||||
{
|
||||
return !double.IsNaN(value) && !double.IsInfinity(value);
|
||||
}
|
||||
|
||||
private readonly struct EntityHotSlabLayout
|
||||
{
|
||||
private EntityHotSlabLayout(
|
||||
long entityIndex,
|
||||
long cargoQuantity,
|
||||
long beamPower,
|
||||
long position,
|
||||
long rotationRadians,
|
||||
long velocity,
|
||||
long physicsBodyRadius,
|
||||
long physicsBodyMass,
|
||||
long physicsBodyInverseMass,
|
||||
long renderScale,
|
||||
long renderVisibility,
|
||||
long renderLod,
|
||||
long renderGroupId,
|
||||
long totalByteLength)
|
||||
{
|
||||
EntityIndex = entityIndex;
|
||||
CargoQuantity = cargoQuantity;
|
||||
BeamPower = beamPower;
|
||||
Position = position;
|
||||
RotationRadians = rotationRadians;
|
||||
Velocity = velocity;
|
||||
PhysicsBodyRadius = physicsBodyRadius;
|
||||
PhysicsBodyMass = physicsBodyMass;
|
||||
PhysicsBodyInverseMass = physicsBodyInverseMass;
|
||||
RenderScale = renderScale;
|
||||
RenderVisibility = renderVisibility;
|
||||
RenderLod = renderLod;
|
||||
RenderGroupId = renderGroupId;
|
||||
TotalByteLength = totalByteLength;
|
||||
}
|
||||
|
||||
public long EntityIndex { get; }
|
||||
public long CargoQuantity { get; }
|
||||
public long BeamPower { get; }
|
||||
public long Position { get; }
|
||||
public long RotationRadians { get; }
|
||||
public long Velocity { get; }
|
||||
public long PhysicsBodyRadius { get; }
|
||||
public long PhysicsBodyMass { get; }
|
||||
public long PhysicsBodyInverseMass { get; }
|
||||
public long RenderScale { get; }
|
||||
public long RenderVisibility { get; }
|
||||
public long RenderLod { get; }
|
||||
public long RenderGroupId { get; }
|
||||
public long TotalByteLength { get; }
|
||||
|
||||
public static EntityHotSlabLayout Create(int count)
|
||||
{
|
||||
count = Math.Max(0, count);
|
||||
var offset = 0L;
|
||||
var entityIndex = Take(ref offset, count, IntStride);
|
||||
var cargoQuantity = Take(ref offset, count, IntStride);
|
||||
var beamPower = Take(ref offset, count, FloatStride);
|
||||
var position = Take(ref offset, count, Float3Stride);
|
||||
var rotationRadians = Take(ref offset, count, FloatStride);
|
||||
var velocity = Take(ref offset, count, Float3Stride);
|
||||
var physicsBodyRadius = Take(ref offset, count, FloatStride);
|
||||
var physicsBodyMass = Take(ref offset, count, FloatStride);
|
||||
var physicsBodyInverseMass = Take(ref offset, count, FloatStride);
|
||||
var renderScale = Take(ref offset, count, FloatStride);
|
||||
var renderVisibility = Take(ref offset, count, ByteStride);
|
||||
offset = Align4(offset);
|
||||
var renderLod = Take(ref offset, count, IntStride);
|
||||
var renderGroupId = Take(ref offset, count, IntStride);
|
||||
return new EntityHotSlabLayout(
|
||||
entityIndex,
|
||||
cargoQuantity,
|
||||
beamPower,
|
||||
position,
|
||||
rotationRadians,
|
||||
velocity,
|
||||
physicsBodyRadius,
|
||||
physicsBodyMass,
|
||||
physicsBodyInverseMass,
|
||||
renderScale,
|
||||
renderVisibility,
|
||||
renderLod,
|
||||
renderGroupId,
|
||||
Math.Max(1, offset));
|
||||
}
|
||||
|
||||
public AetheriaRuntimeDaemonSoaColumnDocument[] CreateColumns(int count)
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
Column("entity-index", AetheriaRuntimeDaemonSoaColumnKinds.EntityIndex, "int32", EntityIndex, IntStride, count, "index", "world"),
|
||||
Column("cargo-quantity", AetheriaRuntimeDaemonSoaColumnKinds.CargoQuantity, "int32", CargoQuantity, IntStride, count, "items", "entity"),
|
||||
Column("beam-power", AetheriaRuntimeDaemonSoaColumnKinds.BeamPower, "float32", BeamPower, FloatStride, count, "normalized", "entity"),
|
||||
Column("position", AetheriaRuntimeDaemonSoaColumnKinds.Position, "float3", Position, Float3Stride, count, "world_units", "world"),
|
||||
Column("rotation-radians", AetheriaRuntimeDaemonSoaColumnKinds.RotationRadians, "float32", RotationRadians, FloatStride, count, "radians", "world"),
|
||||
Column("velocity", AetheriaRuntimeDaemonSoaColumnKinds.Velocity, "float3", Velocity, Float3Stride, count, "world_units_per_second", "world"),
|
||||
Column("physics-body-radius", AetheriaRuntimeDaemonSoaColumnKinds.PhysicsBodyRadius, "float32", PhysicsBodyRadius, FloatStride, count, "world_units", "world"),
|
||||
Column("physics-body-mass", AetheriaRuntimeDaemonSoaColumnKinds.PhysicsBodyMass, "float32", PhysicsBodyMass, FloatStride, count, "mass_units", "world"),
|
||||
Column("physics-body-inverse-mass", AetheriaRuntimeDaemonSoaColumnKinds.PhysicsBodyInverseMass, "float32", PhysicsBodyInverseMass, FloatStride, count, "inverse_mass_units", "world"),
|
||||
Column("render-scale", AetheriaRuntimeDaemonSoaColumnKinds.RenderScale, "float32", RenderScale, FloatStride, count, "scale", "world"),
|
||||
Column("render-visibility", AetheriaRuntimeDaemonSoaColumnKinds.RenderVisibility, "uint8", RenderVisibility, ByteStride, count, "bool", "world"),
|
||||
Column("render-lod", AetheriaRuntimeDaemonSoaColumnKinds.RenderLod, "int32", RenderLod, IntStride, count, "lod", "world"),
|
||||
Column("render-group-id", AetheriaRuntimeDaemonSoaColumnKinds.RenderGroupId, "uint32", RenderGroupId, IntStride, count, "id", "world")
|
||||
};
|
||||
}
|
||||
|
||||
public AetheriaRuntimeDaemonSoaDirtyRangeDocument[] CreateDirtyRanges(int count, long generation)
|
||||
{
|
||||
return CreateColumns(count)
|
||||
.Select(column => new AetheriaRuntimeDaemonSoaDirtyRangeDocument
|
||||
{
|
||||
ColumnId = column.ColumnId,
|
||||
StartIndex = 0,
|
||||
Count = count,
|
||||
Generation = generation
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeDaemonSoaColumnDocument Column(
|
||||
string columnId,
|
||||
string kind,
|
||||
string scalarType,
|
||||
long byteOffset,
|
||||
int stride,
|
||||
int count,
|
||||
string unit,
|
||||
string coordinateSpace)
|
||||
{
|
||||
return new AetheriaRuntimeDaemonSoaColumnDocument
|
||||
{
|
||||
ColumnId = columnId,
|
||||
Kind = kind,
|
||||
BufferId = BodyId,
|
||||
ScalarType = scalarType,
|
||||
ByteOffset = byteOffset,
|
||||
ElementStride = stride,
|
||||
ElementCount = count,
|
||||
Unit = unit,
|
||||
CoordinateSpace = coordinateSpace
|
||||
};
|
||||
}
|
||||
|
||||
private static long Take(ref long offset, int count, int stride)
|
||||
{
|
||||
var current = offset;
|
||||
offset += (long)count * stride;
|
||||
return current;
|
||||
}
|
||||
|
||||
private static long Align4(long value)
|
||||
{
|
||||
var remainder = value % 4;
|
||||
return remainder == 0 ? value : value + 4 - remainder;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue