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
|
|
@ -164,6 +164,9 @@ public sealed class GameWindow : IDisposable
|
|||
AcDream.Core.World.SkyStateProvider.Default());
|
||||
public readonly AcDream.Core.Lighting.LightManager Lighting = new();
|
||||
public readonly AcDream.Core.World.WeatherSystem Weather = new();
|
||||
// Wired into the hook router in OnLoad so SetLightHook fires
|
||||
// from the animation pipeline flip the matching LightSource.IsLit.
|
||||
private AcDream.Core.Lighting.LightingHookSink? _lightingSink;
|
||||
|
||||
// Phase G.1 sky renderer + shared UBO. Created once the GL context
|
||||
// exists in OnLoad; shared across every other renderer via
|
||||
|
|
@ -629,6 +632,12 @@ public sealed class GameWindow : IDisposable
|
|||
_particleSink = new AcDream.Core.Vfx.ParticleHookSink(_particleSystem);
|
||||
_hookRouter.Register(_particleSink);
|
||||
|
||||
// Phase G.2 lighting hooks: SetLightHook flips IsLit on
|
||||
// owner-tagged lights so ignite-torch animations light up,
|
||||
// extinguish-torch animations go dark.
|
||||
_lightingSink = new AcDream.Core.Lighting.LightingHookSink(Lighting);
|
||||
_hookRouter.Register(_lightingSink);
|
||||
|
||||
// Phase E.2 audio: init OpenAL + hook sink. Suppressible via
|
||||
// ACDREAM_NO_AUDIO=1 for headless tests / broken audio drivers.
|
||||
if (Environment.GetEnvironmentVariable("ACDREAM_NO_AUDIO") != "1")
|
||||
|
|
@ -767,6 +776,16 @@ public sealed class GameWindow : IDisposable
|
|||
radius: _streamingRadius,
|
||||
removeTerrain: id =>
|
||||
{
|
||||
// Phase G.2: release any LightSources attached to entities
|
||||
// in this landblock before their records disappear from
|
||||
// _worldState — otherwise the LightManager accumulates
|
||||
// stale entries for every walk across a landblock boundary.
|
||||
if (_lightingSink is not null &&
|
||||
_worldState.TryGetLandblock(id, out var lb))
|
||||
{
|
||||
foreach (var ent in lb!.Entities)
|
||||
_lightingSink.UnregisterOwner(ent.Id);
|
||||
}
|
||||
_terrain?.RemoveLandblock(id);
|
||||
_physicsEngine.RemoveLandblock(id);
|
||||
_cellVisibility.RemoveLandblock((id >> 16) & 0xFFFFu);
|
||||
|
|
@ -2308,6 +2327,32 @@ public sealed class GameWindow : IDisposable
|
|||
int scTried = 0, scHaveBounds = 0, scRegistered = 0, scTooThin = 0, scNoBounds = 0;
|
||||
foreach (var entity in lb.Entities)
|
||||
{
|
||||
// Phase G.2: if the entity's Setup has baked-in LightInfos,
|
||||
// register them with the LightManager so torches, braziers,
|
||||
// and lifestones cast real light on nearby geometry. Hooked
|
||||
// via the LightingHookSink so per-entity owner tracking +
|
||||
// SetLightHook IsLit toggles all go through one codepath.
|
||||
// Only applies to Setup-sourced entities (0x02xxxxxx) — raw
|
||||
// GfxObjs don't carry Lights dictionaries.
|
||||
if (_lightingSink is not null && _dats is not null)
|
||||
{
|
||||
uint src = entity.SourceGfxObjOrSetupId;
|
||||
if ((src & 0xFF000000u) == 0x02000000u)
|
||||
{
|
||||
var datSetup = _dats.Get<DatReaderWriter.DBObjs.Setup>(src);
|
||||
if (datSetup is not null && datSetup.Lights.Count > 0)
|
||||
{
|
||||
var loaded = AcDream.Core.Lighting.LightInfoLoader.Load(
|
||||
datSetup,
|
||||
ownerId: entity.Id,
|
||||
entityPosition: entity.Position,
|
||||
entityRotation: entity.Rotation);
|
||||
foreach (var ls in loaded)
|
||||
_lightingSink.RegisterOwnedLight(ls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int entityBsp = 0, entityCyl = 0;
|
||||
// Treat both procedural scenery (0x80000000+) AND LandBlockInfo
|
||||
// stabs (IDs < 0x40000000 with 0x01/0x02 source) as outdoor-entities
|
||||
|
|
|
|||
|
|
@ -65,6 +65,22 @@ public sealed class GpuWorldState
|
|||
|
||||
public bool IsLoaded(uint landblockId) => _loaded.ContainsKey(landblockId);
|
||||
|
||||
/// <summary>
|
||||
/// Try to grab the loaded record for a landblock — useful for callers
|
||||
/// that need to enumerate entities before the landblock is dropped
|
||||
/// (e.g. unregistering dynamic lights on a RemoveLandblock).
|
||||
/// </summary>
|
||||
public bool TryGetLandblock(uint landblockId, out LoadedLandblock? lb)
|
||||
{
|
||||
if (_loaded.TryGetValue(landblockId, out var found))
|
||||
{
|
||||
lb = found;
|
||||
return true;
|
||||
}
|
||||
lb = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Store the axis-aligned bounding box for a loaded landblock. Called from
|
||||
/// the render thread after the terrain mesh is built and uploaded.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue