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>
92 lines
3.8 KiB
C#
92 lines
3.8 KiB
C#
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Types;
|
|
|
|
namespace AcDream.Core.Lighting;
|
|
|
|
/// <summary>
|
|
/// Converts a <see cref="Setup"/>'s <c>Lights</c> dictionary (dat-level
|
|
/// <see cref="LightInfo"/> records) into runtime <see cref="LightSource"/>
|
|
/// instances the <see cref="LightManager"/> can consume.
|
|
///
|
|
/// <para>
|
|
/// Retail <see cref="LightInfo"/> fields (r13 §1):
|
|
/// <list type="bullet">
|
|
/// <item><description><c>ViewSpaceLocation</c>: local Frame relative to the owning part.</description></item>
|
|
/// <item><description><c>Color</c>: packed ARGB. Alpha is ignored; channels go through <c>/255</c>.</description></item>
|
|
/// <item><description><c>Intensity</c>: multiplies color for final diffuse.</description></item>
|
|
/// <item><description><c>Falloff</c>: world metres — acts as the <see cref="LightSource.Range"/> hard cutoff.</description></item>
|
|
/// <item><description><c>ConeAngle</c>: radians; 0 = point, >0 = spot cone.</description></item>
|
|
/// </list>
|
|
/// </para>
|
|
/// </summary>
|
|
public static class LightInfoLoader
|
|
{
|
|
/// <summary>
|
|
/// Extract all lights from a Setup, positioned in the entity's
|
|
/// world frame (via <paramref name="entityPosition"/> +
|
|
/// <paramref name="entityRotation"/>). The dat's per-light Frame is
|
|
/// treated as a local offset relative to the entity root; acdream
|
|
/// doesn't yet transform through the animated part chain (retail's
|
|
/// hand-held torches), so held lights render at the entity root
|
|
/// until the animation hook layer handles per-part placement.
|
|
/// </summary>
|
|
public static IReadOnlyList<LightSource> Load(
|
|
Setup setup,
|
|
uint ownerId,
|
|
Vector3 entityPosition,
|
|
Quaternion entityRotation)
|
|
{
|
|
var results = new List<LightSource>();
|
|
if (setup?.Lights is null || setup.Lights.Count == 0) return results;
|
|
|
|
foreach (var kvp in setup.Lights)
|
|
{
|
|
var info = kvp.Value;
|
|
if (info is null) continue;
|
|
|
|
// Local Frame offset into world space.
|
|
Vector3 localOffset = Vector3.Zero;
|
|
Quaternion localRot = Quaternion.Identity;
|
|
if (info.ViewSpaceLocation is not null)
|
|
{
|
|
localOffset = new Vector3(
|
|
info.ViewSpaceLocation.Origin.X,
|
|
info.ViewSpaceLocation.Origin.Y,
|
|
info.ViewSpaceLocation.Origin.Z);
|
|
localRot = new Quaternion(
|
|
info.ViewSpaceLocation.Orientation.X,
|
|
info.ViewSpaceLocation.Orientation.Y,
|
|
info.ViewSpaceLocation.Orientation.Z,
|
|
info.ViewSpaceLocation.Orientation.W);
|
|
}
|
|
|
|
// Transform local offset into world space via the entity's
|
|
// rotation + translation. No per-part chain yet — held
|
|
// torches track the entity's root for now.
|
|
Vector3 worldPos = entityPosition + Vector3.Transform(localOffset, entityRotation);
|
|
Quaternion worldRot = entityRotation * localRot;
|
|
Vector3 forward = Vector3.Transform(Vector3.UnitY, worldRot);
|
|
|
|
var light = new LightSource
|
|
{
|
|
Kind = info.ConeAngle > 0f ? LightKind.Spot : LightKind.Point,
|
|
WorldPosition = worldPos,
|
|
WorldForward = forward,
|
|
ColorLinear = new Vector3(
|
|
(info.Color?.Red ?? 255) / 255f,
|
|
(info.Color?.Green ?? 255) / 255f,
|
|
(info.Color?.Blue ?? 255) / 255f),
|
|
Intensity = info.Intensity,
|
|
Range = info.Falloff,
|
|
ConeAngle = info.ConeAngle,
|
|
OwnerId = ownerId,
|
|
IsLit = true,
|
|
};
|
|
results.Add(light);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
}
|