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); } }