Sensor: the pinger's swept set decides each crossing once; allies credit a published ping return inside the bistatic oval

Deletes the stateless interval crossing (_previousRadius, PreviousRadius, Crossing) and the allied-pinger scan over
other ships' Sensors. The pinging Sensor remembers the subjects its wavefront has reached (_reached), in the beam
or not, and a subject's first sweeping tick inside the radius is its one crossing. On it the pinger paints, credits
itself and posts a PingReturn to each ally; Entity publishes returns with paints (PublishPaints is now
PublishInbox) and the ally's own sensors credit them for one tick at the geometric mean range, inside
Rt x Rr < PingRange^2, through the geometric mean of both legs' obscuration (ruling ally-echo-bistatic-oval).
This commit builds and replaces the sensor the previous commit's tests were written against.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-11 15:22:29 +02:00
commit dd896528cc
4 changed files with 71 additions and 36 deletions

View file

@ -187,11 +187,16 @@ public abstract class Entity
public ReactiveProperty<int> TargetedByCount = new ReactiveProperty<int>(0);
// TargetedBy counts selection, which emits nothing; a warning comes only from a paint. A search paint is a ping
// sweeping this ship (Sensor.Execute); a track paint is a lock (LockWeapon.Execute). Each reports into the
// inbox the tick it paints; PublishPaints publishes the inbox once, so a warning lives exactly one tick after
// inbox the tick it paints; PublishInbox publishes the inbox once, so a warning lives exactly one tick after
// its last paint.
private readonly List<PaintWarning> _paintInbox = new List<PaintWarning>();
private readonly List<PaintWarning> _incomingPaints = new List<PaintWarning>();
public IReadOnlyList<PaintWarning> IncomingPaints => _incomingPaints;
// An ally's sensor posts a return on the tick its ping crosses a subject; published with paints, read by this
// ship's own sensors for one tick.
private readonly List<PingReturn> _returnInbox = new List<PingReturn>();
private readonly List<PingReturn> _pingReturns = new List<PingReturn>();
internal IReadOnlyList<PingReturn> PingReturns => _pingReturns;
public ReactiveProperty<SecurityLevel> CurrentSecurityLevel = new ReactiveProperty<SecurityLevel>(SecurityLevel.Open);
public ReadOnlyReactiveProperty<bool> PresencePermitted;
@ -804,16 +809,27 @@ public abstract class Entity
VisibleFriendlies.Clear();
_paintInbox.Clear();
_incomingPaints.Clear();
_returnInbox.Clear();
_pingReturns.Clear();
}
// Zone.Update runs this on every entity before any entity updates, so a paint made during tick N is published
// at the start of tick N+1 whatever order the emitter and this ship update in; a paint that stops is gone
// one tick later for every ordering.
internal void PublishPaints()
internal void PublishInbox()
{
_incomingPaints.Clear();
_incomingPaints.AddRange(_paintInbox);
_paintInbox.Clear();
_pingReturns.Clear();
_pingReturns.AddRange(_returnInbox);
_returnInbox.Clear();
}
internal void ReceiveReturn(PingReturn pingReturn)
{
if (!_active) return;
_returnInbox.Add(pingReturn);
}
// The callers are Sensor.Execute (a Search paint, Strength 0) and LockWeapon.Execute (a Track paint, Strength