From 13c7a08707a417495b5d064e1cef335583b45960 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 15 Oct 2026 08:44:11 +0200 Subject: [PATCH] Gravity.cginc is generated: GravityShader.Source() is its one author, a test pins the committed file to it byte for byte The mirror test compared a normalised reading of two spellings and was beaten twice (a statement before the return, gravityHeight's sum). GravityShader writes the whole file (potential, zero-strength guard, reference term, height sum) from GravityField's constant (TailOffset); GravityShaderTests fails on any difference. GravityShaderMirrorTests deleted. GravityFieldParityVerify opens Main Menu.unity: no ZoneRenderer there, its Fog Surface Displacement camera is the menu's own brush presentation (layers 9 and 10) by spec. Builds not yet verified on this commit; verified at the final merge. Co-Authored-By: Claude Opus 5.5 --- .../Editor/GravityFieldParityVerify.cs | 30 +++++++++ Assets/Scripts/ServerShared/GravityField.cs | 5 +- Assets/Scripts/ServerShared/GravityShader.cs | 65 +++++++++++++++++++ .../ServerShared/GravityShader.cs.meta | 11 ++++ Assets/Shaders/Gravity.cginc | 9 +-- .../GravityShaderMirrorTests.cs | 50 -------------- .../GravityShaderTests.cs | 23 +++++++ 7 files changed, 138 insertions(+), 55 deletions(-) create mode 100644 Assets/Scripts/ServerShared/GravityShader.cs create mode 100644 Assets/Scripts/ServerShared/GravityShader.cs.meta delete mode 100644 tests/Aetheria.Shared.Tests/GravityShaderMirrorTests.cs create mode 100644 tests/Aetheria.Shared.Tests/GravityShaderTests.cs diff --git a/Assets/Scripts/Editor/GravityFieldParityVerify.cs b/Assets/Scripts/Editor/GravityFieldParityVerify.cs index 0f459c016..ae512d16e 100644 --- a/Assets/Scripts/Editor/GravityFieldParityVerify.cs +++ b/Assets/Scripts/Editor/GravityFieldParityVerify.cs @@ -251,6 +251,34 @@ namespace Aetheria.EditorTools if (compared < Size * Size / 2) Fail("gradient", $"only {compared} texels were compared"); } + // Main Menu.unity has no ZoneRenderer, so no Gravity Field and no lowered potential: its gravity cameras draw the + // menu's own brush objects (Boundary on layer 9, the Waves on layer 10) and are presentation by spec (follow-up + // menu-grid-brushes), not a reader of the potential. This pins that: a ZoneRenderer in the menu would make its + // cameras readers of the field, and they would have to cull layer 9 alone like ARPG's. + static void CheckMenuWiring() + { + var scene = EditorSceneManager.OpenScene("Assets/Scenes/Main Menu.unity", OpenSceneMode.Single); + try + { + if (UnityEngine.Object.FindFirstObjectByType(FindObjectsInactive.Include) != null) + Fail("wiring", "Main Menu.unity has a ZoneRenderer: its gravity cameras now read the Gravity Field and must cull layer 9 alone"); + var seen = 0; + foreach (var camera in UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None)) + { + if (camera.targetTexture == null || camera.targetTexture.name != "Fog Surface Displacement") continue; + seen++; + const int menuBrushLayers = (1 << 9) | (1 << 10); + if (camera.cullingMask != menuBrushLayers) + Fail("wiring", $"Main Menu camera {camera.name} culls mask {camera.cullingMask}, not the menu brush layers 9 and 10"); + } + if (seen != 1) Fail("wiring", $"{seen} Fog Surface Displacement cameras in Main Menu.unity, expected 1"); + } + finally + { + EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); + } + } + static void CheckWiring(GameSettings settings) { var scene = EditorSceneManager.OpenScene("Assets/Scenes/ARPG.unity", OpenSceneMode.Single); @@ -287,6 +315,8 @@ namespace Aetheria.EditorTools EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); } + CheckMenuWiring(); + foreach (var path in new[] { "Assets/Prefabs/RPG/Planets/Planet.prefab", "Assets/Prefabs/RPG/Planets/Gas Giant.prefab", diff --git a/Assets/Scripts/ServerShared/GravityField.cs b/Assets/Scripts/ServerShared/GravityField.cs index 71e1f0c2a..01a070e9d 100644 --- a/Assets/Scripts/ServerShared/GravityField.cs +++ b/Assets/Scripts/ServerShared/GravityField.cs @@ -10,13 +10,16 @@ using float2 = CultMath.float2; /// public static class GravityField { + // The tail's additive constant: it makes the tail meet the core in value at the core radius. Gravity.cginc is generated from it. + public const float TailOffset = .5f; + // The potential at distance r from the body, defined up to a constant (callers subtract their reference). // A well of no strength (a body whose curves are unauthored, so c = 0 and R = 0) digs nothing. public static float Potential(float c, float coreRadius, float r) => c == 0f ? 0f : r < coreRadius ? c * r * r / (2 * coreRadius * coreRadius) - : c * (log(r / coreRadius) + .5f); + : c * (log(r / coreRadius) + TailOffset); // The acceleration at `offsetFromBody`, toward the body: zero at the body itself. public static float2 Acceleration(float c, float coreRadius, float2 offsetFromBody) diff --git a/Assets/Scripts/ServerShared/GravityShader.cs b/Assets/Scripts/ServerShared/GravityShader.cs new file mode 100644 index 000000000..e9dfd6224 --- /dev/null +++ b/Assets/Scripts/ServerShared/GravityShader.cs @@ -0,0 +1,65 @@ +using System.Globalization; + +/// +/// The single author of Assets/Shaders/Gravity.cginc, the GPU lowering of the zone's potential. The law is +/// GravityField's (its constants are read here, not respelled) and the height is Zone.GetHeight: each well's +/// potential minus its reference, summed, divided by the exaggeration. Unity does not run C# when it imports a +/// shader, so the committed cginc is this output; GravityShaderTests fails unless it equals Source() exactly. +/// To change the shader, change this file and regenerate: AETHERIA_REGENERATE_GRAVITY_CGINC=1 dotnet test --filter GravityShaderTests. +/// +public static class GravityShader +{ + public const string Path = "Assets/Shaders/Gravity.cginc"; + + /// The whole file, with LF line ends. + public static string Source() => (Template + .Replace("@TAIL@", GravityField.TailOffset.ToString("R", CultureInfo.InvariantCulture))) + .Replace("\r\n", "\n"); + + private const string Template = """ +#ifndef AETHERIA_GRAVITY_INCLUDED +#define AETHERIA_GRAVITY_INCLUDED + +// GENERATED by ServerShared/GravityShader.cs from GravityField; do not edit (GravityShaderTests fails on any difference). +// The lowering of the zone's potential (aetheria-gravity, cut grid-lowering). The law is ServerShared's GravityField +// and the height is Zone.GetHeight; this file decides nothing. GravityLowering.Lower fills the buffer and the globals +// from the zone (positions from Orbit.Position, reference from Zone.WellReference) each frame. +// GravityFieldParityVerify renders it and compares every texel with -Zone.GetHeight. + +struct GravityWell +{ + float2 position; + float strength; + float coreRadius; + float reference; +}; + +StructuredBuffer _GravityWells; +int _GravityWellCount; +float _HeightExaggeration; + +// GravityField.Potential: c is the well's strength, R its core radius, r the distance. +float gravityPotential(float c, float R, float r) +{ + return c == 0 ? 0 : + r < R + ? c * r * r / (2 * R * R) + : c * (log(r / R) + @TAIL@); +} + +// Zone.GetHeight at world xz: each well's potential minus its own reference, summed, divided by the exaggeration. +float gravityHeight(float2 p) +{ + float result = 0; + for (int i = 0; i < _GravityWellCount; i++) + { + GravityWell well = _GravityWells[i]; + result += gravityPotential(well.strength, well.coreRadius, length(p - well.position)) - well.reference; + } + return result / _HeightExaggeration; +} + +#endif + +"""; +} diff --git a/Assets/Scripts/ServerShared/GravityShader.cs.meta b/Assets/Scripts/ServerShared/GravityShader.cs.meta new file mode 100644 index 000000000..e241df3ba --- /dev/null +++ b/Assets/Scripts/ServerShared/GravityShader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9a3fa1e9df764459b709270e58e79974 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Shaders/Gravity.cginc b/Assets/Shaders/Gravity.cginc index 8cc256d94..47a419518 100644 --- a/Assets/Shaders/Gravity.cginc +++ b/Assets/Shaders/Gravity.cginc @@ -1,9 +1,10 @@ #ifndef AETHERIA_GRAVITY_INCLUDED #define AETHERIA_GRAVITY_INCLUDED +// GENERATED by ServerShared/GravityShader.cs from GravityField; do not edit (GravityShaderTests fails on any difference). // The lowering of the zone's potential (aetheria-gravity, cut grid-lowering). The law is ServerShared's GravityField -// and the height is Zone.GetHeight; this file mirrors them and decides nothing. GravityLowering.Lower fills the -// buffer and the globals from the zone (positions from Orbit.Position, reference from Zone.WellReference) each frame. +// and the height is Zone.GetHeight; this file decides nothing. GravityLowering.Lower fills the buffer and the globals +// from the zone (positions from Orbit.Position, reference from Zone.WellReference) each frame. // GravityFieldParityVerify renders it and compares every texel with -Zone.GetHeight. struct GravityWell @@ -18,13 +19,13 @@ StructuredBuffer _GravityWells; int _GravityWellCount; float _HeightExaggeration; -// GravityField.Potential, line for line: c is the well's strength, R its core radius, r the distance. +// GravityField.Potential: c is the well's strength, R its core radius, r the distance. float gravityPotential(float c, float R, float r) { return c == 0 ? 0 : r < R ? c * r * r / (2 * R * R) - : c * (log(r / R) + .5); + : c * (log(r / R) + 0.5); } // Zone.GetHeight at world xz: each well's potential minus its own reference, summed, divided by the exaggeration. diff --git a/tests/Aetheria.Shared.Tests/GravityShaderMirrorTests.cs b/tests/Aetheria.Shared.Tests/GravityShaderMirrorTests.cs deleted file mode 100644 index e4f182e25..000000000 --- a/tests/Aetheria.Shared.Tests/GravityShaderMirrorTests.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.IO; -using System.Text.RegularExpressions; -using Xunit; - -// potential-owns-gravity (aetheria-gravity, cut grid-lowering): Assets/Shaders/Gravity.cginc lowers the potential for -// the GPU and decides nothing, so its gravityPotential must be GravityField.Potential, term for term, zero-strength -// guard and core branch included. The shader cannot be compiled here (GravityFieldParityVerify renders it, on Starfire -// only), so this test is the automated guard: each copy's expression is read from its file, reduced to the same -// spelling (float suffixes, the core radius's name, whitespace and comments), and compared. A drift in either file fails it. -public sealed class GravityShaderMirrorTests -{ - private static string Read(string relative) => - File.ReadAllText(Path.Combine(RunStartTests.FindRepoRoot(), relative)); - - // The text between `start` and the first `;` after it, comments removed and spellings reduced. - private static string Expression(string source, string start, string then = "") - { - source = Regex.Replace(source, @"//[^\n]*", ""); - var from = source.IndexOf(start, System.StringComparison.Ordinal); - Assert.True(from >= 0, $"'{start}' not found"); - var body = source.Substring(from + start.Length); - if (then != "") - { - var at = body.IndexOf(then, System.StringComparison.Ordinal); - Assert.True(at >= 0, $"'{then}' not found after '{start}'"); - body = body.Substring(at + then.Length); - } - body = body.Substring(0, body.IndexOf(';')); - body = Regex.Replace(body, @"(?<=[\d.])f\b", ""); - body = body.Replace("coreRadius", "R"); - return Regex.Replace(body, @"\s+", ""); - } - - private static string CSharpLaw() => - Expression(Read("Assets/Scripts/ServerShared/GravityField.cs"), "Potential(float c, float coreRadius, float r) =>"); - - private static string ShaderLaw() => - Expression(Read("Assets/Shaders/Gravity.cginc"), "float gravityPotential(float c, float R, float r)", "return"); - - [Fact] - public void TheShaderRunsTheSameLawAsGravityFieldPotential() - { - var law = CSharpLaw(); - // The reduction found the whole law, so equality is not two empty strings agreeing. - Assert.Contains("log(r/R)", law); - Assert.Contains("c==0?0:", law); - Assert.Contains("c*r*r/(2*R*R)", law); - Assert.Equal(law, ShaderLaw()); - } -} diff --git a/tests/Aetheria.Shared.Tests/GravityShaderTests.cs b/tests/Aetheria.Shared.Tests/GravityShaderTests.cs new file mode 100644 index 000000000..dcd0e5d22 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/GravityShaderTests.cs @@ -0,0 +1,23 @@ +using System; +using System.IO; +using Xunit; + +// potential-owns-gravity (aetheria-gravity, cut grid-lowering): Assets/Shaders/Gravity.cginc is the output of +// GravityShader.Source(), which reads GravityField's constants and writes the one lowering of the potential, the +// zero-strength guard, each well's reference and gravityHeight's sum. The committed file must equal it, character for +// character (line ends aside): any added statement, respelling, flipped sign, swapped argument or macro fails here. +// To change the shader, change GravityShader and run once with AETHERIA_REGENERATE_GRAVITY_CGINC=1. +public sealed class GravityShaderTests +{ + [Fact] + public void TheCommittedShaderIsTheGeneratedOne() + { + var path = Path.Combine(RunStartTests.FindRepoRoot(), GravityShader.Path); + if (Environment.GetEnvironmentVariable("AETHERIA_REGENERATE_GRAVITY_CGINC") == "1") + File.WriteAllText(path, GravityShader.Source()); + var generated = GravityShader.Source(); + // The law was generated, not an empty template agreeing with an empty file. + Assert.Contains("log(r / R) + 0.5", generated); + Assert.Equal(generated, File.ReadAllText(path).Replace("\r\n", "\n")); + } +}