feat(lighting): Phase G.2 — Setup.Lights + SetLightHook wiring
Register dat-defined LightInfos as runtime LightSources when entities stream in. Every Setup (0x02xxxxxx) with a non-empty Lights dictionary gets its per-part lights pulled via LightInfoLoader, which converts the local Frame + ColorARGB + Intensity + Falloff + ConeAngle fields into world-space LightSource records owned by the entity id. Wire the LightingHookSink into the animation-hook router so retail's SetLightHook animations (ignite-torch, extinguish-lamp) flip the matching LightSource.IsLit latches. One hook may own multiple lights (lamp-posts with two LightInfo entries) — the sink maintains an owner-indexed map so all get toggled together. Unregister on landblock unload: the streaming controller's removeTerrain callback grabs the loaded landblock's entity list (new GpuWorldState.TryGetLandblock helper) and drops every owner from the sink before the entities disappear — otherwise walking across landblocks accumulates stale LightSources. 9 new tests (LightingHookSink routing + LightInfoLoader conversion). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9618c66813
commit
7b9a66c9ea
5 changed files with 350 additions and 0 deletions
119
tests/AcDream.Core.Tests/Lighting/LightingHookSinkTests.cs
Normal file
119
tests/AcDream.Core.Tests/Lighting/LightingHookSinkTests.cs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Lighting;
|
||||
using DatReaderWriter.Types;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Lighting;
|
||||
|
||||
public sealed class LightingHookSinkTests
|
||||
{
|
||||
[Fact]
|
||||
public void SetLightHook_FlipsOwnedLights()
|
||||
{
|
||||
var mgr = new LightManager();
|
||||
var sink = new LightingHookSink(mgr);
|
||||
|
||||
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 };
|
||||
sink.RegisterOwnedLight(light1);
|
||||
sink.RegisterOwnedLight(light2);
|
||||
sink.RegisterOwnedLight(other);
|
||||
|
||||
var hook = new SetLightHook { LightsOn = false };
|
||||
sink.OnHook(entityId: 42, entityWorldPosition: Vector3.Zero, hook: hook);
|
||||
|
||||
Assert.False(light1.IsLit);
|
||||
Assert.False(light2.IsLit);
|
||||
Assert.True(other.IsLit); // owner 99 untouched
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnregisterOwner_RemovesAllOwnedLights()
|
||||
{
|
||||
var mgr = new LightManager();
|
||||
var sink = new LightingHookSink(mgr);
|
||||
|
||||
sink.RegisterOwnedLight(new LightSource { OwnerId = 7 });
|
||||
sink.RegisterOwnedLight(new LightSource { OwnerId = 7 });
|
||||
Assert.Equal(2, mgr.RegisteredCount);
|
||||
|
||||
sink.UnregisterOwner(7);
|
||||
Assert.Equal(0, mgr.RegisteredCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnrelatedHook_Ignored()
|
||||
{
|
||||
var mgr = new LightManager();
|
||||
var sink = new LightingHookSink(mgr);
|
||||
var light = new LightSource { OwnerId = 1, IsLit = true };
|
||||
sink.RegisterOwnedLight(light);
|
||||
|
||||
// Should not crash or change state for non-SetLight hooks.
|
||||
var noise = new SoundHook();
|
||||
sink.OnHook(entityId: 1, entityWorldPosition: Vector3.Zero, hook: noise);
|
||||
|
||||
Assert.True(light.IsLit);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class LightInfoLoaderTests
|
||||
{
|
||||
[Fact]
|
||||
public void Load_EmptyLights_ReturnsEmpty()
|
||||
{
|
||||
var setup = new DatReaderWriter.DBObjs.Setup();
|
||||
var result = LightInfoLoader.Load(setup, 1u, Vector3.Zero, Quaternion.Identity);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_PointLight_ProducesCorrectSource()
|
||||
{
|
||||
var setup = new DatReaderWriter.DBObjs.Setup();
|
||||
setup.Lights[0] = new LightInfo
|
||||
{
|
||||
ViewSpaceLocation = new Frame
|
||||
{
|
||||
Origin = new Vector3(1, 2, 3),
|
||||
Orientation = Quaternion.Identity,
|
||||
},
|
||||
Color = new ColorARGB { Red = 255, Green = 200, Blue = 50, Alpha = 255 },
|
||||
Intensity = 0.8f,
|
||||
Falloff = 8f,
|
||||
ConeAngle = 0f, // point
|
||||
};
|
||||
|
||||
var result = LightInfoLoader.Load(setup, ownerId: 77,
|
||||
entityPosition: new Vector3(100, 200, 300),
|
||||
entityRotation: Quaternion.Identity);
|
||||
|
||||
Assert.Single(result);
|
||||
var light = result[0];
|
||||
Assert.Equal(LightKind.Point, light.Kind);
|
||||
Assert.Equal(77u, light.OwnerId);
|
||||
Assert.Equal(8f, light.Range);
|
||||
Assert.Equal(0.8f, light.Intensity);
|
||||
Assert.Equal(new Vector3(101, 202, 303), light.WorldPosition);
|
||||
Assert.InRange(light.ColorLinear.X, 0.99f, 1.01f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_NonZeroConeAngle_ProducesSpot()
|
||||
{
|
||||
var setup = new DatReaderWriter.DBObjs.Setup();
|
||||
setup.Lights[0] = new LightInfo
|
||||
{
|
||||
ViewSpaceLocation = new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity },
|
||||
Color = new ColorARGB { Red = 255, Green = 255, Blue = 255, Alpha = 255 },
|
||||
Intensity = 1f,
|
||||
Falloff = 5f,
|
||||
ConeAngle = 0.5f,
|
||||
};
|
||||
|
||||
var result = LightInfoLoader.Load(setup, ownerId: 1, entityPosition: Vector3.Zero, entityRotation: Quaternion.Identity);
|
||||
Assert.Equal(LightKind.Spot, result[0].Kind);
|
||||
Assert.Equal(0.5f, result[0].ConeAngle);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue