From aeb979f8d5cea1978ff8be6da19523a3133dce53 Mon Sep 17 00:00:00 2001 From: Luis Gonzalez Date: Wed, 15 Jul 2026 08:51:24 -0700 Subject: [PATCH] =?UTF-8?q?LANTERN=20P1=20step=208:=20light-as-territory?= =?UTF-8?q?=20relevancy=20prototype=20(=E2=98=85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per connection, marks every ENEMY ghost OUTSIDE the player's lamp radius IRRELEVANT (monsters in the dark aren't replicated — the gamma test). Modelled on the proven RegionRelevancySystem: SetIsIrrelevant so players/global ghosts stay relevant for free. Shared-set discipline — RegionRelevancySystem is the SOLE clearer (runs every tick); LightRelevancySystem is [UpdateAfter] it and only ADDs light hides via TryAdd (no clear; defensive against a region+light overlap dup). Gated on GymTag (LANTERN-only; legacy region relevancy untouched). Pure decision in LightRelevancyMath (+3 EditMode tests, 500 green). Per-player dim/bright (variable radius) + the L3 gamma-test verification are follow-ups; the ★adversarial netcode review is recommended before this ships. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Server/World/LightRelevancySystem.cs | 74 +++++++++++++++++++ .../Server/World/LightRelevancySystem.cs.meta | 2 + .../Simulation/World/LightRelevancyMath.cs | 21 ++++++ .../World/LightRelevancyMath.cs.meta | 2 + .../Tests/EditMode/LightRelevancyMathTests.cs | 36 +++++++++ .../EditMode/LightRelevancyMathTests.cs.meta | 2 + 6 files changed, 137 insertions(+) create mode 100644 Assets/_Project/Scripts/Server/World/LightRelevancySystem.cs create mode 100644 Assets/_Project/Scripts/Server/World/LightRelevancySystem.cs.meta create mode 100644 Assets/_Project/Scripts/Simulation/World/LightRelevancyMath.cs create mode 100644 Assets/_Project/Scripts/Simulation/World/LightRelevancyMath.cs.meta create mode 100644 Assets/_Project/Tests/EditMode/LightRelevancyMathTests.cs create mode 100644 Assets/_Project/Tests/EditMode/LightRelevancyMathTests.cs.meta diff --git a/Assets/_Project/Scripts/Server/World/LightRelevancySystem.cs b/Assets/_Project/Scripts/Server/World/LightRelevancySystem.cs new file mode 100644 index 000000000..09941107c --- /dev/null +++ b/Assets/_Project/Scripts/Server/World/LightRelevancySystem.cs @@ -0,0 +1,74 @@ +using ProjectM.Simulation; +using Unity.Burst; +using Unity.Collections; +using Unity.Entities; +using Unity.Mathematics; +using Unity.NetCode; +using Unity.Transforms; + +namespace ProjectM.Server +{ + /// + /// LANTERN "light is territory" relevancy prototype (Build Spec Phase 1 step 8): per connection, marks every + /// ENEMY ghost OUTSIDE that player's lamp radius IRRELEVANT — so monsters in the dark aren't replicated (the + /// gamma test: "nothing to see"). Players + untagged/global ghosts stay relevant for free (SetIsIrrelevant). + /// + /// SHARED-SET DISCIPLINE: is the SOLE clearer of the GhostRelevancy + /// set — its OnUpdate runs every tick (requires only GhostRelevancy), setting the mode + clearing + + /// adding its region hides. This system runs [UpdateAfter(RegionRelevancySystem)] and only ADDS light + /// hides (never clears) via TryAdd (defensive against a ghost hidden by BOTH region and light for one + /// connection — a duplicate Add would throw). Gated on so it runs ONLY in the + /// LANTERN gym; the legacy base/expedition game keeps region relevancy alone. Runs in + /// before GhostSendSystem reads the set. Per-player dim/bright (a + /// variable radius) is the tuning follow-up; the prototype uses . + /// + /// + [BurstCompile] + [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] + [UpdateInGroup(typeof(GhostSimulationSystemGroup))] + [UpdateAfter(typeof(RegionRelevancySystem))] + public partial struct LightRelevancySystem : ISystem + { + [BurstCompile] + public void OnCreate(ref SystemState state) + { + state.RequireForUpdate(); + state.RequireForUpdate(); // prototype: light-as-territory runs only in the LANTERN gym + } + + [BurstCompile] + public void OnUpdate(ref SystemState state) + { + // Each in-game connection's player position (per-player lamp radius = the dim/bright follow-up; fixed here). + var connPos = new NativeHashMap(8, Allocator.Temp); + foreach (var (owner, lt) in + SystemAPI.Query, RefRO>().WithAll()) + connPos[owner.ValueRO.NetworkId] = lt.ValueRO.Position; + + if (connPos.IsEmpty) { connPos.Dispose(); return; } + + // The set is already mode=SetIsIrrelevant + cleared + region-populated THIS tick by RegionRelevancySystem. + ref var relevancy = ref SystemAPI.GetSingletonRW().ValueRW; + var set = relevancy.GhostRelevancySet; + var conns = connPos.GetKeyValueArrays(Allocator.Temp); + + float rSq = LightRelevancyMath.LampRadiusDefault * LightRelevancyMath.LampRadiusDefault; + foreach (var (ghost, lt) in + SystemAPI.Query, RefRO>().WithAll()) + { + int ghostId = ghost.ValueRO.ghostId; + if (ghostId == 0) continue; // ghost id not assigned yet this tick + + float3 gp = lt.ValueRO.Position; + for (int i = 0; i < conns.Keys.Length; i++) + { + if (LightRelevancyMath.IsHidden(conns.Values[i], gp, rSq)) + set.TryAdd(new RelevantGhostForConnection { Connection = conns.Keys[i], Ghost = ghostId }, 1); + } + } + + conns.Dispose(); + connPos.Dispose(); + } + } +} diff --git a/Assets/_Project/Scripts/Server/World/LightRelevancySystem.cs.meta b/Assets/_Project/Scripts/Server/World/LightRelevancySystem.cs.meta new file mode 100644 index 000000000..fb5ed0b7b --- /dev/null +++ b/Assets/_Project/Scripts/Server/World/LightRelevancySystem.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e7c6c7ce249ae9b44821e998e0331a52 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Simulation/World/LightRelevancyMath.cs b/Assets/_Project/Scripts/Simulation/World/LightRelevancyMath.cs new file mode 100644 index 000000000..f3cf28189 --- /dev/null +++ b/Assets/_Project/Scripts/Simulation/World/LightRelevancyMath.cs @@ -0,0 +1,21 @@ +using Unity.Mathematics; + +namespace ProjectM.Simulation +{ + /// + /// Pure helpers for the LANTERN "light is territory" relevancy prototype (Build Spec Phase 1 step 8): an entity + /// outside a player's lamp radius isn't replicated to that player. The decision is a planar (XZ) distance test + /// so it's Burst-safe + unit-testable independent of the netcode GhostRelevancy set manipulation. + /// + public static class LightRelevancyMath + { + /// Prototype lamp radius (world units). Per-player dim/bright (a variable radius = the quiet/blind + /// trade) is the tuning follow-up; the prototype uses one fixed radius to prove the gamma test. + public const float LampRadiusDefault = 25f; + + /// True when the ghost at is OUTSIDE the lamp centred on + /// (planar XZ distance beyond the radius) — i.e. hidden from that player. + public static bool IsHidden(float3 playerPos, float3 ghostPos, float radiusSq) + => math.distancesq(playerPos.xz, ghostPos.xz) > radiusSq; + } +} diff --git a/Assets/_Project/Scripts/Simulation/World/LightRelevancyMath.cs.meta b/Assets/_Project/Scripts/Simulation/World/LightRelevancyMath.cs.meta new file mode 100644 index 000000000..e3ab41f1b --- /dev/null +++ b/Assets/_Project/Scripts/Simulation/World/LightRelevancyMath.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e2224a38c42888d408d54961131dfff3 \ No newline at end of file diff --git a/Assets/_Project/Tests/EditMode/LightRelevancyMathTests.cs b/Assets/_Project/Tests/EditMode/LightRelevancyMathTests.cs new file mode 100644 index 000000000..2a1c3bdf5 --- /dev/null +++ b/Assets/_Project/Tests/EditMode/LightRelevancyMathTests.cs @@ -0,0 +1,36 @@ +using NUnit.Framework; +using ProjectM.Simulation; +using Unity.Mathematics; + +namespace ProjectM.Tests +{ + /// + /// Unit tests for the LANTERN light-as-territory relevancy DECISION (LightRelevancyMath). The full per-connection + /// GhostRelevancy set population is netcode-world (the L3 gamma test); this pins the pure planar-distance rule. + /// + public class LightRelevancyMathTests + { + [Test] + public void InsideLamp_IsVisible() + { + float r = LightRelevancyMath.LampRadiusDefault; + Assert.IsFalse(LightRelevancyMath.IsHidden(float3.zero, new float3(r - 1f, 0f, 0f), r * r), "just inside the radius = visible"); + Assert.IsFalse(LightRelevancyMath.IsHidden(float3.zero, float3.zero, r * r), "at the player = visible"); + } + + [Test] + public void OutsideLamp_IsHidden() + { + float r = LightRelevancyMath.LampRadiusDefault; + Assert.IsTrue(LightRelevancyMath.IsHidden(float3.zero, new float3(r + 1f, 0f, 0f), r * r), "just beyond the radius = hidden (nothing to see)"); + } + + [Test] + public void Planar_IgnoresY() + { + float r = 5f; + // Within the planar (XZ) radius but far in Y is still VISIBLE — top-down relevancy ignores height. + Assert.IsFalse(LightRelevancyMath.IsHidden(float3.zero, new float3(2f, 1000f, 2f), r * r), "Y is ignored (planar XZ test)"); + } + } +} diff --git a/Assets/_Project/Tests/EditMode/LightRelevancyMathTests.cs.meta b/Assets/_Project/Tests/EditMode/LightRelevancyMathTests.cs.meta new file mode 100644 index 000000000..7e8f84c68 --- /dev/null +++ b/Assets/_Project/Tests/EditMode/LightRelevancyMathTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 14e936313eb9a864181228bcee28c321 \ No newline at end of file