using AcDream.App.World;
using AcDream.Core.Lighting;
using AcDream.Core.Physics;
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 Dictionary _serverGuidByOwner = new();
private readonly HashSet _presentOwners = new();
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;
}
_serverGuidByOwner[entity.Id] = serverGuid;
_presentOwners.Add(entity.Id);
_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)
{
_presentOwners.Remove(ownerLocalId);
_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(uint ownerLocalId)
{
_presentOwners.Remove(ownerLocalId);
_serverGuidByOwner.Remove(ownerLocalId);
_lighting.UnregisterOwner(ownerLocalId);
}
public void Refresh() => _lighting.RefreshAttachedLights();
internal int TrackedOwnerCount => _serverGuidByOwner.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
|| _presentOwners.Contains(entity.Id))
{
return;
}
Register(serverGuid);
}
private void OnOwnerLightingChanged(uint ownerLocalId, bool enabled)
{
if (!_presentOwners.Contains(ownerLocalId)
|| !_serverGuidByOwner.TryGetValue(ownerLocalId, out uint serverGuid))
{
return;
}
if (!enabled)
{
_lighting.UnregisterOwner(ownerLocalId, forgetState: false);
return;
}
Register(serverGuid);
}
public void Dispose()
{
_lighting.OwnerLightingChanged -= OnOwnerLightingChanged;
_liveEntities.ProjectionVisibilityChanged -= OnProjectionVisibilityChanged;
foreach (uint ownerId in _serverGuidByOwner.Keys.ToArray())
Forget(ownerId);
}
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);
}
}