diff --git a/Assets/Scripts/ServerShared/Behaviors/Sensor.cs b/Assets/Scripts/ServerShared/Behaviors/Sensor.cs index 9b83c0330..dc384f70f 100644 --- a/Assets/Scripts/ServerShared/Behaviors/Sensor.cs +++ b/Assets/Scripts/ServerShared/Behaviors/Sensor.cs @@ -24,8 +24,7 @@ public class SensorData : BehaviorData [Inspectable, JsonProperty("pingEnergy"), Key(6), RuntimeInspectable] public PerformanceStat PingEnergy; - [Inspectable, JsonProperty("pingVisibility"), Key(7), RuntimeInspectable] - public PerformanceStat PingVisibility; + // Key 7 (pingVisibility) is retired: never reuse it. [Inspectable, JsonProperty("pingRange"), Key(8), RuntimeInspectable] public PerformanceStat PingRange; @@ -137,7 +136,6 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer // nothing would ever fill this capacitor; bypass it rather than starving such an instance forever. if(_pingCooldown < 0 && (Item == null || _capacitor.TrySpend(_capacitor.Capacity))) { - Entity.SetSignature(this, SignatureChannel.Emission, Evaluate(_data.PingVisibility), Item); // interim: emissions-heard replaces this with a range-gated emission _pinging = true; _pingZone = Entity.Zone; _pingCooldown = 1; diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index 6618bd9bb..c63ad9874 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -93,9 +93,14 @@ public abstract class Entity public IReadOnlyDictionary VisibilitySources => _visibilityView ??= new ReadOnlyDictionary(_visibilitySources); // The one writer of VisibilitySources (the decay loop in Update is the one decayer). Every light signature - // names its channel and the item that produced it (null for the hull). - public void SetSignature(object source, SignatureChannel channel, float strength, EquippedItem item) => + // names its channel and the item that produced it (null for the hull). Emission is active emission (paints now, + // Entity.Emit after emissions-heard), never light: ruling visibility-per-capability-reveal, map R1. + public void SetSignature(object source, SignatureChannel channel, float strength, EquippedItem item) + { + if (channel == SignatureChannel.Emission) + throw new ArgumentException("Emission is active emission, never a light signature.", nameof(channel)); _visibilitySources[source] = new Signature(channel, strength, item); + } public readonly ReactiveDictionary EntityInfoGathered = new ReactiveDictionary(); public (List weapons, List items)[] WeaponGroups; @@ -169,8 +174,6 @@ public abstract class Entity public int AssignedPopulation => PopulationAssignments.Sum(pa => pa.AssignedPopulation); public float Mass { get; private set; } public float Visibility => VisibilitySources.Values.Sum(s => s.Strength); - public float SignatureOn(SignatureChannel channel) => - VisibilitySources.Values.Where(s => s.Channel == channel).Sum(s => s.Strength); public Subject IncomingHit = new Subject(); public Subject<(int2 pos, float damage)> ArmorDamage = new Subject<(int2, float)>(); @@ -952,7 +955,7 @@ public abstract class Entity // What this observer's instruments receive of other ships' acts this tick (Zone.Update calls it once, after every // entity has updated): every light signature of an item that would alone settle this observer's passive info above // TargetDetectionInfoThreshold is revealed (ruling visibility-per-capability-reveal). Hull radiation names no - // item and only feeds the track; an Emission entry is never light. One PassiveInfo per ship. Zone.Update calls it + // item and only feeds the track. One PassiveInfo per ship. Zone.Update calls it // for every entity after all of them have updated, so what a ship shows is what every observer receives on that // tick whatever order the zone lists them in. internal void Sense() @@ -967,7 +970,7 @@ public abstract class Entity var perSignature = -1f; foreach (var signature in other._visibilitySources.Values) { - if (signature.Item == null || signature.Channel == SignatureChannel.Emission) continue; + if (signature.Item == null) continue; if (perSignature < 0f) perSignature = PassiveInfo(other.Position.xz, 1f); if (signature.Strength * perSignature > threshold) RecordReveal(other, signature.Item, signature.Channel); } diff --git a/Assets/Scripts/ServerShared/Sensing.cs b/Assets/Scripts/ServerShared/Sensing.cs index 129513dda..c1f0932d9 100644 --- a/Assets/Scripts/ServerShared/Sensing.cs +++ b/Assets/Scripts/ServerShared/Sensing.cs @@ -94,7 +94,7 @@ public readonly struct Reveal } // A subject's removal from a zone as the zone recorded it (death, dock, warp, any removal): where it was and how -// bright its light was (its signature outside the Emission channel). Judged by each observer's Sense under the +// bright its light was (its Visibility at removal). Judged by each observer's Sense under the // light-act gate; notification only, one tick long, it decides nothing. public readonly struct Departure { diff --git a/Assets/Scripts/ServerShared/Zone.cs b/Assets/Scripts/ServerShared/Zone.cs index 97554a9ad..df73a9414 100644 --- a/Assets/Scripts/ServerShared/Zone.cs +++ b/Assets/Scripts/ServerShared/Zone.cs @@ -114,7 +114,7 @@ public class Zone Entities.ObserveAdd().Subscribe(add => add.Value.Death.Subscribe(_ => { Entities.Remove(add.Value); add.Value.Deactivate(); })); // The one record of a removal for observers (ruling reveal-ends-on-perception): read by the next Sense pass. Entities.ObserveRemove().Subscribe(remove => _departures.Add(new Departure(remove.Value, remove.Value.Position.xz, - remove.Value.Visibility - remove.Value.SignatureOn(SignatureChannel.Emission)))); + remove.Value.Visibility))); Targets.Add(new EntityTargets()); diff --git a/tests/Aetheria.Shared.Tests/ContactRevealTests.cs b/tests/Aetheria.Shared.Tests/ContactRevealTests.cs index a1aabcfd6..e5464d325 100644 --- a/tests/Aetheria.Shared.Tests/ContactRevealTests.cs +++ b/tests/Aetheria.Shared.Tests/ContactRevealTests.cs @@ -147,7 +147,7 @@ public sealed class ContactRevealTests : IDisposable { Sensitivity = Const(sensitivity), SensitivityCurve = new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 1, 0, 0) } }, // the same gain at every bearing - PingBoost = Const(.0001f), PingEnergy = Const(0), PingVisibility = Const(5), PingRange = Const(300), PingCooldown = Const(4) + PingBoost = Const(.0001f), PingEnergy = Const(0), PingRange = Const(300), PingCooldown = Const(4) }; private static GearData Eye(string name, float sensitivity) => new GearData @@ -378,18 +378,34 @@ public sealed class ContactRevealTests : IDisposable Assert.Empty(o.RevealsOf(e)); } - // An Emission entry is never light (R1): a bright, near one reveals nothing through Sense. Emission reaches the - // book only through a published paint. + // A ping is the pinger's act on its beam, not light (R1): it adds nothing to the pinger's Visibility, so it raises + // no observer's track of the pinger. The control run is the same scene without the Ping() call. [Fact] - public void AnEmissionEntryIsNotLight() + public void APingNoLongerBrightensThePinger() { - var e = Add(float3(0, 0, 20), new[] { "Eye" }); - var o = Add(float3(0, 0, 0), new[] { "Eye" }); - e.SetSignature(new object(), SignatureChannel.Emission, 1000f, e.Equipment.Single(i => i.Behaviors.Any(b => b is Sensor))); + var (pingedLight, pingedTrack) = RunPinger(ping: true); + var (controlLight, controlTrack) = RunPinger(ping: false); - Tick(.01f); + Assert.Equal(controlLight, pingedLight); + Assert.True(controlLight.Max() > 0f, "fixture: the pinger's hull is lit"); + Assert.Equal(controlTrack, pingedTrack, 6); + } - Assert.Empty(o.RevealsOf(e)); + // P is past the ping's radius from O; its Visibility is read on the call's own tick and on each tick after. + private static (float[] visibility, float track) RunPinger(bool ping) + { + using var t = new ContactRevealTests(); + var p = t.Add(float3(0, 0, 1000), new[] { "Eye" }); + var o = t.Add(float3(0, 0, 0), new[] { "Eye" }); + var sensor = p.Equipment.SelectMany(i => i.Behaviors).OfType().Single(); + if (ping) sensor.Ping(); + var light = new List { p.Visibility }; + for (var tick = 0; tick < 10; tick++) + { + t.Tick(.1f); + light.Add(p.Visibility); + } + return (light.ToArray(), Track(o, p)); } // Two items revealed at different times: the contact's bearing, while untracked, is the freshest reveal's. @@ -666,23 +682,6 @@ public sealed class ContactRevealTests : IDisposable Assert.Single(o.RevealsOf(e)); } - // A signature on the Emission channel (paint strength, a ping) is not light: it does not make a departure seen. - [Fact] - public void AnEmissionSignatureDoesNotMakeADepartureSeen() - { - var e = Add(float3(0, 0, 100), new[] { "Gun" }); - var o = Add(float3(0, 0, 0), new[] { "Eye" }); - Fire(e, Guns(e)[0]); - Tick(.01f); - ShowOnly(e, 0f, Guns(e)[0]); - e.SetSignature(new object(), SignatureChannel.Emission, 1000f, Guns(e)[0]); - - _zone.Entities.Remove(e); - Tick(.1f); - - Assert.Single(o.RevealsOf(e)); - } - // A ship removed and put back before anyone senses has not left: its reveals stand, and its next act refreshes them. [Fact] public void AReAdmittedSubjectHasNotDeparted() diff --git a/tests/Aetheria.Shared.Tests/MiningCut3Tests.cs b/tests/Aetheria.Shared.Tests/MiningCut3Tests.cs index ea29e418b..c3bad4824 100644 --- a/tests/Aetheria.Shared.Tests/MiningCut3Tests.cs +++ b/tests/Aetheria.Shared.Tests/MiningCut3Tests.cs @@ -119,7 +119,6 @@ public sealed partial class MiningCut3Tests : IDisposable SensitivityCurve = Falloff(), PingBoost = Constant(.02f), PingEnergy = Constant(0f), - PingVisibility = Constant(5f), PingRange = Constant(300f), PingCooldown = Constant(4f) }); @@ -132,7 +131,6 @@ public sealed partial class MiningCut3Tests : IDisposable SensitivityCurve = Falloff(), PingBoost = Constant(.02f), PingEnergy = Constant(0f), - PingVisibility = Constant(5f), PingRange = Constant(300f), PingCooldown = Constant(4f) }); @@ -1032,7 +1030,7 @@ public sealed partial class MiningCut3Tests : IDisposable var eye = Gear("Eye", new SensorData { Sensitivity = Constant(sensitivity), SensitivityCurve = Falloff(), PingBoost = Constant(boost), - PingEnergy = Constant(0f), PingVisibility = Constant(5f), PingRange = Constant(300f), PingCooldown = Constant(4f) + PingEnergy = Constant(0f), PingRange = Constant(300f), PingCooldown = Constant(4f) }); cache.Upsert(eye); var items = new ItemManager(cache, new ProvenanceLedger(), GameSettings(), _ => { }); diff --git a/tests/Aetheria.Shared.Tests/MiningIndexTests.cs b/tests/Aetheria.Shared.Tests/MiningIndexTests.cs index 785854911..fe36125bc 100644 --- a/tests/Aetheria.Shared.Tests/MiningIndexTests.cs +++ b/tests/Aetheria.Shared.Tests/MiningIndexTests.cs @@ -44,7 +44,6 @@ public sealed partial class MiningCut3Tests SensitivityCurve = curve, PingBoost = Constant(.02f), PingEnergy = Constant(0f), - PingVisibility = Constant(5f), PingRange = Constant(300f), PingCooldown = Constant(4f) }); diff --git a/tests/Aetheria.Shared.Tests/PaintModeTests.cs b/tests/Aetheria.Shared.Tests/PaintModeTests.cs index 2928e2907..403917243 100644 --- a/tests/Aetheria.Shared.Tests/PaintModeTests.cs +++ b/tests/Aetheria.Shared.Tests/PaintModeTests.cs @@ -99,7 +99,7 @@ public sealed class PaintModeTests : IDisposable { Sensitivity = Const(sensitivity), SensitivityCurve = new BezierCurve { Keys = new[] { float4(0, 0, 0, 0), float4(1, 0, 0, 0) } }, // no passive gain - PingBoost = Const(pingBoost), PingEnergy = Const(0), PingVisibility = Const(5), PingRange = Const(300), + PingBoost = Const(pingBoost), PingEnergy = Const(0), PingRange = Const(300), PingCooldown = Const(4), BeamWidth = beamWidth.HasValue ? Const(beamWidth.Value) : null } @@ -259,7 +259,6 @@ public sealed class PaintModeTests : IDisposable var e = NewShip(float3(-30, 0, 130), "Ear", a.Faction); // same faction but hostile to A e.SetIff(a, true); Settle(); - var cEmission = c.SignatureOn(SignatureChannel.Emission); var (held, _) = SweepAndWatch(a, .1f); @@ -268,7 +267,6 @@ public sealed class PaintModeTests : IDisposable Assert.Equal(.21213f, Track(c, t), 4); Assert.Equal(.1f, Track(a, t), 4); // Nothing of C is emitted or painted: T's only paint is A's sweep. - Assert.Equal(cEmission, c.SignatureOn(SignatureChannel.Emission)); var paint = Assert.Single(held[t].SelectMany(h => h)); Assert.Same(GearOf(a).Data, paint.EmitterClass); // A hostile or foreign listener hears nothing of A's ping. diff --git a/tests/Aetheria.Shared.Tests/SignatureTests.cs b/tests/Aetheria.Shared.Tests/SignatureTests.cs index c9b7efb8a..1fb259231 100644 --- a/tests/Aetheria.Shared.Tests/SignatureTests.cs +++ b/tests/Aetheria.Shared.Tests/SignatureTests.cs @@ -324,29 +324,28 @@ public sealed class SignatureTests : IDisposable Assert.Null(s.Item); } - [Theory] - [InlineData(5f)] - [InlineData(9f)] - public void APingShowsAnEmissionWithItsItem(float pingVisibility) + // Emission is active emission, never light: SetSignature refuses it before writing, and a Visibility gear + // authored on it throws on the Zone.Update that first executes it. + [Fact] + public void SetSignatureRefusesEmission() { - var r = Build(Armament.None, 0f, null, new SensorData - { - Sensitivity = Constant(3f), PingBoost = Constant(.02f), PingEnergy = Constant(0f), PingVisibility = Constant(pingVisibility), - PingRange = Constant(300f), PingCooldown = Constant(4f) - }); - var sensor = Single(r.Ship); - sensor.Ping(); - var s = Of(r.Ship, sensor); - Assert.Equal(SignatureChannel.Emission, s.Channel); - Assert.Same(sensor.Item, s.Item); - Assert.Equal(pingVisibility, s.Strength, 3); + var r = Build(Armament.Gun, 0f, null); + Fire(r); + var before = r.Ship.VisibilitySources.ToDictionary(kv => kv.Key, kv => kv.Value); + Assert.NotEmpty(before); + + Assert.Throws(() => r.Ship.SetSignature(new object(), SignatureChannel.Emission, 5f, r.WeaponItem)); + + Assert.Equal(before.Count, r.Ship.VisibilitySources.Count); + foreach (var kv in before) Assert.Equal(kv.Value, r.Ship.VisibilitySources[kv.Key]); + Assert.Throws(() => Build(Armament.None, 0f, null, + new VisibilityData { Visibility = Constant(2), VisibilityDecay = Constant(0), Channel = SignatureChannel.Emission })); } // A catalog-authored Visibility behaviour travels on the channel its data names, thermal unless it says more. [Theory] [InlineData(SignatureChannel.Thermal)] [InlineData(SignatureChannel.DrivePlume)] - [InlineData(SignatureChannel.Emission)] public void AVisibilityBehaviourTravelsOnItsAuthoredChannel(SignatureChannel channel) { var r = Build(Armament.None, 0f, null, new VisibilityData { Visibility = Constant(7), VisibilityDecay = Constant(0), Channel = channel }); @@ -366,9 +365,9 @@ public sealed class SignatureTests : IDisposable // ---- VisibilityIsStillTheSum ---- // Every channel lit at once: a fired gun (100 on LaunchFlash), Visibility gear on Thermal (7), Reflection (4), - // DrivePlume (3) and Emission (2), a reflector under no sun (0) and the non-radiating hull (0). Visibility is the - // sum of the strengths, 116, the value the single-number build gave on this fixture; SignatureOn splits the - // same total by channel. + // DrivePlume (3) and a second Reflection (2), a reflector under no sun (0) and the non-radiating hull (0). + // Visibility is the sum of the strengths, 116, the value the single-number build gave on this fixture; the same + // total splits by channel over VisibilitySources, and none of it is Emission. [Fact] public void VisibilityIsStillTheSum() { @@ -376,18 +375,19 @@ public sealed class SignatureTests : IDisposable new VisibilityData { Visibility = Constant(7), VisibilityDecay = Constant(0) }, new VisibilityData { Visibility = Constant(4), VisibilityDecay = Constant(0), Channel = SignatureChannel.Reflection }, new VisibilityData { Visibility = Constant(3), VisibilityDecay = Constant(0), Channel = SignatureChannel.DrivePlume }, - new VisibilityData { Visibility = Constant(2), VisibilityDecay = Constant(0), Channel = SignatureChannel.Emission }, + new VisibilityData { Visibility = Constant(2), VisibilityDecay = Constant(0), Channel = SignatureChannel.Reflection }, new ReflectorData { CrossSection = Constant(3) }); Fire(r); var ship = r.Ship; Assert.Equal(116f, ship.Visibility, 2); Assert.Equal(ship.VisibilitySources.Values.Sum(s => s.Strength), ship.Visibility, 3); - Assert.Equal(100f, ship.SignatureOn(SignatureChannel.LaunchFlash), 2); - Assert.Equal(7f, ship.SignatureOn(SignatureChannel.Thermal), 2); - Assert.Equal(4f, ship.SignatureOn(SignatureChannel.Reflection), 2); - Assert.Equal(2f, ship.SignatureOn(SignatureChannel.Emission), 2); - Assert.Equal(3f, ship.SignatureOn(SignatureChannel.DrivePlume), 2); + float On(SignatureChannel channel) => ship.VisibilitySources.Values.Where(s => s.Channel == channel).Sum(s => s.Strength); + Assert.Equal(100f, On(SignatureChannel.LaunchFlash), 2); + Assert.Equal(7f, On(SignatureChannel.Thermal), 2); + Assert.Equal(6f, On(SignatureChannel.Reflection), 2); + Assert.Equal(0f, On(SignatureChannel.Emission), 2); + Assert.Equal(3f, On(SignatureChannel.DrivePlume), 2); } // ---- DecayKeepsChannelAndItem ---- @@ -412,28 +412,24 @@ public sealed class SignatureTests : IDisposable Assert.False(r.Ship.VisibilitySources.ContainsKey(r.Weapon), "a signature below 0.1 is gone"); } - // Decay rebuilds every entry from its own. A gun, a drive that has stopped burning and a sensor that has pinged - // each wrote once and are never rewritten, so after a long tick the only thing touching them is decay; each - // still names the item of the source that wrote it, and the three items differ. + // Decay rebuilds every entry from its own. A gun and a drive that has stopped burning wrote once and are never + // rewritten, so after a long tick the only thing touching them is decay; with a Visibility gear (which rewrites + // its light each tick) each still names the item of the source that wrote it, and the three items differ. [Fact] public void DecayKeepsEachSourcesOwnItem() { - var r = Build(Armament.Gun, .5f, Drive(10), new SensorData - { - Sensitivity = Constant(3f), PingBoost = Constant(.02f), PingEnergy = Constant(0f), PingVisibility = Constant(5f), - PingRange = Constant(300f), PingCooldown = Constant(4f) - }); + var r = Build(Armament.Gun, .5f, Drive(10), + new VisibilityData { Visibility = Constant(5f), VisibilityDecay = Constant(0), Channel = SignatureChannel.Reflection }); var thruster = Single(r.Ship); - var sensor = Single(r.Ship); + var sensor = Single(r.Ship); thruster.Item.UpdatePerformance(); Burn(r, 1f); Burn(r, 0f); // the drive stops: its entry is no longer rewritten - sensor.Ping(); Fire(r); var sources = new Behavior[] { thruster, sensor, r.Weapon }; // What each writer authors, named here and not read back from the fixture's own ticks. - var writes = new[] { SignatureChannel.DrivePlume, SignatureChannel.Emission, SignatureChannel.LaunchFlash }; + var writes = new[] { SignatureChannel.DrivePlume, SignatureChannel.Reflection, SignatureChannel.LaunchFlash }; var before = sources.Select(b => Of(r.Ship, b)).ToArray(); Assert.Equal(3, before.Select(e => e.Item).Distinct().Count()); Assert.All(before, e => Assert.True(e.Strength > 1f, "fixture: each source has a live entry")); @@ -442,7 +438,7 @@ public sealed class SignatureTests : IDisposable for (var i = 0; i < sources.Length; i++) { var after = Of(r.Ship, sources[i]); - Assert.True(after.Strength < before[i].Strength, "fixture: decay touched the entry"); + if (sources[i] != sensor) Assert.True(after.Strength < before[i].Strength, "fixture: decay touched the entry"); Assert.Same(sources[i].Item, after.Item); Assert.Equal(writes[i], after.Channel); } diff --git a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs index 58ede257f..e35b311b4 100644 --- a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs +++ b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs @@ -85,7 +85,7 @@ public sealed class VapourCloudTests : IDisposable { Sensitivity = Constant(1f), SensitivityCurve = new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 1, 0, 0) } }, - PingBoost = Constant(.000004f), PingEnergy = Constant(0f), PingVisibility = Constant(5f), + PingBoost = Constant(.000004f), PingEnergy = Constant(0f), PingRange = Constant(300f), PingCooldown = Constant(4f) })); cache.Upsert(Gear("Vent", HardpointType.Tool, new VapourDumpData diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index b24161383..333149a6c 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -197,7 +197,7 @@ public static class Program { ["Sensitivity"] = "receiver", ["PingBoost"] = "emitter", ["PingEnergy"] = "emitter", ["PingRange"] = "emitter", - ["PingVisibility"] = "emitter", ["PingCooldown"] = "emitter", + ["PingCooldown"] = "emitter", }; private static readonly Dictionary ThrusterRoles = new Dictionary