using AcDream.App.World;
using AcDream.Core.Lighting;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
using DatReaderWriter.DBObjs;
namespace AcDream.App.Rendering.Vfx;
///
/// Projects Setup lights for live top-level and attached entities, while the
/// shared pose registry supplies their current root on each update frame.
///
///
/// Retail CPartArray::SetFrame (0x00519310) calls
/// LIGHTLIST::set_frame (0x00517C60) whenever the owning physics
/// object's frame changes. Logical ownership remains in
/// ; leaving the world removes only this
/// cell-scoped presentation and re-entry registers it again.
///
public sealed class LiveEntityLightController : IDisposable
{
private readonly LiveEntityRuntime _liveEntities;
private readonly EntityEffectPoseRegistry _poses;
private readonly LightingHookSink _lighting;
private readonly Func _loadSetup;
private readonly HashSet _trackedOwners = [];
private readonly HashSet _presentOwners = [];
public LiveEntityLightController(
LiveEntityRuntime liveEntities,
EntityEffectPoseRegistry poses,
LightingHookSink lighting,
Func loadSetup)
{
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
_poses = poses ?? throw new ArgumentNullException(nameof(poses));
_lighting = lighting ?? throw new ArgumentNullException(nameof(lighting));
_loadSetup = loadSetup ?? throw new ArgumentNullException(nameof(loadSetup));
_lighting.OwnerLightingChanged += OnOwnerLightingChanged;
_liveEntities.ProjectionVisibilityChanged += OnProjectionVisibilityChanged;
}
public bool Register(uint serverGuid)
{
if (!_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
|| record.WorldEntity is not { } entity
|| !record.IsSpatiallyProjected
|| !record.IsSpatiallyVisible)
{
return false;
}
RuntimeEntityKey key = RequireProjectionKey(record);
_trackedOwners.Add(key);
_presentOwners.Add(key);
_lighting.UnregisterOwner(entity.Id, forgetState: false);
_lighting.InitializeOwnerLighting(
entity.Id,
(record.FinalPhysicsState & PhysicsStateFlags.Lighting) != 0);
if ((entity.SourceGfxObjOrSetupId & 0xFF000000u) != 0x02000000u)
return false;
if (record.ProjectionKind is LiveEntityProjectionKind.World)
{
if (!_poses.UpdateRoot(entity))
_poses.PublishMeshRefs(entity);
}
Setup? setup = _loadSetup(entity.SourceGfxObjOrSetupId);
if (setup is null
|| setup.Lights.Count == 0
|| !_lighting.IsOwnerLightingEnabled(entity.Id))
return false;
bool isDynamic = (record.FinalPhysicsState & PhysicsStateFlags.Static) == 0;
IReadOnlyList loaded = LightInfoLoader.Load(
setup,
entity.Id,
entity.Position,
entity.Rotation,
isDynamic,
entity.ParentCellId ?? 0u,
tracksOwnerPose: true);
for (int i = 0; i < loaded.Count; i++)
_lighting.RegisterOwnedLight(loaded[i]);
return loaded.Count > 0;
}
/// Withdraw cell-scoped presentation but retain logical light state.
public void Unregister(uint ownerLocalId)
{
if (_liveEntities.TryGetRecordByLocalEntityId(
ownerLocalId,
out LiveEntityRecord record)
&& record.ProjectionKey is { } key)
{
_presentOwners.Remove(key);
}
_lighting.UnregisterOwner(ownerLocalId, forgetState: false);
}
/// Apply a fresh authoritative PhysicsState Lighting transition.
public void OnStateChanged(uint serverGuid)
{
if (!_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
|| record.WorldEntity is not { } entity)
{
return;
}
_lighting.SetOwnerLighting(
entity.Id,
(record.FinalPhysicsState & PhysicsStateFlags.Lighting) != 0);
}
/// End logical ownership after leave-world presentation teardown.
public void Forget(LiveEntityRecord record)
{
ArgumentNullException.ThrowIfNull(record);
if (record.ProjectionKey is not { } key)
return;
_presentOwners.Remove(key);
_trackedOwners.Remove(key);
_lighting.UnregisterOwner(key.LocalEntityId);
}
public void Refresh() => _lighting.RefreshAttachedLights();
internal int TrackedOwnerCount => _trackedOwners.Count;
internal int PresentedOwnerCount => _presentOwners.Count;
///
/// Attached projection visibility can become true before its holding-part
/// composition is published. The attachment owner calls this barrier only
/// after the composed child root is current.
///
public void OnAttachedPoseReady(uint serverGuid)
{
if (!_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
|| record.ProjectionKind is not LiveEntityProjectionKind.Attached
|| record.WorldEntity is not { } entity
|| record.ProjectionKey is not { } key
|| _presentOwners.Contains(key))
{
return;
}
Register(serverGuid);
}
private void OnOwnerLightingChanged(uint ownerLocalId, bool enabled)
{
if (!_liveEntities.TryGetRecordByLocalEntityId(
ownerLocalId,
out LiveEntityRecord record)
|| record.ProjectionKey is not { } key
|| !_presentOwners.Contains(key))
{
return;
}
if (!enabled)
{
_lighting.UnregisterOwner(ownerLocalId, forgetState: false);
return;
}
Register(record.ServerGuid);
}
public void Dispose()
{
_lighting.OwnerLightingChanged -= OnOwnerLightingChanged;
_liveEntities.ProjectionVisibilityChanged -= OnProjectionVisibilityChanged;
foreach (RuntimeEntityKey key in _trackedOwners)
_lighting.UnregisterOwner(key.LocalEntityId);
_trackedOwners.Clear();
_presentOwners.Clear();
}
private void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible)
{
if (record.WorldEntity is not { } entity)
return;
if (record.ProjectionKind is LiveEntityProjectionKind.Attached)
{
// The true edge precedes attachment pose publication. False is
// safe and must immediately withdraw the cell-scoped light.
if (!visible)
Unregister(entity.Id);
return;
}
if (visible)
Register(record.ServerGuid);
else
Unregister(entity.Id);
}
private static RuntimeEntityKey RequireProjectionKey(
LiveEntityRecord record) =>
record.ProjectionKey
?? throw new InvalidOperationException(
$"Live entity 0x{record.ServerGuid:X8}/{record.Generation} " +
"has no exact projection key.");
}