diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index e821e96ac..5ee4b474a 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -336,7 +336,7 @@ public class ActionGameManager : MonoBehaviour Input.Global.ZoneMap.performed += context => { ToggleMenuTab(MenuTab.Map); - MenuMap.Position = CurrentEntity.Position.xz; + MenuMap.Position = DrawAhead.Position(CurrentEntity, Clock.Lead).xz; }; Input.Global.Inventory.performed += context => ToggleMenuTab(MenuTab.Inventory); @@ -1316,13 +1316,13 @@ public class ActionGameManager : MonoBehaviour foreach (var indicator in _visibleHostileIndicators) { indicator.Value.gameObject.SetActive(indicator.Key!=CurrentEntity.Target.Value.Entity); - indicator.Value.Place.Target = indicator.Key.Position.ToUnity(); + indicator.Value.Place.Target = DrawAhead.Position(indicator.Key, Clock.Lead).ToUnity(); PlaceFill(indicator.Value, indicator.Key); } foreach (var indicator in _visibleFriendlyIndicators) { indicator.Value.gameObject.SetActive(indicator.Key!=CurrentEntity.Target.Value.Entity); - indicator.Value.Place.Target = indicator.Key.Position.ToUnity(); + indicator.Value.Place.Target = DrawAhead.Position(indicator.Key, Clock.Lead).ToUnity(); PlaceFill(indicator.Value, indicator.Key); } var look = Input.Player.Look.ReadValue(); diff --git a/Assets/Scripts/Gameplay/Weapons/Projectile.cs b/Assets/Scripts/Gameplay/Weapons/Projectile.cs index aa43051c1..e5f0b1e86 100644 --- a/Assets/Scripts/Gameplay/Weapons/Projectile.cs +++ b/Assets/Scripts/Gameplay/Weapons/Projectile.cs @@ -51,7 +51,7 @@ public class Projectile : MonoBehaviour { var lead = ActionGameManager.Clock.Lead; var drawn = DrawAhead.Time(_zone, lead); - transform.position = V(DrawAhead.Round(_shot, _known, _barrel, BlendTime, drawn, lead)); + transform.position = V(DrawAhead.Round(_shot, _known, _barrel, BlendTime, drawn)); if (DrawAhead.RoundOver(_shot, _known, drawn)) Finish(); } diff --git a/Assets/Scripts/ServerShared/DrawAhead.cs b/Assets/Scripts/ServerShared/DrawAhead.cs index c6d5b8309..92c5c8d9f 100644 --- a/Assets/Scripts/ServerShared/DrawAhead.cs +++ b/Assets/Scripts/ServerShared/DrawAhead.cs @@ -91,18 +91,18 @@ public static class DrawAhead t.w * q.w - t.x * q.x - t.y * q.y - t.z * q.z)); } - // A ballistic round at the drawn time (DrawAhead.Time, passed in by the caller, as lead is): on its frozen line while the outcome is unknown or a + // A ballistic round at the drawn time (DrawAhead.Time, passed in by the caller; the target's lead is that time less its zone's step time, derived here, never a second input): on its frozen line while the outcome is unknown or a // miss, with the barrel's offset from that line decaying to nothing over blendTime sim seconds from the moment of firing. // A round the sim published as a hit or burst steers from its line, from the commit tick, onto where it ends by the // time damage lands: a burst's frozen point, else the impact cell on the target's drawn pose; and is held there after. - public static float3 Round(in PendingShot shot, ShotOutcome known, float3 barrel, float blendTime, double drawnTime, float lead) + public static float3 Round(in PendingShot shot, ShotOutcome known, float3 barrel, float blendTime, double drawnTime) { var time = (float) drawnTime; var drawn = Lifted(shot, Line(shot, known?.Result, time), barrel, blendTime, time); if (known == null || known.Result == ShotResult.Miss) return drawn; var end = known.HasBurstPoint ? float3(known.BurstPoint.x, shot.FireOrigin.y, known.BurstPoint.y) - : Impact(known.Target, known.Cell, lead); + : Impact(known.Target, known.Cell, (float) (drawnTime - known.Target.Zone.ExactTime)); var commit = shot.ArrivalTime - known.ArrivalIn; var steer = shot.ArrivalTime > commit ? saturate((time - commit) / (shot.ArrivalTime - commit)) : 1f; return drawn + (end - drawn) * steer; diff --git a/Assets/Scripts/ServerShared/Zone.cs b/Assets/Scripts/ServerShared/Zone.cs index 1f2b0ca5e..b7b099253 100644 --- a/Assets/Scripts/ServerShared/Zone.cs +++ b/Assets/Scripts/ServerShared/Zone.cs @@ -533,8 +533,13 @@ public class Zone // The same rail law at any time, with no memo read or written: a parent is its own rail at that time. private float2 RailAt(CultRecordKey orbitID, double time) { - if (!orbitID.IsSet() || !Orbits.TryGetValue(orbitID, out var orbit)) + if (!orbitID.IsSet()) return float2.zero; + if (!Orbits.TryGetValue(orbitID, out var orbit)) + { + Log?.Invoke("Requested orbit is not part of this zone!"); + return float2.zero; + } var parentPosition = !orbit.Data.Parent.IsSet() ? orbit.Data.FixedPosition : RailAt(orbit.Data.Parent.Key, time); return parentPosition + Offset(orbit, time); } diff --git a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs index eb878718c..ad70f24b8 100644 --- a/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs +++ b/tests/Aetheria.Shared.Tests/BallisticFlightTests.cs @@ -67,28 +67,28 @@ public sealed partial class FireControlCut124Tests // The shooter moves on after the round left: the line is the one frozen at Fire, not one drawn from where it is now. e.Shooter.Position += float3(7, 0, 3); - var start = DrawAhead.Round(burst, null, burst.FireOrigin, 0f, burst.FireTime, 0f); + var start = DrawAhead.Round(burst, null, burst.FireOrigin, 0f, burst.FireTime); Assert.Equal(origin.x, start.x, 3); Assert.Equal(origin.z, start.z, 3); - var second = DrawAhead.Round(burst, null, burst.FireOrigin, 0f, burst.FireTime + 1f, 0f); + var second = DrawAhead.Round(burst, null, burst.FireOrigin, 0f, burst.FireTime + 1f); Assert.Equal(origin.x + aim.x * BallisticSpeed, second.x, 3); Assert.Equal(origin.z + aim.y * BallisticSpeed, second.z, 3); foreach (var known in new[] { null, outcome }) foreach (var later in new[] { 0f, 1f, 50f }) { - var arrival = DrawAhead.Round(burst, known, burst.FireOrigin, 0f, burst.ArrivalTime + later, 0f); + var arrival = DrawAhead.Round(burst, known, burst.FireOrigin, 0f, burst.ArrivalTime + later); Assert.Equal(burst.BurstPosition.x, arrival.x, 3); Assert.Equal(burst.BurstPosition.z, arrival.z, 3); } // Past the weapon's range it is held at the range point, however late it is asked. - var held = DrawAhead.Round(burst, Known(burst, ShotResult.Miss), burst.FireOrigin, 0f, burst.FireTime + NoLockRange / BallisticSpeed + 50f, 0f); + var held = DrawAhead.Round(burst, Known(burst, ShotResult.Miss), burst.FireOrigin, 0f, burst.FireTime + NoLockRange / BallisticSpeed + 50f); Assert.Equal(origin.x + aim.x * NoLockRange, held.x, 3); Assert.Equal(origin.z + aim.y * NoLockRange, held.z, 3); // Before it was fired it is still at the origin, not behind it. - var before = DrawAhead.Round(burst, null, burst.FireOrigin, 0f, burst.FireTime - 5f, 0f); + var before = DrawAhead.Round(burst, null, burst.FireOrigin, 0f, burst.FireTime - 5f); Assert.Equal(origin.x, before.x, 3); Assert.Equal(origin.z, before.z, 3); @@ -100,12 +100,12 @@ public sealed partial class FireControlCut124Tests // A round not known to miss waits at its arrival point however late it is asked. foreach (var at in new[] { direct.ArrivalTime, direct.FireTime + 99f }) { - var waiting = DrawAhead.Round(direct, null, direct.FireOrigin, 0f, at, 0f); + var waiting = DrawAhead.Round(direct, null, direct.FireOrigin, 0f, at); Assert.Equal(d.Target.Position.x, waiting.x, 2); Assert.Equal(d.Target.Position.z, waiting.z, 2); } - Assert.Equal(dOrigin.z + 100f / 2f, DrawAhead.Round(direct, null, direct.FireOrigin, 0f, direct.FireTime + 2.5f, 0f).z, 2); - var heldDirect = DrawAhead.Round(direct, Known(direct, ShotResult.Miss), direct.FireOrigin, 0f, direct.FireTime + 1000f / BallisticSpeed + 99f, 0f); + Assert.Equal(dOrigin.z + 100f / 2f, DrawAhead.Round(direct, null, direct.FireOrigin, 0f, direct.FireTime + 2.5f).z, 2); + var heldDirect = DrawAhead.Round(direct, Known(direct, ShotResult.Miss), direct.FireOrigin, 0f, direct.FireTime + 1000f / BallisticSpeed + 99f); Assert.Equal(dOrigin.z + 1000f, heldDirect.z, 2); } @@ -119,7 +119,7 @@ public sealed partial class FireControlCut124Tests FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); var shot = SafeAssert.OnlyShot(e.Zone); - var late = DrawAhead.Round(shot, Known(shot, ShotResult.Miss), shot.FireOrigin, 0f, shot.FireTime + 5000f, 0f); + var late = DrawAhead.Round(shot, Known(shot, ShotResult.Miss), shot.FireOrigin, 0f, shot.FireTime + 5000f); Assert.Equal(origin.x, late.x, 3); Assert.Equal(origin.z, late.z, 3); } @@ -139,7 +139,7 @@ public sealed partial class FireControlCut124Tests var after = SafeAssert.OnlyShot(e.Zone); Assert.Equal(BallisticSpeed, after.Speed, 4); var aim = normalize(float2(2, 1)); - var at = DrawAhead.Round(after, null, after.FireOrigin, 0f, after.FireTime + 1f, 0f); + var at = DrawAhead.Round(after, null, after.FireOrigin, 0f, after.FireTime + 1f); Assert.Equal(origin.x + aim.x * BallisticSpeed, at.x, 3); Assert.Equal(origin.z + aim.y * BallisticSpeed, at.z, 3); } @@ -164,7 +164,7 @@ public sealed partial class FireControlCut124Tests for (var i = 0; i < 25; i++) { var at = shot.FireTime + i * .37f; - DrawAhead.Round(shot, known, barrel, .1f, at, .01f); + DrawAhead.Round(shot, known, barrel, .1f, at); DrawAhead.RoundOver(shot, known, at); DrawAhead.RoundEnd(shot, known?.Result); DrawAhead.Impact(e.Target, cell, .01f * i); @@ -198,7 +198,7 @@ public sealed partial class FireControlCut124Tests Assert.Equal(NoLockRange, after.MaxRange, 4); Assert.Equal(endBefore, DrawAhead.RoundEnd(after, ShotResult.Miss), 4); var aim = normalize(float2(2, 1)); - var held = DrawAhead.Round(after, Known(after, ShotResult.Miss), after.FireOrigin, 0f, after.FireTime + 1000f, 0f); + var held = DrawAhead.Round(after, Known(after, ShotResult.Miss), after.FireOrigin, 0f, after.FireTime + 1000f); Assert.Equal(origin.x + aim.x * NoLockRange, held.x, 3); Assert.Equal(origin.z + aim.y * NoLockRange, held.z, 3); } @@ -269,17 +269,17 @@ public sealed partial class FireControlCut124Tests foreach (var lead in new[] { 0f, step / 2f, .999f * step }) { - var waiting = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, shot.ArrivalTime + lead, lead); + var waiting = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, shot.ArrivalTime + lead); Assert.Equal(origin.x, waiting.x, 3); Assert.Equal(origin.z + 100f, waiting.z, 3); - var missed = DrawAhead.Round(shot, Known(shot, ShotResult.Miss), shot.FireOrigin, 0f, shot.ArrivalTime + lead, lead); + var missed = DrawAhead.Round(shot, Known(shot, ShotResult.Miss), shot.FireOrigin, 0f, shot.ArrivalTime + lead); Assert.Equal(origin.x, missed.x, 3); Assert.Equal(origin.z + 100f + lead * speed, missed.z, 3); } for (var time = shot.FireTime; time <= shot.ArrivalTime + 1f; time += 1f / 144f) { - var drawn = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, time, 0f); + var drawn = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, time); Assert.True(drawn.z - origin.z <= 100f + 1e-3f, "the round was drawn past its arrival point"); } } @@ -315,7 +315,7 @@ public sealed partial class FireControlCut124Tests Assert.Equal(fired.FireTime + 1.05f, record.ArrivalTime, 2); Assert.True(record.ArrivalTime < fired.ArrivalTime); - var stop = DrawAhead.Round(record, outcome, record.FireOrigin, 0f, record.ArrivalTime, 0f); + var stop = DrawAhead.Round(record, outcome, record.FireOrigin, 0f, record.ArrivalTime); var expected = outcome.HasBurstPoint ? outcome.BurstPoint : record.FireOrigin.xz + record.TravelDirection * (20f * (record.ArrivalTime - record.FireTime)); @@ -323,7 +323,7 @@ public sealed partial class FireControlCut124Tests Assert.Equal(expected.y, stop.z, 3); foreach (var later in new[] { 0f, .5f, 5f }) { - var drawn = DrawAhead.Round(record, outcome, record.FireOrigin, 0f, record.ArrivalTime + later, 0f); + var drawn = DrawAhead.Round(record, outcome, record.FireOrigin, 0f, record.ArrivalTime + later); Assert.Equal(expected.x, drawn.x, 3); Assert.Equal(expected.y, drawn.z, 3); } @@ -350,11 +350,11 @@ public sealed partial class FireControlCut124Tests Assert.Equal(want.z, got.z, 3); } - Close(barrel, DrawAhead.Round(shot, null, barrel, blend, shot.FireTime, 0f)); - Close(Line(blend / 2f) + offset * .5f, DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + blend / 2f, 0f)); - Close(Line(blend), DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + blend, 0f)); - Close(Line(2f * blend), DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + 2f * blend, 0f)); - Close(Line(0f), DrawAhead.Round(shot, null, barrel, 0f, shot.FireTime, 0f)); + Close(barrel, DrawAhead.Round(shot, null, barrel, blend, shot.FireTime)); + Close(Line(blend / 2f) + offset * .5f, DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + blend / 2f)); + Close(Line(blend), DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + blend)); + Close(Line(2f * blend), DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + 2f * blend)); + Close(Line(0f), DrawAhead.Round(shot, null, barrel, 0f, shot.FireTime)); } // Placement depends on sim time alone: any history of frames walked to a checkpoint draws what a fresh call draws, which @@ -379,18 +379,10 @@ public sealed partial class FireControlCut124Tests while (elapsed + 1e-4f < checkpoint) { elapsed = min(checkpoint, elapsed + frames[frame++ % frames.Length]); - DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + elapsed, 0f); - } - var drawn = DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint, 0f); - var fresh = DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint, 0f); - // A frame that falls `lead` after the step draws what the sim time lead later would. - foreach (var lead in new[] { 1f / 60f, 1f / 90f }) - { - var ahead = DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint, lead); - Assert.Equal(fresh.x, ahead.x, 3); - Assert.Equal(fresh.y, ahead.y, 3); - Assert.Equal(fresh.z, ahead.z, 3); + DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + elapsed); } + var drawn = DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint); + var fresh = DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint); Assert.Equal(fresh.z, drawn.z, 3); Assert.Equal(shot.FireOrigin.z + speed * min(checkpoint, 1000f / speed), drawn.z, 3); Assert.Equal(shot.FireOrigin.x, drawn.x, 3); @@ -420,11 +412,13 @@ public sealed partial class FireControlCut124Tests e.Target.TurnRate = 3f; var cell = FarCell(e); var hit = Hit(shot, e.Target, cell, shot.CommitTime); - var arrivalOnTheLine = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, shot.ArrivalTime, 0f); + var arrivalOnTheLine = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, shot.ArrivalTime); var cellSize = e.Items.GameplaySettings.SchematicCellSize; - foreach (var lead in new[] { 0f, step / 2f, .999f * step }) + // The drawn time is the only input: the target's lead is that time less its zone's step time. + foreach (var drawnTime in new[] { shot.ArrivalTime, shot.ArrivalTime + step / 2f, shot.ArrivalTime + .5f }) { + var lead = (float) (drawnTime - e.Zone.ExactTime); var position = e.Target.Position; var direction = e.Target.Direction; e.Target.Position += float3(e.Target.Velocity.x, 0, e.Target.Velocity.y) * lead; @@ -439,13 +433,10 @@ public sealed partial class FireControlCut124Tests Assert.Equal(expected.x, impact.x, 3); Assert.Equal(expected.y, impact.y, 3); Assert.Equal(expected.z, impact.z, 3); - foreach (var simTime in new[] { shot.ArrivalTime, shot.ArrivalTime + .5f }) - { - var drawn = DrawAhead.Round(shot, hit, shot.FireOrigin, .1f, simTime + lead, lead); - Assert.Equal(expected.x, drawn.x, 3); - Assert.Equal(expected.y, drawn.y, 3); - Assert.Equal(expected.z, drawn.z, 3); - } + var drawn = DrawAhead.Round(shot, hit, shot.FireOrigin, .1f, drawnTime); + Assert.Equal(expected.x, drawn.x, 3); + Assert.Equal(expected.y, drawn.y, 3); + Assert.Equal(expected.z, drawn.z, 3); } } @@ -464,26 +455,21 @@ public sealed partial class FireControlCut124Tests foreach (var t in new[] { commit - 1f, commit }) { - var steered = DrawAhead.Round(shot, hit, shot.FireOrigin, 0f, t, 0f); - var line = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, t, 0f); + var steered = DrawAhead.Round(shot, hit, shot.FireOrigin, 0f, t); + var line = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, t); Assert.Equal(line.x, steered.x, 3); Assert.Equal(line.y, steered.y, 3); Assert.Equal(line.z, steered.z, 3); } var m = commit + 1f; - var onTheLine = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, m, 0f); - var end = DrawAhead.Impact(e.Target, cell, 0f); + var onTheLine = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, m); + var end = DrawAhead.Impact(e.Target, cell, (float) (m - e.Zone.ExactTime)); Assert.True(length((end - onTheLine).xz) > 1f, "the fixture put the impact cell on the line"); - var halfway = DrawAhead.Round(shot, hit, shot.FireOrigin, 0f, m, 0f); + var halfway = DrawAhead.Round(shot, hit, shot.FireOrigin, 0f, m); Assert.Equal(onTheLine.x + (end.x - onTheLine.x) * .5f, halfway.x, 3); Assert.Equal(onTheLine.y + (end.y - onTheLine.y) * .5f, halfway.y, 3); Assert.Equal(onTheLine.z + (end.z - onTheLine.z) * .5f, halfway.z, 3); - - var ahead = DrawAhead.Round(shot, hit, shot.FireOrigin, 0f, m, .02f); - Assert.Equal(halfway.x, ahead.x, 3); - Assert.Equal(halfway.y, ahead.y, 3); - Assert.Equal(halfway.z, ahead.z, 3); } // The sim states facts and draws nothing: no drawing function, and no lead, blend or barrel, lives in FireControl; @@ -526,8 +512,8 @@ public sealed partial class FireControlCut124Tests Assert.Equal(origin.x, outcome.BurstPoint.x, 3); Assert.Equal(origin.z + NoLockRange, outcome.BurstPoint.y, 3); - Assert.Equal(origin.z + 50f, DrawAhead.Round(record, null, record.FireOrigin, 0f, record.ArrivalTime, 0f).z, 3); - var stop = DrawAhead.Round(record, outcome, record.FireOrigin, 0f, record.ArrivalTime, 0f); + Assert.Equal(origin.z + 50f, DrawAhead.Round(record, null, record.FireOrigin, 0f, record.ArrivalTime).z, 3); + var stop = DrawAhead.Round(record, outcome, record.FireOrigin, 0f, record.ArrivalTime); Assert.Equal(outcome.BurstPoint.x, stop.x, 3); Assert.Equal(outcome.BurstPoint.y, stop.z, 3); } diff --git a/tests/Aetheria.Shared.Tests/DrawnPoseTests.cs b/tests/Aetheria.Shared.Tests/DrawnPoseTests.cs index 0f9133130..e30f2869b 100644 --- a/tests/Aetheria.Shared.Tests/DrawnPoseTests.cs +++ b/tests/Aetheria.Shared.Tests/DrawnPoseTests.cs @@ -394,6 +394,23 @@ public sealed class DrawnPoseTests : IDisposable } } + // An orbit whose parent the zone does not hold is reported when a rail is evaluated at a drawn time. + [Fact] + public void AMissingParentOrbitIsLoggedAtADrawnTime() + { + var w = Build(); + Step(w, 1f / 60f, 30); + var log = new List(); + w.Zone.Log = log.Add; + w.Zone.Orbits.Remove(w.PlanetOrbit); + foreach (var lead in new[] { .004f, .5f }) + { + log.Clear(); + w.Zone.OrbitPositionAt(w.MoonOrbit, w.Zone.ExactTime + lead); + Assert.Contains("Requested orbit is not part of this zone!", log); + } + } + private static void AssertNear(float2 expected, float2 actual, float eps, string what) => Assert.True(length(expected - actual) <= eps, $"{what}: expected {expected}, got {actual} (eps {eps})"); }