From 205098ef93bd0f7fa59112abe4772d88c68fbc79 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 21:19:09 +0200 Subject: [PATCH] Zone.Update takes every previous position before placing any orbit GetOrbitPosition places a parent as a side effect of placing its child, so with a child listed first the parent's previous position was already its new one and its Velocity came out zero. The constructor's unread PreviousPosition write is dropped; the first Update takes it from the placed Position. The velocity test now runs moon-first and planet-first packs over varying dt. Builds not verified yet (verify follows). Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/Zone.cs | 7 ++-- .../ZonePlacedAtBuildTests.cs | 32 ++++++++++++------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/Assets/Scripts/ServerShared/Zone.cs b/Assets/Scripts/ServerShared/Zone.cs index 715d965bd..eee1aa869 100644 --- a/Assets/Scripts/ServerShared/Zone.cs +++ b/Assets/Scripts/ServerShared/Zone.cs @@ -165,7 +165,7 @@ public class Zone // when the zone is built, so each well, light and body pose read before the first Update is where the // body is. Velocity stays zero until the first Update differences two placed positions. foreach (var orbit in Orbits) - orbit.Value.PreviousPosition = GetOrbitPosition(orbit.Key); + GetOrbitPosition(orbit.Key); foreach (var body in pack.Planets) { @@ -276,9 +276,12 @@ public class Zone { _time += deltaTime; _updatedOrbits.Clear(); + // Every previous position is taken before any orbit is placed: GetOrbitPosition places a parent as a + // side effect of placing its child, so differencing inside one loop would depend on dictionary order. + foreach (var orbit in Orbits) + orbit.Value.PreviousPosition = orbit.Value.Position; foreach (var orbit in Orbits) { - orbit.Value.PreviousPosition = orbit.Value.Position; orbit.Value.Position = GetOrbitPosition(orbit.Key); orbit.Value.Velocity = (orbit.Value.Position - orbit.Value.PreviousPosition) / deltaTime; } diff --git a/tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs b/tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs index 87085b922..de9015a57 100644 --- a/tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs +++ b/tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs @@ -60,7 +60,7 @@ public sealed class ZonePlacedAtBuildTests : IDisposable public CultRecordKey PlanetOrbit; } - private Scene Build(float phase, double time) + private Scene Build(float phase, double time, bool moonFirst = false) { var dir = Path.Combine(_root, $"zone{_caches.Count}"); Directory.CreateDirectory(dir); @@ -78,15 +78,21 @@ public sealed class ZonePlacedAtBuildTests : IDisposable { Parent = new CultRecordRef(rootOrbit), Distance = Distance, Phase = phase }).Key; + var moonOrbit = cache.Upsert(new OrbitData + { + Parent = new CultRecordRef(planetOrbit), Distance = 100f + }).Key; var planet = cache.Upsert(new PlanetData { Orbit = new CultRecordRef(planetOrbit), Mass = 1f }).Key; var pack = new ZonePack { - Orbits = { new CultRecordRef(rootOrbit), new CultRecordRef(planetOrbit) }, Planets = { new CultRecordRef(planet) }, Radius = 5000f, Mass = 10000f, Time = time }; + // The same three orbits either way; only the order the pack lists them in differs. + var listed = moonFirst ? new[] { moonOrbit, planetOrbit, rootOrbit } : new[] { rootOrbit, planetOrbit, moonOrbit }; + foreach (var key in listed) pack.Orbits.Add(new CultRecordRef(key)); var zone = new Zone(items, Settings(), pack, new GalaxyZone { Name = "Placed", Owner = null }, null); return new Scene { Cache = cache, Items = items, Hull = hull, Zone = zone, PlanetOrbit = planetOrbit }; } @@ -104,7 +110,6 @@ public sealed class ZonePlacedAtBuildTests : IDisposable var expected = RootAt + OrbitData.Evaluate(frac(turns + phase)) * Distance; Assert.InRange(length(orbit.Position - expected), 0f, .01f); - Assert.InRange(length(orbit.PreviousPosition - expected), 0f, .01f); // The well pulls toward the body's place: a point 30 u east of it is pushed west. var force = s.Zone.GetForce(expected + new float2(30f, 0f)); @@ -112,16 +117,21 @@ public sealed class ZonePlacedAtBuildTests : IDisposable Assert.InRange(normalize(force).x, -1f, -.99f); } - [Fact] - public void TheFirstTickVelocityIsTheOrbitsSpeed() + // A parent's velocity is its own orbital velocity on the first tick and every tick after, whichever order + // the pack lists a moon and its planet in. dt differs per tick so a velocity taken from a fixed step fails. + [Theory] + [InlineData(false)] + [InlineData(true)] + public void TheFirstTickVelocityIsTheOrbitsSpeed(bool moonFirst) { - var s = Build(.35f, 0.0); - const float dt = 1f / 60f; - s.Zone.Update(dt); - - var speed = length(s.Zone.Orbits[s.PlanetOrbit].Velocity); + var s = Build(.35f, 0.0, moonFirst); var expected = 2f * PI * Distance / Period; - Assert.InRange(speed, expected * .99f, expected * 1.01f); + foreach (var dt in new[] { 1f / 60f, 1f / 30f, 1f / 60f }) + { + s.Zone.Update(dt); + var speed = length(s.Zone.Orbits[s.PlanetOrbit].Velocity); + Assert.InRange(speed, expected * .99f, expected * 1.01f); + } } [Fact]