diff --git a/Aetheria.State.Daemon.Smoke/Program.cs b/Aetheria.State.Daemon.Smoke/Program.cs index 491d6c7e1..27fa8a967 100644 --- a/Aetheria.State.Daemon.Smoke/Program.cs +++ b/Aetheria.State.Daemon.Smoke/Program.cs @@ -116,6 +116,22 @@ internal sealed class AetheriaDaemonYmirSmokeChecks "positive authored gravity depth must accelerate a body toward the well center through Ymir"); Require(AetheriaRuntimeDaemonRenderQueries.EvaluateGravityTerrainHeight(zone, 10, 0, 0) < 0, "the same positive gravity-depth magnitude must project as a negative terrain well"); + var splats = AetheriaRuntimeGameDocuments.RenderSplatsViewport( + new AetheriaRuntimeDaemonFrameDocument + { + Run = new AetheriaRuntimeRunCheckpointCommit + { + RunId = "gravity-sign-smoke", + CurrentZoneIndex = 0, + Zones = [zone] + } + }, + new AetheriaRuntimeViewportBounds { MinX = -20, MinY = -20, MaxX = 20, MaxY = 20 }); + var bodySplatIndex = splats.Splats.SourceKey + .Select((sourceKey, index) => (sourceKey, index)) + .Single(value => value.sourceKey == body.BodyKey).index; + Require(splats.Splats.ValueR[bodySplatIndex] < 0, + "render-splat projection must convert positive canonical body depth into a negative gravity-height value"); } private static void ConstantWeaponRunsOnDaemonThroughYmirBeamContact() diff --git a/Aetheria.State.Daemon/Program.cs b/Aetheria.State.Daemon/Program.cs index 3db37dacc..c434aded6 100644 --- a/Aetheria.State.Daemon/Program.cs +++ b/Aetheria.State.Daemon/Program.cs @@ -3002,10 +3002,16 @@ static bool HasPlayableRun(AetheriaRuntimeRunCheckpointCommit? run) static bool HasTerminusRun(AetheriaRuntimeRunCheckpointCommit? run) { var zones = run?.Zones ?? Array.Empty(); + var terminusZone = zones.FirstOrDefault(zone => + string.Equals(zone.Name, "Daemon Generated Terminus", StringComparison.Ordinal)); var entities = zones.SelectMany(zone => zone.Entities ?? Array.Empty()).ToArray(); var bodies = zones.SelectMany(zone => zone.Bodies ?? Array.Empty()).ToArray(); return run?.GenerationSeed == AetheriaDaemonZoneGenerator.GenerationSeed && - zones.Any(zone => string.Equals(zone.Name, "Daemon Generated Terminus", StringComparison.Ordinal)) && + terminusZone != null && + terminusZone.GravityTerrainDepth > 0 && + (terminusZone.Bodies ?? Array.Empty()) + .Where(body => body.GravityInfluenceRadius > 0) + .All(body => body.GravityWellDepth > 0) && entities.Any(entity => string.Equals(entity.Name, "Anchor Station", StringComparison.Ordinal)) && entities.Count(entity => string.Equals(entity.FactionKey, "player", StringComparison.OrdinalIgnoreCase)) >= 4 && entities.Count(entity => string.Equals(entity.FactionKey, "raider", StringComparison.OrdinalIgnoreCase)) >= 3 && diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 3a7a30f9a..e8815d915 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -10097,8 +10097,11 @@ static void RequireClientTargetBootAuthority(string root) { "private static AetheriaRuntimeSurfaceDocument BuildRoot(", "\"STARBRIDGE\"", - "AetheriaMainMenuGravityField.FromDaemonFrame(daemonFrame)", - "GravitySurface($" + "GravitySurface($", + "AetheriaRuntimeDaemonSchemas.RenderSplatsViewport", + "AetheriaRuntimeDaemonSchemas.GravityViewport", + "AetheriaRuntimeDaemonSchemas.ObjectsViewport", + "new AetheriaRuntimeEmbeddedDocumentSlot(" }; var missingMainMenuSymbols = requiredMainMenuSymbols .Where(symbol => !mainMenu.Contains(symbol, StringComparison.Ordinal)) @@ -13999,8 +14002,11 @@ static void RequireMainMenuVerseHostDocumentAccess(string root) "public static AetheriaRuntimeSurfaceDocument BuildRoot(", "AetheriaRuntimeStateBootReport stateBoot", "AetheriaRuntimeVerseHostSettingsDocument verseHost", - "AetheriaMainMenuGravityField.FromDaemonFrame(daemonFrame)", "GravitySurface($", + "AetheriaRuntimeDaemonSchemas.RenderSplatsViewport", + "AetheriaRuntimeDaemonSchemas.GravityViewport", + "AetheriaRuntimeDaemonSchemas.ObjectsViewport", + "new AetheriaRuntimeEmbeddedDocumentSlot(", "\"AETHERIA\"", "\"STARBRIDGE\"", "\"Aetheria Starbridge\"" diff --git a/Aetheria.State/docs/daemon-code-map-for-rust-port.md b/Aetheria.State/docs/daemon-code-map-for-rust-port.md index dcb253347..6a6728789 100644 --- a/Aetheria.State/docs/daemon-code-map-for-rust-port.md +++ b/Aetheria.State/docs/daemon-code-map-for-rust-port.md @@ -83,7 +83,7 @@ These files define the deprecated reference surface. They should be treated as A | Command application | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs` | Current meaning of every gameplay command and rejection. | | Tick composition | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonTickRunner.cs` | Current order of command filtering, operation execution, sim step, frame creation, publication payloads. | | Daemon sim | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs` | Gameplay intents, hostile AI, combat meaning, heat, and projection of mandatory Ymir world results. | -| Ymir world adapter | `Aetheria.State.Daemon/AetheriaYmirWorldPhysics.cs` | Projects daemon state into Ymir and returns candidate body, field, contact, position, and velocity results for the daemon to commit. | +| Ymir world adapter | `Aetheria.State.Daemon/AetheriaYmirWorldPhysics.cs` | Projects daemon state into Ymir. Positive daemon gravity-depth magnitudes become positive attracting radial strengths; the adapter returns body, contact, position, and velocity facts for the daemon to commit without owning sign policy. | | Game viewport documents | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs` | Existing map, object, gravity, selected object, docking, refit, sector, and inventory projections. | | Client facade | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaClient.cs` | C# client observation and typed operation ergonomics. | | Verse client | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseClient.cs` | Current lower-level typed document reads/watches and command submission. | diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameDocuments.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameDocuments.cs index 5fa9121ff..0572e984d 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameDocuments.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameDocuments.cs @@ -220,7 +220,7 @@ namespace GameCult.Aetheria.State.Verse radius, AetheriaRuntimeRenderSplatChannels.Gravity, AetheriaRuntimeRenderSplatFalloffs.InverseSmooth, - body.GravityWellDepth, + -body.GravityWellDepth, 0, 0, 1, diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeMainMenuSurfaceBuilder.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeMainMenuSurfaceBuilder.cs index ff6b0e939..d634edf67 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeMainMenuSurfaceBuilder.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeMainMenuSurfaceBuilder.cs @@ -42,7 +42,6 @@ namespace GameCult.Aetheria.State.Verse { return BuildRoot( inGame, - gravityField: AetheriaMainMenuGravityField.Default(), updatedAtUtc, version); } @@ -59,7 +58,6 @@ namespace GameCult.Aetheria.State.Verse { return BuildRoot( inGame, - gravityField: AetheriaMainMenuGravityField.FromDaemonFrame(daemonFrame), updatedAtUtc, version); } @@ -76,7 +74,6 @@ namespace GameCult.Aetheria.State.Verse { return BuildRoot( inGame, - gravityField: AetheriaMainMenuGravityField.Default(), updatedAtUtc, version); } @@ -112,11 +109,9 @@ namespace GameCult.Aetheria.State.Verse private static AetheriaRuntimeSurfaceDocument BuildRoot( bool inGame, - AetheriaMainMenuGravityField gravityField, string updatedAtUtc, long version) { - gravityField = gravityField.IsEmpty ? AetheriaMainMenuGravityField.Default() : gravityField; var actions = new List(); if (!inGame) actions.Add(MenuButton($"{AetheriaRuntimeMainMenuCommands.RootSurfaceId}.continue", "Continue", AetheriaRuntimeMainMenuCommands.ContinueRun)); @@ -135,7 +130,7 @@ namespace GameCult.Aetheria.State.Verse ("height", "100vh"), ("minHeight", "100vh")), Style(("background", "#020606")), - GravitySurface($"{AetheriaRuntimeMainMenuCommands.RootSurfaceId}.gravity", gravityField), + GravitySurface($"{AetheriaRuntimeMainMenuCommands.RootSurfaceId}.gravity"), Node( $"{AetheriaRuntimeMainMenuCommands.RootSurfaceId}.menu", "column", @@ -419,9 +414,7 @@ namespace GameCult.Aetheria.State.Verse style); } - private static AetheriaRuntimeSurfaceComponent GravitySurface( - string id, - AetheriaMainMenuGravityField gravityField) + private static AetheriaRuntimeSurfaceComponent GravitySurface(string id) { var viewport = MainMenuViewport(); var renderSplatsDocumentId = ViewportDocumentId("aetheria.viewport.render_splats", viewport); @@ -581,279 +574,6 @@ namespace GameCult.Aetheria.State.Verse return value.ToString("0.###", CultureInfo.InvariantCulture); } - private static string GravityBodies(IReadOnlyList bodies) - { - return string.Join( - ";", - (bodies ?? Array.Empty()) - .Select(body => string.Join( - "|", - body.Key, - body.Kind, - F(body.X), - F(body.Y), - F(body.Radius), - F(body.Depth), - F(body.Exponent), - F(body.WaveRadius), - F(body.WaveDepth), - F(body.WaveSpeed), - body.IconAssetUri, - body.TintSplatAssetUri))); - } - - private static string GravityObjects(IReadOnlyList objects) - { - return string.Join( - ";", - (objects ?? Array.Empty()) - .Select(obj => string.Join( - "|", - S(obj.Key), - S(obj.Kind), - S(obj.Label), - F(obj.X), - F(obj.Y), - F(obj.DirectionX), - F(obj.DirectionY), - S(obj.FactionKey), - obj.Controlled ? "1" : "0", - F(obj.Visibility), - S(obj.IconAssetUri)))); - } - - private static string S(string value) - { - return (value ?? "").Replace("|", "/").Replace(";", ","); - } - } - - internal readonly struct AetheriaMainMenuGravityField - { - public AetheriaMainMenuGravityField( - double viewRadius, - double terrainRadius, - double terrainDepth, - double terrainDepthExponent, - double terrainWaveFrequency, - double simulationTimeSeconds, - IReadOnlyList bodies, - IReadOnlyList? objects = null) - { - ViewRadius = viewRadius; - TerrainRadius = terrainRadius; - TerrainDepth = terrainDepth; - TerrainDepthExponent = terrainDepthExponent <= 0 ? 1.0 : terrainDepthExponent; - TerrainWaveFrequency = terrainWaveFrequency; - SimulationTimeSeconds = simulationTimeSeconds; - Bodies = bodies ?? Array.Empty(); - Objects = objects ?? Array.Empty(); - } - - public double ViewRadius { get; } - public double TerrainRadius { get; } - public double TerrainDepth { get; } - public double TerrainDepthExponent { get; } - public double TerrainWaveFrequency { get; } - public double SimulationTimeSeconds { get; } - public IReadOnlyList Bodies { get; } - public IReadOnlyList Objects { get; } - public bool IsEmpty => Bodies == null || Bodies.Count == 0; - - public static AetheriaMainMenuGravityField Default() - { - return new AetheriaMainMenuGravityField( - 1200, - 1200, - -8, - 1.2, - 0.6, - 0, - new[] - { - new AetheriaMainMenuGravityBody("terminus-sun", "Sun", 0, 0, 900, -80, 3, 450, 10, 2), - new AetheriaMainMenuGravityBody("anchor-moon", "Planet", 360, -180, 260, -22, 2.2, 180, 4, 1.4) - }, - new[] - { - new AetheriaMainMenuGravityObject("anchor-station", "station", "Anchor Station", -220, -90, 1, 0, "player", true, 1), - new AetheriaMainMenuGravityObject("vanguard-one", "ship", "Vanguard One", -60, -40, 1, 0.2, "player", true, 1), - new AetheriaMainMenuGravityObject("ash-raider", "ship", "Ash Raider", 420, 180, -0.8, -0.1, "raider", false, 1) - }); - } - - public static AetheriaMainMenuGravityField FromDaemonFrame(AetheriaRuntimeDaemonFrameDocument? frame) - { - var run = frame?.Run; - var zones = run?.Zones ?? Array.Empty(); - var currentZone = zones.FirstOrDefault(zone => zone != null && zone.ZoneIndex == run?.CurrentZoneIndex) - ?? zones.FirstOrDefault(zone => zone != null); - if (currentZone == null) - return Default(); - - var bodies = (currentZone.Bodies ?? Array.Empty()) - .Where(body => body != null) - .Select(body => new AetheriaMainMenuGravityBody( - string.IsNullOrWhiteSpace(body.BodyKey) ? body.Name : body.BodyKey, - body.Kind, - double.IsNaN(body.GravityInfluenceCenterX) ? 0 : body.GravityInfluenceCenterX, - double.IsNaN(body.GravityInfluenceCenterZ) ? 0 : body.GravityInfluenceCenterZ, - body.GravityInfluenceRadius > 0 ? body.GravityInfluenceRadius : Math.Max(160, body.GravityRadiusMultiplier * 220), - body.GravityWellDepth, - body.GravityDepthExponent <= 0 ? 1.0 : body.GravityDepthExponent, - body.GravityWaveRadius, - body.GravityWaveDepth, - body.GravityWaveSpeed)) - .ToArray(); - var objects = (currentZone.Entities ?? Array.Empty()) - .Where(entity => entity != null && entity.IsActive) - .Select(entity => - { - var entityKey = AetheriaRuntimeRunCheckpointCommit.EntityRecordKey(run?.RunId ?? "local-terminus", currentZone.ZoneIndex, entity.EntityIndex); - return new AetheriaMainMenuGravityObject( - entityKey, - entity.Kind, - string.IsNullOrWhiteSpace(entity.Name) ? $"entity {entity.EntityIndex.ToString(CultureInfo.InvariantCulture)}" : entity.Name, - entity.PositionX, - entity.PositionZ, - entity.DirectionX, - entity.DirectionY, - entity.FactionKey, - string.Equals(entity.FactionKey, "player", StringComparison.OrdinalIgnoreCase) || - string.Equals(entityKey, run?.CurrentEntityKey, StringComparison.Ordinal), - entity.Visibility <= 0 ? 1 : entity.Visibility); - }) - .ToArray(); - - return new AetheriaMainMenuGravityField( - Math.Max(900, AutoViewRadius(currentZone, bodies, objects)), - currentZone.GravityTerrainRadius > 0 ? currentZone.GravityTerrainRadius : 1200, - currentZone.GravityTerrainDepth == 0 ? 8 : currentZone.GravityTerrainDepth, - currentZone.GravityTerrainDepthExponent <= 0 ? 1.0 : currentZone.GravityTerrainDepthExponent, - currentZone.GravityTerrainWaveFrequency, - currentZone.SimulationTimeSeconds, - bodies.Length == 0 ? Default().Bodies : bodies, - objects); - } - - private static double AutoViewRadius( - AetheriaRuntimeZoneSnapshotCommit currentZone, - IReadOnlyList bodies, - IReadOnlyList objects) - { - var radius = currentZone.GravityTerrainRadius > 0 ? currentZone.GravityTerrainRadius : 1200; - foreach (var body in bodies ?? Array.Empty()) - radius = Math.Max(radius, Math.Max(Math.Abs(body.X), Math.Abs(body.Y)) + body.Radius * 1.1); - foreach (var obj in objects ?? Array.Empty()) - radius = Math.Max(radius, Math.Max(Math.Abs(obj.X), Math.Abs(obj.Y)) + 220); - return radius; - } - } - - internal readonly struct AetheriaMainMenuGravityBody - { - public AetheriaMainMenuGravityBody( - string key, - string kind, - double x, - double y, - double radius, - double depth, - double exponent, - double waveRadius, - double waveDepth, - double waveSpeed) - { - Key = key ?? ""; - Kind = string.IsNullOrWhiteSpace(kind) ? "Body" : kind; - X = x; - Y = y; - Radius = radius <= 0 ? 1 : radius; - Depth = depth; - Exponent = exponent <= 0 ? 1.0 : exponent; - WaveRadius = waveRadius; - WaveDepth = waveDepth; - WaveSpeed = waveSpeed; - IconAssetUri = $"cultmesh://aetheria/assets/icons/ui/{IconKind(Kind)}"; - TintSplatAssetUri = "cultmesh://aetheria/assets/textures/tint_splat"; - } - - public string Key { get; } - public string Kind { get; } - public double X { get; } - public double Y { get; } - public double Radius { get; } - public double Depth { get; } - public double Exponent { get; } - public double WaveRadius { get; } - public double WaveDepth { get; } - public double WaveSpeed { get; } - public string IconAssetUri { get; } - public string TintSplatAssetUri { get; } - - private static string IconKind(string kind) - { - var normalized = kind?.ToLowerInvariant() ?? ""; - if (normalized.Contains("sun") || normalized.Contains("star")) - return "sun"; - if (normalized.Contains("gas")) - return "gasgiant"; - if (normalized.Contains("moon")) - return "moon"; - if (normalized.Contains("orbital") || normalized.Contains("station") || normalized.Contains("colony")) - return "orbital"; - if (normalized.Contains("wormhole")) - return "wormhole"; - return "planet"; - } - } - - internal readonly struct AetheriaMainMenuGravityObject - { - public AetheriaMainMenuGravityObject( - string key, - string kind, - string label, - double x, - double y, - double directionX, - double directionY, - string factionKey, - bool controlled, - double visibility) - { - Key = key ?? ""; - Kind = string.IsNullOrWhiteSpace(kind) ? "object" : kind; - Label = label ?? ""; - X = x; - Y = y; - DirectionX = directionX; - DirectionY = directionY; - FactionKey = factionKey ?? ""; - Controlled = controlled; - Visibility = visibility <= 0 ? 1 : visibility; - IconAssetUri = $"cultmesh://aetheria/assets/icons/ui/{IconKind(Kind)}"; - } - - public string Key { get; } - public string Kind { get; } - public string Label { get; } - public double X { get; } - public double Y { get; } - public double DirectionX { get; } - public double DirectionY { get; } - public string FactionKey { get; } - public bool Controlled { get; } - public double Visibility { get; } - public string IconAssetUri { get; } - - private static string IconKind(string kind) - { - var normalized = kind?.ToLowerInvariant() ?? ""; - if (normalized.Contains("station") || normalized.Contains("colony")) - return "orbital"; - return "ship"; - } } public enum AetheriaRuntimeMainMenuCommandKind diff --git a/docs/aetheria-current-codebase-model.md b/docs/aetheria-current-codebase-model.md index 3f9b70536..34ecddca7 100644 --- a/docs/aetheria-current-codebase-model.md +++ b/docs/aetheria-current-codebase-model.md @@ -193,25 +193,21 @@ Remaining debt: ## Gravity Terrain -There are now shared daemon render queries for gravity influence brushes and -terrain sampling. `ZoneRenderer` uses -`AetheriaRuntimeDaemonRenderQueries.EvaluateGravityTerrainHeight` and -`QueryGravityTerrainBand` for rendering. +Daemon zone/body state owns gravity as positive depth magnitudes. The Ymir +adapter passes `GravityWellDepth` directly into positive-strength radial fields, +which attract. Shared render queries subtract those magnitudes into negative +terrain wells, and `AetheriaRuntimeGameDocuments.RenderSplatsViewport` performs +the same derived sign conversion for `gravity.height` splats. -The old `Assets/Scripts/Gravity.cs` still exists. It samples Unity -`GravityObject` materials and shader names directly. That path is a legacy -Unity-local gravity evaluator. It should not own gameplay gravity or canonical -terrain facts. The desirable shape is: +`AetheriaRuntimeDaemonRenderQueries` owns gravity influence brushes, terrain +sampling, and terrain bands. `ZoneRenderer` consumes those queries. The main +menu embeds the typed gravity, render-splat, and object viewport documents; its +old fallback body/object world has been deleted. -- daemon zone state defines body/orbit/gravity influence data; -- shared runtime query code answers viewport/intersection questions such as - "which influence brushes intersect this XZ area?"; -- Unity receives only the brushes and terrain samples needed for the current - camera/minimap/render pass; -- Ymir receives physics-relevant fields from the same daemon state. - -`Aetheria.State.Verify` already checks for shared gravity influence query -symbols and pushes `ZoneRenderer` away from mirrored Unity `Zone` math. +The old `Assets/Scripts/Gravity.cs` remains a legacy Unity-local visual +evaluator. It does not own gameplay gravity or canonical terrain facts. Unity +receives provider projections for the active camera/minimap/render pass, while +`AetheriaYmirWorldPhysics` receives physics fields from the same daemon state. ## Stats And Behaviors diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 551cd042a..9a58b88ae 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -839,6 +839,32 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. are deleted. The old enum ordinal remains reserved only to avoid shifting serialized command values and is rejected by the daemon. +### Daemon Gravity Field Contract + +- Owner: typed daemon zone/body state owns positive gravity-depth magnitudes; + embedded Ymir owns radial integration and contact facts without reinterpreting + their sign. +- Inputs: body poses, influence radii, positive `GravityWellDepth`, positive + zone `GravityTerrainDepth`, exponents, waves, and fixed delta. +- Outputs: Ymir produces inward acceleration, positions, velocities, and + contacts. Daemon projections publish typed gravity viewport and render-splat + documents from the same state. +- Derived state: terrain samples and `gravity.height` splat values are negative + because presentation subtracts the positive magnitude. Bands, Eve fields, + Unity meshes, and cameras remain derived. +- Forbidden writers: negative authored depths, adapter-side sign compensators, + Unity gravity scripts, Eve lowerers, main-menu fallback fixtures, and stale + persisted Terminus rows cannot author gravity semantics. +- Shared paths: generated, restored, and imported worlds use the same magnitude + convention; physics and every render projection consume those canonical + values. +- Cut line: the dead main-menu fallback world is deleted. Built-in Terminus + state with the old sign fails the current-scenario check and is regenerated; + only presentation conversion may negate depth. +- Verification layer: daemon smoke proves inward Ymir velocity, negative sampled + height, and negative body splats. Released cold/warm witnesses exercise the + persisted daemon path. + - Owner: `Aetheria.State` owns the new typed state spine for durable state. The embedded `GameCult.Aetheria.State.Unity` package owns Unity's runtime typed state file path plus the boot-time state-file probe, and is the landing