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>
78 lines
2.8 KiB
C#
78 lines
2.8 KiB
C#
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;
|
|
}
|
|
}
|