diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index 472ec14a4..fe3ace4ce 100644 Binary files a/.voidbot/state/aetheria.cc and b/.voidbot/state/aetheria.cc differ diff --git a/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs b/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs index 5d713f52b..cac92d896 100644 --- a/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs +++ b/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs @@ -27,9 +27,9 @@ public abstract class Behavior if(Item != null) { var hardpoint = Entity.Hardpoints[Item.Position.x, Item.Position.y]; - if (hardpoint != null && Entity.HardpointTransforms.ContainsRuntimeCatalogKey(hardpoint)) + if (hardpoint != null && Entity.HardpointTransforms.TryGetValue(hardpoint, out var hardpointTransform)) { - return normalize(Entity.HardpointTransforms[hardpoint].direction); + return normalize(hardpointTransform.direction); } else { diff --git a/Assets/Scripts/ServerShared/Behaviors/Thruster.cs b/Assets/Scripts/ServerShared/Behaviors/Thruster.cs index c37d6001e..f184fa1f1 100644 --- a/Assets/Scripts/ServerShared/Behaviors/Thruster.cs +++ b/Assets/Scripts/ServerShared/Behaviors/Thruster.cs @@ -79,10 +79,10 @@ public class Thruster : Behavior, IAnalogBehavior Unity.Mathematics.float2x2.Rotate(_input * Torque * Thrust * ItemManager.GameplaySettings.TorqueMultiplier / Entity.Mass * dt)); AddHeat(_input * Evaluate(_data.Heat) * dt); var vis = _input * Evaluate(_data.Visibility); - if (!Entity.VisibilitySources.ContainsRuntimeCatalogKey(this) || vis > Entity.VisibilitySources[this]) + if (!Entity.VisibilitySources.TryGetValue(this, out var existingVisibility) || vis > existingVisibility) Entity.VisibilitySources[this] = vis; return true; } return false; } -} \ No newline at end of file +} diff --git a/Assets/Scripts/ServerShared/ItemData.cs b/Assets/Scripts/ServerShared/ItemData.cs index badfe1ed3..58c45933a 100644 --- a/Assets/Scripts/ServerShared/ItemData.cs +++ b/Assets/Scripts/ServerShared/ItemData.cs @@ -561,7 +561,7 @@ public class HardpointData : ITintInspector x => ColorMath.HsvToRgb(float3(frac((float)(int)x/hardpointTypes.Length + .25f), 1, 1))); } - return _tintColors.ContainsRuntimeCatalogKey(type) ? _tintColors[type] : _tintColors[HardpointType.Hull]; + return _tintColors.TryGetValue(type, out var color) ? color : _tintColors[HardpointType.Hull]; } private static Dictionary _tintColors; @@ -597,7 +597,7 @@ public class PerformanceStat public Dictionary GetScaleModifiers(Entity entity) { - if(!ScaleModifiers.ContainsRuntimeCatalogKey(entity)) + if(!ScaleModifiers.ContainsKey(entity)) ScaleModifiers[entity] = new Dictionary(); return ScaleModifiers[entity]; @@ -605,7 +605,7 @@ public class PerformanceStat public Dictionary GetConstantModifiers(Entity entity) { - if(!ConstantModifiers.ContainsRuntimeCatalogKey(entity)) + if(!ConstantModifiers.ContainsKey(entity)) ConstantModifiers[entity] = new Dictionary(); return ConstantModifiers[entity]; @@ -629,4 +629,4 @@ public class PersonalityAttribute : DatabaseEntry, INamedEntry get => Name; set => Name = value; } -} \ No newline at end of file +} diff --git a/Packages/manifest.json b/Packages/manifest.json index 258d9f2f0..6748b1d86 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -1,5 +1,7 @@ { "dependencies": { + "org.gamecult.aetheria.eve-runtime": "file:org.gamecult.aetheria.eve-runtime", + "org.gamecult.aetheria.state": "file:org.gamecult.aetheria.state", "org.gamecult.eve.surface": "file:org.gamecult.eve.surface", "org.gamecult.eve.unity-uitoolkit": "file:org.gamecult.eve.unity-uitoolkit", "com.unity.2d.sprite": "1.0.0", diff --git a/Packages/org.gamecult.aetheria.eve-runtime/README.md b/Packages/org.gamecult.aetheria.eve-runtime/README.md new file mode 100644 index 000000000..2448ec1df --- /dev/null +++ b/Packages/org.gamecult.aetheria.eve-runtime/README.md @@ -0,0 +1,15 @@ +# Aetheria Eve Runtime + +This package mounts provider-owned Eve surfaces from Aetheria's typed +CultCache state into Unity UI Toolkit. + +`AetheriaEveSurfacePresenter` is a projection bridge: + +- it reads `gamecult.eve.surface.v1` from `GameData/aetheria-world.cc`; +- it lowers the retained tree through `org.gamecult.eve.unity-uitoolkit`; +- it does not accept, mutate, or persist Aetheria state. + +Commands are surfaced as `gamecult.eve.command.v1` requests and currently log a +capability gap until the CultMesh command bridge is wired. That gap is visible +on purpose; renderer-local command effects would be the wrong organ wearing a +clean shirt. diff --git a/Packages/org.gamecult.aetheria.eve-runtime/Runtime/AetheriaEveSurfacePresenter.cs b/Packages/org.gamecult.aetheria.eve-runtime/Runtime/AetheriaEveSurfacePresenter.cs new file mode 100644 index 000000000..85dbed9f0 --- /dev/null +++ b/Packages/org.gamecult.aetheria.eve-runtime/Runtime/AetheriaEveSurfacePresenter.cs @@ -0,0 +1,104 @@ +using System; +using System.IO; +using System.Linq; +using GameCult.Aetheria.State.Unity; +using GameCult.Eve.Surface; +using GameCult.Eve.UnityUIToolkit; +using UnityEngine; +using UnityEngine.UIElements; + +#nullable enable + +namespace GameCult.Aetheria.EveRuntime +{ + [DisallowMultipleComponent] + [RequireComponent(typeof(UIDocument))] + public sealed class AetheriaEveSurfacePresenter : MonoBehaviour + { + [SerializeField] + private string surfaceId = "aetheria.catalog.operator"; + + [SerializeField] + private string stateFilePathOverride = ""; + + [SerializeField] + private bool mountOnEnable = true; + + private UIDocument? _document; + + public string SurfaceId + { + get => surfaceId; + set => surfaceId = value ?? ""; + } + + public string StateFilePathOverride + { + get => stateFilePathOverride; + set => stateFilePathOverride = value ?? ""; + } + + public void Mount() + { + var document = ResolveDocument(); + var root = document.rootVisualElement; + root.Clear(); + + var statePath = ResolveStateFilePath(); + if (!File.Exists(statePath)) + { + root.Add(BuildError($"Aetheria state file not found: {statePath}")); + return; + } + + var surface = AetheriaRuntimeCatalogStore.ReadEveSurfaces(statePath) + .FirstOrDefault(candidate => string.Equals(candidate.Surface.Id, surfaceId, StringComparison.Ordinal)); + if (surface == null) + { + root.Add(BuildError($"Eve surface not found: {surfaceId}")); + return; + } + + var lowerer = new EveUiToolkitSurfaceLowerer(); + root.Add(lowerer.Lower(surface, EmitCommand)); + } + + private void OnEnable() + { + if (mountOnEnable) + Mount(); + } + + private UIDocument ResolveDocument() + { + if (_document == null) + _document = GetComponent(); + return _document; + } + + private string ResolveStateFilePath() + { + if (!string.IsNullOrWhiteSpace(stateFilePathOverride)) + return Path.GetFullPath(stateFilePathOverride); + + var gameDataDirectory = new DirectoryInfo(Path.Combine(Application.dataPath, "..", "GameData")); + return AetheriaRuntimeStateBoundary.GetStateFilePath(gameDataDirectory); + } + + 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 static void EmitCommand(EveSurfaceCommandRequest request) + { + Debug.LogWarning( + $"Eve command emitted without a CultMesh command bridge: {request.ProviderId}/{request.SurfaceId}/{request.Command}"); + } + } +} diff --git a/Packages/org.gamecult.aetheria.eve-runtime/Runtime/GameCult.Aetheria.EveRuntime.asmdef b/Packages/org.gamecult.aetheria.eve-runtime/Runtime/GameCult.Aetheria.EveRuntime.asmdef new file mode 100644 index 000000000..7f2ac2fa2 --- /dev/null +++ b/Packages/org.gamecult.aetheria.eve-runtime/Runtime/GameCult.Aetheria.EveRuntime.asmdef @@ -0,0 +1,18 @@ +{ + "name": "GameCult.Aetheria.EveRuntime", + "rootNamespace": "GameCult.Aetheria.EveRuntime", + "references": [ + "GameCult.Aetheria.State.Unity", + "GameCult.Eve.Surface", + "GameCult.Eve.UnityUIToolkit" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Packages/org.gamecult.aetheria.eve-runtime/package.json b/Packages/org.gamecult.aetheria.eve-runtime/package.json new file mode 100644 index 000000000..8d1479157 --- /dev/null +++ b/Packages/org.gamecult.aetheria.eve-runtime/package.json @@ -0,0 +1,19 @@ +{ + "name": "org.gamecult.aetheria.eve-runtime", + "displayName": "Aetheria Eve Runtime", + "version": "0.1.0", + "unity": "2021.3", + "description": "Aetheria runtime bridge from typed CultCache Eve surfaces to Unity UI Toolkit projection.", + "dependencies": { + "org.gamecult.aetheria.state": "0.1.0", + "org.gamecult.eve.unity-uitoolkit": "0.1.0" + }, + "keywords": [ + "Aetheria", + "Eve", + "CultCache", + "UI Toolkit" + ], + "license": "MPL-2.0", + "category": "Scripting" +} diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index aaa45b08e..3a9b1c2c6 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -1,5 +1,22 @@ { "dependencies": { + "org.gamecult.aetheria.eve-runtime": { + "version": "file:org.gamecult.aetheria.eve-runtime", + "depth": 0, + "source": "embedded", + "dependencies": { + "org.gamecult.aetheria.state": "0.1.0", + "org.gamecult.eve.unity-uitoolkit": "0.1.0" + } + }, + "org.gamecult.aetheria.state": { + "version": "file:org.gamecult.aetheria.state", + "depth": 0, + "source": "embedded", + "dependencies": { + "org.gamecult.eve.surface": "0.1.0" + } + }, "org.gamecult.eve.surface": { "version": "file:org.gamecult.eve.surface", "depth": 0, diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 87a38c837..3df9c5ce3 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -194,6 +194,10 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. DTOs. This gives Unity the same provider-owned retained tree that the SDK-style state client reads, without JSON fixtures, HTTP dashboard summaries, or renderer-owned state. +- `AetheriaEveSurfacePresenter` is the first runtime UI Toolkit consumer of + those typed surface documents. It owns mounting only: state file resolution, + surface lookup, lowering, and visible capability gaps for commands. Provider + command acceptance still belongs to the future CultMesh command bridge. - `GameData/aetheria-world.cc` is now materialized from the importer as the project-local typed state file for the checked-in catalog. The importer stores relative provenance in the state document, not machine-local absolute paths. @@ -281,6 +285,11 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. the first carries no-engine `gamecult.eve.surface.v1` contract DTOs, and the second lowers those retained trees into UI Toolkit `VisualElement` trees. These packages are renderer boundaries, not UI truth owners. + `org.gamecult.aetheria.eve-runtime` now adds the Aetheria-specific + `UIDocument` presenter that reads provider-owned Eve surfaces from + `GameData/aetheria-world.cc` and mounts them through the lowerer. Its command + path currently reports the missing CultMesh command bridge instead of + accepting renderer-local command effects. ## Invariants @@ -540,6 +549,11 @@ First Aetheria surfaces to publish: - Done: teach the embedded Unity state package to read `gamecult.eve.surface` records from the `.cc` store into that shared contract. + - Done: add `org.gamecult.aetheria.eve-runtime` with a `UIDocument` + presenter that mounts typed Eve surfaces from `GameData/aetheria-world.cc` + through UI Toolkit without giving the renderer state authority. + - Wire the presenter into a Unity scene/prefab for the first runtime surface + and replace a concrete uGUI screen. - Move the staged packages into the Eve repo once its worktree is clean, then import them back into Aetheria from Eve instead of carrying a local copy. - Replace the old IMGUI DB inspector first, because it is closest to state