fix(overhaul): integrate reviewed room-light selection repair
Exact26 code/test/architecture/register blobs from621b41fa3; campaign ledger and lead verification included. Independent retail and production/lifetime/ABI reviews PASS. Lead69Core/176App/2actualshader pixels, viewer/clear/NaN negative controls fail as intended, exact restoration69PASS. AP68retired; AP16/35/85 residuals honest. Fresh campaign Release and graphical lighting proof still owed; temporary observer cleanup contract conditional. FPS deferred; no G4 or main merge.
This commit is contained in:
parent
e8efe1131f
commit
a5debaca2b
30 changed files with 928 additions and 399 deletions
|
|
@ -12,6 +12,7 @@ public sealed class LightManagerTests
|
|||
{
|
||||
Kind = LightKind.Point,
|
||||
WorldPosition = pos,
|
||||
RankingOrigin = pos,
|
||||
Range = range,
|
||||
IsLit = lit,
|
||||
OwnerId = ownerId,
|
||||
|
|
@ -23,6 +24,7 @@ public sealed class LightManagerTests
|
|||
{
|
||||
Kind = LightKind.Point,
|
||||
WorldPosition = pos,
|
||||
RankingOrigin = pos,
|
||||
Range = range,
|
||||
IsLit = true,
|
||||
IsDynamic = true,
|
||||
|
|
@ -53,6 +55,21 @@ public sealed class LightManagerTests
|
|||
Assert.Equal(1, mgr.RegisteredCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_NondirectionalWithoutExplicitRankingOrigin_Throws()
|
||||
{
|
||||
var mgr = new LightManager();
|
||||
var light = new LightSource
|
||||
{
|
||||
Kind = LightKind.Point,
|
||||
WorldPosition = new Vector3(3f, 4f, 5f),
|
||||
};
|
||||
|
||||
ArgumentException error = Assert.Throws<ArgumentException>(() => mgr.Register(light));
|
||||
Assert.Contains("ranking origin", error.Message, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Equal(0, mgr.RegisteredCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tick_SelectsByDistance_Top8()
|
||||
{
|
||||
|
|
@ -175,18 +192,148 @@ public sealed class LightManagerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildPointLightSnapshot_IndexStable_InBudget()
|
||||
public void BuildPointLightSnapshot_UnderCap_SortsByRootDistance()
|
||||
{
|
||||
var mgr = new LightManager();
|
||||
// Registration order preserved when under MaxGlobalLights (no sort).
|
||||
mgr.Register(MakePoint(new Vector3(100, 0, 0), 5f)); // far
|
||||
mgr.Register(MakePoint(new Vector3(1, 0, 0), 5f)); // near
|
||||
|
||||
mgr.BuildPointLightSnapshot(Vector3.Zero);
|
||||
|
||||
Assert.Equal(2, mgr.PointSnapshot.Count);
|
||||
Assert.Equal(100f, mgr.PointSnapshot[0].WorldPosition.X, 3); // index 0 = first registered
|
||||
Assert.Equal(1f, mgr.PointSnapshot[1].WorldPosition.X, 3);
|
||||
Assert.Equal(1f, mgr.PointSnapshot[0].WorldPosition.X, 3);
|
||||
Assert.Equal(100f, mgr.PointSnapshot[1].WorldPosition.X, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildPointLightSnapshot_RanksRootBeforeAuthoredOffsetFinalPosition()
|
||||
{
|
||||
var manager = new LightManager();
|
||||
LightSource nearRootFarFinal = MakePoint(new Vector3(100f, 0f, 0f), 20f, ownerId: 1);
|
||||
nearRootFarFinal.RankingOrigin = new Vector3(1f, 0f, 0f);
|
||||
LightSource farRootNearFinal = MakePoint(new Vector3(2f, 0f, 0f), 20f, ownerId: 2);
|
||||
farRootNearFinal.RankingOrigin = new Vector3(50f, 0f, 0f);
|
||||
manager.Register(farRootNearFinal);
|
||||
manager.Register(nearRootFarFinal);
|
||||
|
||||
manager.BuildPointLightSnapshot(Vector3.Zero);
|
||||
|
||||
Assert.Equal(new[] { nearRootFarFinal, farRootNearFinal }, manager.PointSnapshot);
|
||||
Assert.Equal(1f, manager.PointSnapshot[0].DistSq);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildPointLightSnapshot_StrictForwardInsertion_PreservesEqualAndNaNOrder()
|
||||
{
|
||||
var manager = new LightManager();
|
||||
LightSource nan = MakePoint(Vector3.Zero, 20f, ownerId: 1);
|
||||
nan.RankingOrigin = new Vector3(float.NaN, 0f, 0f);
|
||||
LightSource equalA = MakePoint(new Vector3(10f, 0f, 0f), 20f, ownerId: 2);
|
||||
equalA.RankingOrigin = new Vector3(2f, 0f, 0f);
|
||||
LightSource equalB = MakePoint(new Vector3(20f, 0f, 0f), 20f, ownerId: 3);
|
||||
equalB.RankingOrigin = new Vector3(-2f, 0f, 0f);
|
||||
manager.Register(nan);
|
||||
manager.Register(equalA);
|
||||
manager.Register(equalB);
|
||||
|
||||
manager.BuildPointLightSnapshot(Vector3.Zero);
|
||||
|
||||
Assert.Equal(new[] { nan, equalA, equalB }, manager.PointSnapshot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildPointLightSnapshot_StrictForwardInsertion_AdvancesPastNaN()
|
||||
{
|
||||
var manager = new LightManager();
|
||||
LightSource far = MakePoint(new Vector3(10f, 0f, 0f), 20f, ownerId: 1);
|
||||
LightSource nan = MakePoint(Vector3.Zero, 20f, ownerId: 2);
|
||||
nan.RankingOrigin = new Vector3(float.NaN, 0f, 0f);
|
||||
LightSource near = MakePoint(Vector3.One, 20f, ownerId: 3);
|
||||
manager.Register(far);
|
||||
manager.Register(nan);
|
||||
manager.Register(near);
|
||||
|
||||
manager.BuildPointLightSnapshot(Vector3.Zero);
|
||||
|
||||
Assert.Equal(new[] { near, far, nan }, manager.PointSnapshot);
|
||||
Assert.Equal(new[] { 3f, 100f }, manager.PointSnapshot.Take(2).Select(light => light.DistSq));
|
||||
Assert.True(float.IsNaN(manager.PointSnapshot[2].DistSq));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildPointLightSnapshot_SpotUsesZeroRank_PointUsesRootDistance()
|
||||
{
|
||||
var manager = new LightManager();
|
||||
LightSource point = MakePoint(new Vector3(1f, 0f, 0f), 20f, ownerId: 1);
|
||||
LightSource spot = MakePoint(new Vector3(100f, 0f, 0f), 20f, ownerId: 2);
|
||||
spot.Kind = LightKind.Spot;
|
||||
manager.Register(point);
|
||||
manager.Register(spot);
|
||||
|
||||
manager.BuildPointLightSnapshot(Vector3.Zero);
|
||||
|
||||
Assert.Equal(new[] { spot, point }, manager.PointSnapshot);
|
||||
Assert.Equal(0f, spot.DistSq);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildPointLightSnapshot_IndependentSevenAndFortyProducts_DoNotCrossEvict()
|
||||
{
|
||||
var manager = new LightManager();
|
||||
var dynamics = new List<LightSource>();
|
||||
var statics = new List<LightSource>();
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
LightSource light = MakeDynamic(new Vector3(100f + i, 0f, 0f), 10f);
|
||||
light.OwnerId = checked((uint)(100 + i));
|
||||
dynamics.Add(light);
|
||||
manager.Register(light);
|
||||
}
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
LightSource light = MakePoint(new Vector3(i, 0f, 0f), 10f, checked((uint)(200 + i)));
|
||||
statics.Add(light);
|
||||
manager.Register(light);
|
||||
}
|
||||
|
||||
manager.BuildPointLightSnapshot(Vector3.Zero);
|
||||
|
||||
Assert.Equal(LightManager.MaxGlobalLights, manager.PointSnapshot.Count);
|
||||
Assert.Equal(dynamics.Take(7), manager.PointSnapshot.Take(7));
|
||||
Assert.Equal(statics.Take(40), manager.PointSnapshot.Skip(7));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildPointLightSnapshot_ViewerWinsEqualRootTieThoughRegisteredLast()
|
||||
{
|
||||
var manager = new LightManager();
|
||||
LightSource ordinary = MakeDynamic(new Vector3(0f, 0f, 2f), 15f);
|
||||
ordinary.RankingOrigin = Vector3.Zero;
|
||||
manager.Register(ordinary);
|
||||
manager.UpdateViewerLight(Vector3.Zero);
|
||||
|
||||
manager.BuildPointLightSnapshot(Vector3.Zero);
|
||||
|
||||
Assert.Equal(2, manager.PointSnapshot.Count);
|
||||
Assert.NotSame(ordinary, manager.PointSnapshot[0]);
|
||||
Assert.Equal(new Vector3(0f, 0f, 2f), manager.PointSnapshot[0].WorldPosition);
|
||||
Assert.Equal(Vector3.Zero, manager.PointSnapshot[0].RankingOrigin);
|
||||
Assert.Same(ordinary, manager.PointSnapshot[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildPointLightSnapshot_ClearRemovesRetainedProducts()
|
||||
{
|
||||
var manager = new LightManager();
|
||||
manager.Register(MakePoint(Vector3.One, 5f));
|
||||
manager.BuildPointLightSnapshot(Vector3.Zero);
|
||||
Assert.NotEmpty(manager.PointSnapshot);
|
||||
|
||||
manager.Clear();
|
||||
Assert.Empty(manager.PointSnapshot);
|
||||
manager.BuildPointLightSnapshot(Vector3.Zero);
|
||||
|
||||
Assert.Empty(manager.PointSnapshot);
|
||||
}
|
||||
|
||||
// ── Resident collection (#176 corrected reading, 2026-07-06) ───────────────
|
||||
|
|
@ -280,8 +427,6 @@ public sealed class LightManagerTests
|
|||
manager.BuildPointLightSnapshot(Vector3.Zero);
|
||||
|
||||
Assert.Equal(expected, manager.PointSnapshot);
|
||||
Assert.True(manager.LastPointSnapshotUsedTieFallback);
|
||||
Assert.False(manager.LastPointSnapshotUsedBoundedSelection);
|
||||
|
||||
manager.BuildPointLightSnapshot(Vector3.Zero);
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
|
|
@ -369,8 +514,6 @@ public sealed class LightManagerTests
|
|||
|
||||
Assert.Equal(LightManager.MaxGlobalLights, manager.PointSnapshot.Count);
|
||||
Assert.Equal(expected, manager.PointSnapshot);
|
||||
Assert.True(manager.LastPointSnapshotUsedBoundedSelection);
|
||||
Assert.False(manager.LastPointSnapshotUsedTieFallback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -391,8 +534,6 @@ public sealed class LightManagerTests
|
|||
}
|
||||
manager.BuildPointLightSnapshot(Vector3.Zero);
|
||||
manager.BuildPointLightSnapshot(Vector3.Zero);
|
||||
Assert.True(manager.LastPointSnapshotUsedBoundedSelection);
|
||||
Assert.False(manager.LastPointSnapshotUsedTieFallback);
|
||||
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (int iteration = 0; iteration < 100; iteration++)
|
||||
|
|
@ -498,14 +639,14 @@ public sealed class LightManagerTests
|
|||
Assert.Equal(a[0], b[0]);
|
||||
}
|
||||
|
||||
// ── SelectForCell — retail minimize_envcell_lighting (all dynamics on every cell) ──
|
||||
// ── SelectForCell — complete retained 7-dynamic + 40-static products ──
|
||||
|
||||
[Fact]
|
||||
public void SelectForCell_AppliesAllDynamicLights_EvenOutOfReach()
|
||||
{
|
||||
// Retail enables the WHOLE dynamic subset for every cell (cdb-verified: the same
|
||||
// portal lights on every Facility Hub cell) — including ones that don't reach it,
|
||||
// since the shader's range cutoff zeroes those. Static lights still cull by reach.
|
||||
// Retail enables the whole dynamic subset and bakes the whole retained static
|
||||
// product. The GPU path supplies both unchanged; per-vertex range cutoff decides
|
||||
// which entries contribute.
|
||||
var snapshot = new[]
|
||||
{
|
||||
MakePoint(new Vector3(1, 0, 0), range: 5f), // 0: static, reaches
|
||||
|
|
@ -513,8 +654,8 @@ public sealed class LightManagerTests
|
|||
MakeDynamic(new Vector3(2, 0, 0), range: 5f), // 2: dynamic, near
|
||||
MakePoint(new Vector3(50, 0, 0), range: 5f), // 3: static, far (out of reach)
|
||||
};
|
||||
Span<int> sel = stackalloc int[LightManager.MaxLightsPerObject];
|
||||
int n = LightManager.SelectForCell(snapshot, Vector3.Zero, radius: 1f, sel);
|
||||
Span<int> sel = stackalloc int[LightManager.MaxLightsPerEnvCell];
|
||||
int n = LightManager.SelectForCell(snapshot, sel);
|
||||
|
||||
bool d1 = false, d2 = false, s0 = false, s3 = false;
|
||||
for (int i = 0; i < n; i++)
|
||||
|
|
@ -527,7 +668,7 @@ public sealed class LightManagerTests
|
|||
Assert.True(d1, "the FAR dynamic light must still be applied — retail enables all dynamics");
|
||||
Assert.True(d2, "the near dynamic light is applied");
|
||||
Assert.True(s0, "the near static light reaches the cell → selected");
|
||||
Assert.False(s3, "the far static light doesn't reach → not selected");
|
||||
Assert.True(s3, "the complete retained static product is supplied; the shader applies range");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -541,15 +682,35 @@ public sealed class LightManagerTests
|
|||
MakeDynamic(new Vector3(0, 0, 0), range: 5f),
|
||||
MakeDynamic(new Vector3(100, 0, 0), range: 5f),
|
||||
};
|
||||
Span<int> a = stackalloc int[8];
|
||||
Span<int> b = stackalloc int[8];
|
||||
int na = LightManager.SelectForCell(snapshot, new Vector3(0, 0, 0), 1f, a);
|
||||
int nb = LightManager.SelectForCell(snapshot, new Vector3(500, 0, 0), 1f, b);
|
||||
Span<int> a = stackalloc int[LightManager.MaxLightsPerEnvCell];
|
||||
Span<int> b = stackalloc int[LightManager.MaxLightsPerEnvCell];
|
||||
int na = LightManager.SelectForCell(snapshot, a);
|
||||
int nb = LightManager.SelectForCell(snapshot, b);
|
||||
|
||||
Assert.Equal(2, na); // both dynamics on the near cell
|
||||
Assert.Equal(2, nb); // both dynamics on the far cell too — identical, no flap
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectForCell_CarriesAllSevenDynamicsAndFortyStatics_WhileObjectStaysEight()
|
||||
{
|
||||
var snapshot = new List<LightSource>();
|
||||
for (int i = 0; i < LightManager.MaxDynamicPointLights; i++)
|
||||
snapshot.Add(MakeDynamic(new Vector3(i, 0f, 0f), 100f));
|
||||
for (int i = 0; i < LightManager.MaxStaticPointLights; i++)
|
||||
snapshot.Add(MakePoint(new Vector3(i + 10f, 0f, 0f), 100f));
|
||||
|
||||
Span<int> cell = stackalloc int[LightManager.MaxLightsPerEnvCell];
|
||||
int cellCount = LightManager.SelectForCell(snapshot, cell);
|
||||
Span<int> obj = stackalloc int[LightManager.MaxLightsPerEnvCell];
|
||||
int objectCount = LightManager.SelectForObject(snapshot, Vector3.Zero, 100f, obj);
|
||||
|
||||
Assert.Equal(47, cellCount);
|
||||
for (int index = 0; index < cellCount; index++)
|
||||
Assert.Equal(index, cell[index]);
|
||||
Assert.Equal(8, objectCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #176/#177 (2026-07-06, corrected same day) — the end-state pin. The pool is
|
||||
/// retail's RESIDENT collection anchored at the PLAYER: a light in range of an
|
||||
|
|
@ -599,7 +760,7 @@ public sealed class LightManagerTests
|
|||
"an in-range light beside the player was evicted from the pool — " +
|
||||
"per-cell lighting would pop (the #176/#177 mechanism)");
|
||||
Assert.True(torchSelected2, "consecutive same-player builds must select identically");
|
||||
Assert.Equal(LightManager.MaxGlobalLights, mgr.PointSnapshot.Count); // cap applied to the far cluster
|
||||
Assert.Equal(LightManager.MaxStaticPointLights, mgr.PointSnapshot.Count);
|
||||
|
||||
static bool SelectedContains(
|
||||
System.Collections.Generic.IReadOnlyList<LightSource> snapshot,
|
||||
|
|
@ -623,22 +784,34 @@ public sealed class LightManagerTests
|
|||
continue;
|
||||
ranked.Add(new OracleRank(
|
||||
light,
|
||||
Vector3.DistanceSquared(light.WorldPosition, player)));
|
||||
light.Kind == LightKind.Point
|
||||
? Vector3.DistanceSquared(light.RankingOrigin, player)
|
||||
: 0f));
|
||||
}
|
||||
|
||||
if (ranked.Count <= LightManager.MaxGlobalLights)
|
||||
return ranked.Select(static item => item.Light).ToArray();
|
||||
var dynamics = ranked.Where(static item => item.Light.IsDynamic).ToList();
|
||||
var statics = ranked.Where(static item => !item.Light.IsDynamic).ToList();
|
||||
StableRetailInsertion(dynamics, LightManager.MaxDynamicPointLights);
|
||||
StableRetailInsertion(statics, LightManager.MaxStaticPointLights);
|
||||
return dynamics.Concat(statics).Select(static item => item.Light).ToArray();
|
||||
|
||||
ranked.Sort(static (left, right) =>
|
||||
static void StableRetailInsertion(List<OracleRank> values, int cap)
|
||||
{
|
||||
if (left.Light.IsDynamic != right.Light.IsDynamic)
|
||||
return left.Light.IsDynamic ? -1 : 1;
|
||||
return left.DistanceSq.CompareTo(right.DistanceSq);
|
||||
});
|
||||
return ranked
|
||||
.Take(LightManager.MaxGlobalLights)
|
||||
.Select(static item => item.Light)
|
||||
.ToArray();
|
||||
var selected = new List<OracleRank>(cap);
|
||||
foreach (OracleRank value in values)
|
||||
{
|
||||
int index = 0;
|
||||
while (index < selected.Count && !(value.DistanceSq < selected[index].DistanceSq))
|
||||
index++;
|
||||
if (index >= cap)
|
||||
continue;
|
||||
selected.Insert(index, value);
|
||||
if (selected.Count > cap)
|
||||
selected.RemoveAt(cap);
|
||||
}
|
||||
values.Clear();
|
||||
values.AddRange(selected);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct OracleRank(
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ public sealed class LightingHookSinkTests
|
|||
var mgr = new LightManager();
|
||||
var sink = new LightingHookSink(mgr, new MutablePoseSource());
|
||||
|
||||
var light1 = new LightSource { Kind = LightKind.Point, OwnerId = 42, IsLit = true };
|
||||
var light2 = new LightSource { Kind = LightKind.Point, OwnerId = 42, IsLit = true };
|
||||
var other = new LightSource { Kind = LightKind.Point, OwnerId = 99, IsLit = true };
|
||||
var light1 = new LightSource { Kind = LightKind.Point, OwnerId = 42, IsLit = true, RankingOrigin = Vector3.Zero };
|
||||
var light2 = new LightSource { Kind = LightKind.Point, OwnerId = 42, IsLit = true, RankingOrigin = Vector3.Zero };
|
||||
var other = new LightSource { Kind = LightKind.Point, OwnerId = 99, IsLit = true, RankingOrigin = Vector3.Zero };
|
||||
sink.RegisterOwnedLight(light1);
|
||||
sink.RegisterOwnedLight(light2);
|
||||
sink.RegisterOwnedLight(other);
|
||||
|
|
@ -35,8 +35,8 @@ public sealed class LightingHookSinkTests
|
|||
var mgr = new LightManager();
|
||||
var sink = new LightingHookSink(mgr, new MutablePoseSource());
|
||||
|
||||
sink.RegisterOwnedLight(new LightSource { OwnerId = 7 });
|
||||
sink.RegisterOwnedLight(new LightSource { OwnerId = 7 });
|
||||
sink.RegisterOwnedLight(new LightSource { OwnerId = 7, RankingOrigin = Vector3.Zero });
|
||||
sink.RegisterOwnedLight(new LightSource { OwnerId = 7, RankingOrigin = Vector3.Zero });
|
||||
Assert.Equal(2, mgr.RegisteredCount);
|
||||
|
||||
sink.UnregisterOwner(7);
|
||||
|
|
@ -48,7 +48,7 @@ public sealed class LightingHookSinkTests
|
|||
{
|
||||
var mgr = new LightManager();
|
||||
var sink = new LightingHookSink(mgr, new MutablePoseSource());
|
||||
var light = new LightSource { OwnerId = 1, IsLit = true };
|
||||
var light = new LightSource { OwnerId = 1, IsLit = true, RankingOrigin = Vector3.Zero };
|
||||
sink.RegisterOwnedLight(light);
|
||||
|
||||
// Should not crash or change state for non-SetLight hooks.
|
||||
|
|
@ -71,6 +71,7 @@ public sealed class LightingHookSinkTests
|
|||
var light = new LightSource
|
||||
{
|
||||
OwnerId = 42u,
|
||||
RankingOrigin = Vector3.Zero,
|
||||
LocalPose = Matrix4x4.CreateTranslation(1, 0, 2),
|
||||
TracksOwnerPose = true,
|
||||
};
|
||||
|
|
@ -81,6 +82,7 @@ public sealed class LightingHookSinkTests
|
|||
Assert.InRange(light.WorldPosition.X, 9.99f, 10.01f);
|
||||
Assert.InRange(light.WorldPosition.Y, 20.99f, 21.01f);
|
||||
Assert.InRange(light.WorldPosition.Z, 31.99f, 32.01f);
|
||||
Assert.Equal(new Vector3(10f, 20f, 30f), light.RankingOrigin);
|
||||
Assert.Equal(0x01010002u, light.CellId);
|
||||
}
|
||||
|
||||
|
|
@ -95,6 +97,7 @@ public sealed class LightingHookSinkTests
|
|||
{
|
||||
OwnerId = 42u,
|
||||
WorldPosition = new Vector3(7, 8, 9),
|
||||
RankingOrigin = new Vector3(7, 8, 9),
|
||||
LocalPose = Matrix4x4.CreateTranslation(1, 0, 0),
|
||||
TracksOwnerPose = false,
|
||||
};
|
||||
|
|
@ -114,7 +117,7 @@ public sealed class LightingHookSinkTests
|
|||
sink.InitializeOwnerLighting(7u, enabled: true);
|
||||
sink.SetOwnerLighting(7u, enabled: false);
|
||||
sink.UnregisterOwner(7u, forgetState: false);
|
||||
var replacement = new LightSource { OwnerId = 7u };
|
||||
var replacement = new LightSource { OwnerId = 7u, RankingOrigin = Vector3.Zero };
|
||||
|
||||
sink.RegisterOwnedLight(replacement);
|
||||
|
||||
|
|
@ -160,6 +163,7 @@ public sealed class LightInfoLoaderTests
|
|||
Assert.Equal(10.4f, light.Range, 3); // Falloff 8 × static_light_factor 1.3 (calc_point_light 0x00820e24)
|
||||
Assert.Equal(0.8f, light.Intensity);
|
||||
Assert.Equal(new Vector3(101, 202, 303), light.WorldPosition);
|
||||
Assert.Equal(new Vector3(100, 200, 300), light.RankingOrigin);
|
||||
Assert.Equal(new Vector3(1, 2, 3), light.LocalPose.Translation);
|
||||
Assert.InRange(light.ColorLinear.X, 0.99f, 1.01f);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ public sealed class SceneLightingUboTests
|
|||
{
|
||||
Kind = LightKind.Point,
|
||||
WorldPosition = new Vector3(1, 2, 3),
|
||||
RankingOrigin = new Vector3(1, 2, 3),
|
||||
ColorLinear = new Vector3(1f, 0.5f, 0.25f),
|
||||
Intensity = 0.8f,
|
||||
Range = 6f,
|
||||
|
|
@ -84,6 +85,7 @@ public sealed class SceneLightingUboTests
|
|||
{
|
||||
Kind = LightKind.Point,
|
||||
WorldPosition = new Vector3(i, 0, 0),
|
||||
RankingOrigin = new Vector3(i, 0, 0),
|
||||
Range = 200f, // all in range
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using AcDream.Content;
|
||||
using AcDream.Core.Lighting;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Options;
|
||||
|
|
@ -111,7 +114,7 @@ public class Issue93TownNetworkFountainRoomLightInspectionTests
|
|||
|
||||
_out.WriteLine($"=== Setup 0x{setupId:X8}: Parts={setup!.Parts.Count} PlacementFrames={setup.PlacementFrames.Count} Lights={setup.Lights.Count} ===");
|
||||
foreach (var kvp in setup.Lights)
|
||||
_out.WriteLine($" light[{kvp.Key}] Color=({kvp.Value.Color?.Red},{kvp.Value.Color?.Green},{kvp.Value.Color?.Blue}) Intensity={kvp.Value.Intensity} Falloff={kvp.Value.Falloff} ConeAngle={kvp.Value.ConeAngle}");
|
||||
_out.WriteLine($" light[{kvp.Key}] Color=({kvp.Value.Color?.Red},{kvp.Value.Color?.Green},{kvp.Value.Color?.Blue}) Intensity={kvp.Value.Intensity} Falloff={kvp.Value.Falloff} ConeAngle={kvp.Value.ConeAngle} LocalOrigin=({kvp.Value.ViewSpaceLocation?.Origin.X:R},{kvp.Value.ViewSpaceLocation?.Origin.Y:R},{kvp.Value.ViewSpaceLocation?.Origin.Z:R})");
|
||||
|
||||
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
|
||||
_out.WriteLine($" SetupMesh.Flatten -> {flat.Count} MeshRefs");
|
||||
|
|
@ -139,6 +142,68 @@ public class Issue93TownNetworkFountainRoomLightInspectionTests
|
|||
$"=> GameWindow.cs:7324 would {(survivors == 0 ? "DROP" : "KEEP")} this entity");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TownCeilingFixture_AuthoredTypeKeyAndRootRankOrigin_ArePinnedFromInstalledDat()
|
||||
{
|
||||
var datDir = ResolveDatDir();
|
||||
if (datDir is null)
|
||||
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
|
||||
const uint cellId = 0x00070144u;
|
||||
const uint setupId = 0x02000365u;
|
||||
EnvCell cell = Assert.IsType<EnvCell>(dats.Get<EnvCell>(cellId));
|
||||
var fixture = Assert.Single(cell.StaticObjects, entry => entry.Id == setupId);
|
||||
Setup setup = Assert.IsType<Setup>(dats.Get<Setup>(setupId));
|
||||
var authored = Assert.Single(setup.Lights);
|
||||
|
||||
// The dictionary key is native LIGHTINFO.type. The current loader's
|
||||
// cone-derived Point/Spot projection deliberately remains unchanged.
|
||||
Assert.Equal(0, authored.Key);
|
||||
Assert.Equal(0xCDCDCDCDu, BitConverter.SingleToUInt32Bits(authored.Value.ConeAngle));
|
||||
Assert.Equal(0.000759337f, authored.Value.ViewSpaceLocation.Origin.X);
|
||||
Assert.Equal(0.00675148f, authored.Value.ViewSpaceLocation.Origin.Y);
|
||||
Assert.Equal(0.0277f, authored.Value.ViewSpaceLocation.Origin.Z);
|
||||
|
||||
Vector3 root = new(
|
||||
fixture.Frame.Origin.X,
|
||||
fixture.Frame.Origin.Y,
|
||||
fixture.Frame.Origin.Z);
|
||||
Quaternion rotation = new(
|
||||
fixture.Frame.Orientation.X,
|
||||
fixture.Frame.Orientation.Y,
|
||||
fixture.Frame.Orientation.Z,
|
||||
fixture.Frame.Orientation.W);
|
||||
Assert.Equal(new Vector3(69.875f, -69.916f, 5.005f), root);
|
||||
Assert.Equal(new Quaternion(0f, 0f, -0.94372f, 0.330745f), rotation);
|
||||
LightSource light = Assert.Single(LightInfoLoader.Load(
|
||||
setup,
|
||||
ownerId: 0x4000712Fu,
|
||||
entityPosition: root,
|
||||
entityRotation: rotation,
|
||||
isDynamic: false,
|
||||
cellId: cellId));
|
||||
|
||||
Assert.True(light.HasRankingOrigin);
|
||||
Assert.Equal(root, light.RankingOrigin);
|
||||
Assert.Equal(LightKind.Point, light.Kind);
|
||||
Assert.NotEqual(root, light.WorldPosition);
|
||||
var localFrame = authored.Value.ViewSpaceLocation;
|
||||
var localOffset = new Vector3(
|
||||
localFrame.Origin.X,
|
||||
localFrame.Origin.Y,
|
||||
localFrame.Origin.Z);
|
||||
var localRotation = new Quaternion(
|
||||
localFrame.Orientation.X,
|
||||
localFrame.Orientation.Y,
|
||||
localFrame.Orientation.Z,
|
||||
localFrame.Orientation.W);
|
||||
Matrix4x4 expectedWorld = (Matrix4x4.CreateFromQuaternion(localRotation)
|
||||
* Matrix4x4.CreateTranslation(localOffset))
|
||||
* (Matrix4x4.CreateFromQuaternion(rotation) * Matrix4x4.CreateTranslation(root));
|
||||
Assert.Equal(expectedWorld.Translation, light.WorldPosition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Follow-up (same session, 2026-07-09): user confirmed lighting improved but
|
||||
/// reported missing candle flames + fountain water particles. Hypothesis tested:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue