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
78
src/AcDream.Core/Lighting/LightingHookSink.cs
Normal file
78
src/AcDream.Core/Lighting/LightingHookSink.cs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.Core.Lighting;
|
||||
|
||||
/// <summary>
|
||||
/// Routes <see cref="SetLightHook"/> animation hooks to the
|
||||
/// <see cref="LightManager"/> — when a torch lights / extinguishes via
|
||||
/// an animation frame, flip the corresponding
|
||||
/// <see cref="LightSource.IsLit"/> latch. Per r13 §2 the hook is AC's
|
||||
/// way of saying "this Setup's baked-in LightInfo is now active".
|
||||
///
|
||||
/// <para>
|
||||
/// Registration: at entity spawn time the caller walks the Setup's
|
||||
/// <c>Lights</c> dictionary and registers a <see cref="LightSource"/>
|
||||
/// per <c>LightInfo</c>, tagging it with the owning entity id. When a
|
||||
/// hook fires later, we look up every light tagged to that owner and
|
||||
/// flip them all together (retail's SetLightHook is a per-setup
|
||||
/// boolean, not per-light).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class LightingHookSink : IAnimationHookSink
|
||||
{
|
||||
private readonly LightManager _lights;
|
||||
|
||||
// Index owner → the set of LightSource instances they registered.
|
||||
// Maintained lazily — populated on first RegisterLight for that owner.
|
||||
private readonly Dictionary<uint, List<LightSource>> _byOwner = new();
|
||||
|
||||
public LightingHookSink(LightManager lights)
|
||||
{
|
||||
_lights = lights ?? throw new System.ArgumentNullException(nameof(lights));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a light with the manager + track it by owner so later
|
||||
/// SetLightHook / Unregister calls can reach it.
|
||||
/// </summary>
|
||||
public void RegisterOwnedLight(LightSource light)
|
||||
{
|
||||
System.ArgumentNullException.ThrowIfNull(light);
|
||||
_lights.Register(light);
|
||||
if (!_byOwner.TryGetValue(light.OwnerId, out var list))
|
||||
{
|
||||
list = new List<LightSource>();
|
||||
_byOwner[light.OwnerId] = list;
|
||||
}
|
||||
list.Add(light);
|
||||
}
|
||||
|
||||
/// <summary>Drop every light tagged to this owner (despawn / unload).</summary>
|
||||
public void UnregisterOwner(uint ownerId)
|
||||
{
|
||||
if (!_byOwner.TryGetValue(ownerId, out var list)) return;
|
||||
foreach (var l in list) _lights.Unregister(l);
|
||||
_byOwner.Remove(ownerId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the set of registered lights for an owner — exposed so
|
||||
/// callers can reposition them (torch on hand follows hand part).
|
||||
/// </summary>
|
||||
public IReadOnlyList<LightSource>? GetOwnedLights(uint ownerId)
|
||||
{
|
||||
return _byOwner.TryGetValue(ownerId, out var list) ? list : null;
|
||||
}
|
||||
|
||||
public void OnHook(uint entityId, Vector3 entityWorldPosition, AnimationHook hook)
|
||||
{
|
||||
if (hook is not SetLightHook slh) return;
|
||||
if (!_byOwner.TryGetValue(entityId, out var list)) return;
|
||||
|
||||
foreach (var light in list)
|
||||
light.IsLit = slh.LightsOn;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue