using AcDream.App.Physics; using AcDream.Core.Physics; namespace AcDream.App.World; /// /// Applies the presentation/collision side effects of accepted retail /// PhysicsState transitions after the canonical live owner is ready. /// Logical ownership stays in ; Hidden never /// unregisters scripts, particles, lights, meshes, or the live record. /// /// /// Ports CPhysicsObj::set_state (0x00514DD0), /// set_nodraw (0x0050FCA0), and set_hidden /// (0x00514C60). Typed script ids are retail /// PS_UnHide=0x75 and PS_Hidden=0x76 from acclient.h. /// public sealed class LiveEntityPresentationController : IDisposable { public const uint UnHideScriptType = 0x75u; public const uint HiddenScriptType = 0x76u; private readonly LiveEntityRuntime _liveEntities; private readonly ShadowObjectRegistry _shadows; private readonly Func _playTyped; private readonly Action _setDirectChildrenNoDraw; private readonly Action _clearInvalidTarget; private readonly Func<(int X, int Y)> _liveCenter; private readonly Action? _onShadowRestored; private readonly Dictionary _readyGenerationByGuid = new(); private readonly Dictionary _suspendedShadowGenerationByGuid = new(); private readonly Dictionary _activePlacementGenerationByGuid = new(); private bool _disposed; public LiveEntityPresentationController( LiveEntityRuntime liveEntities, ShadowObjectRegistry shadows, Func playTyped, Action? setDirectChildrenNoDraw = null, Action? clearInvalidTarget = null, Func<(int X, int Y)>? liveCenter = null, Action? onShadowRestored = null) { _liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities)); _shadows = shadows ?? throw new ArgumentNullException(nameof(shadows)); _playTyped = playTyped ?? throw new ArgumentNullException(nameof(playTyped)); _setDirectChildrenNoDraw = setDirectChildrenNoDraw ?? ((_, _) => { }); _clearInvalidTarget = clearInvalidTarget ?? (_ => { }); _liveCenter = liveCenter ?? (() => (0, 0)); _onShadowRestored = onShadowRestored; _liveEntities.ProjectionVisibilityChanged += OnProjectionVisibilityChanged; } /// /// Opens the state-side-effect barrier after render/effect owners have /// registered, then drains constructor and pre-materialization transitions /// exactly once in accepted order. /// public bool OnLiveEntityReady(uint serverGuid) { if (!_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record) || record.WorldEntity is null || !record.ResourcesRegistered) { return false; } _readyGenerationByGuid[serverGuid] = record.Generation; ApplyPendingTransitions(record); return true; } /// Drains a newly accepted SetState when this incarnation is ready. public bool OnStateAccepted(uint serverGuid) { if (!_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record) || !_readyGenerationByGuid.TryGetValue(serverGuid, out ushort generation) || generation != record.Generation) { return false; } ApplyPendingTransitions(record); return true; } /// /// Retains ownership of a collision shadow that another retail transition /// suspended while this incarnation's spatial projection is unavailable. /// Hidden/UnHide and teleport rollback intentionally converge on this one /// generation-scoped restoration marker. /// public bool DeferShadowRestore(uint serverGuid, ushort generation) => CompleteAuthoritativePlacement(serverGuid, generation, deferShadowRestore: true); /// /// Completes active placement ownership after its final frame/cell is /// known, either returning a stable deferred restore to presentation or /// clearing the suspension for an immediate controller-owned restore. /// public bool CompleteAuthoritativePlacement( uint serverGuid, ushort generation, bool deferShadowRestore) { if (!_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record) || record.Generation != generation || record.WorldEntity is null) { return false; } if (_activePlacementGenerationByGuid.TryGetValue( serverGuid, out ushort activeGeneration) && activeGeneration == generation) { _activePlacementGenerationByGuid.Remove(serverGuid); } if (deferShadowRestore) _suspendedShadowGenerationByGuid[serverGuid] = generation; else if (_suspendedShadowGenerationByGuid.TryGetValue( serverGuid, out ushort deferredGeneration) && deferredGeneration == generation) _suspendedShadowGenerationByGuid.Remove(serverGuid); return true; } /// /// Transfers a deferred restore to a newer authoritative placement before /// that placement rebuckets and can become visible. The placement hands /// ownership back after it reaches a stable Hidden destination/rollback; /// this prevents an intervening UnHide from restoring an unresolved pose. /// public bool BeginAuthoritativePlacement(uint serverGuid, ushort generation) { if (!_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record) || record.Generation != generation) { return false; } _activePlacementGenerationByGuid[serverGuid] = generation; if (_suspendedShadowGenerationByGuid.TryGetValue( serverGuid, out ushort deferredGeneration) && deferredGeneration == generation) { _suspendedShadowGenerationByGuid.Remove(serverGuid); } return true; } internal bool HasDeferredShadowRestore(uint serverGuid) => _suspendedShadowGenerationByGuid.ContainsKey(serverGuid); internal bool HasActivePlacement(uint serverGuid) => _activePlacementGenerationByGuid.ContainsKey(serverGuid); public void Forget(LiveEntityRecord record) { ArgumentNullException.ThrowIfNull(record); if (_readyGenerationByGuid.TryGetValue(record.ServerGuid, out ushort generation) && generation == record.Generation) { _readyGenerationByGuid.Remove(record.ServerGuid); } if (_suspendedShadowGenerationByGuid.TryGetValue( record.ServerGuid, out ushort suspendedGeneration) && suspendedGeneration == record.Generation) { _suspendedShadowGenerationByGuid.Remove(record.ServerGuid); } if (_activePlacementGenerationByGuid.TryGetValue( record.ServerGuid, out ushort activeGeneration) && activeGeneration == record.Generation) { _activePlacementGenerationByGuid.Remove(record.ServerGuid); } } public void Clear() { _readyGenerationByGuid.Clear(); _suspendedShadowGenerationByGuid.Clear(); _activePlacementGenerationByGuid.Clear(); } public void Dispose() { if (_disposed) return; _disposed = true; _liveEntities.ProjectionVisibilityChanged -= OnProjectionVisibilityChanged; Clear(); } private void ApplyPendingTransitions(LiveEntityRecord record) { while (record.TryDequeueStateTransition(out RetailPhysicsStateTransition transition)) { if (record.WorldEntity is not { } entity) return; // set_state writes the final state before any PartArray/cell side // effect. Retained shadow registrations must see those same bits // even while their cell rows are suspended. _shadows.UpdatePhysicsState(entity.Id, (uint)transition.FinalState); switch (transition.HiddenTransition) { case RetailHiddenTransition.BecameHidden: _playTyped(entity.Id, HiddenScriptType, 1f); _setDirectChildrenNoDraw(record.ServerGuid, true); _shadows.Suspend(entity.Id); if (!IsPlacementActive(record)) _suspendedShadowGenerationByGuid[record.ServerGuid] = record.Generation; _clearInvalidTarget(record.ServerGuid); break; case RetailHiddenTransition.BecameVisible: _playTyped(entity.Id, UnHideScriptType, 1f); _setDirectChildrenNoDraw(record.ServerGuid, false); if (!IsPlacementActive(record) && RestoreShadow(record, entity)) _suspendedShadowGenerationByGuid.Remove(record.ServerGuid); break; } } } private bool RestoreShadow(LiveEntityRecord record, AcDream.Core.World.WorldEntity entity) { if (!record.IsSpatiallyProjected || !record.IsSpatiallyVisible || record.FullCellId == 0) { return false; } (int centerX, int centerY) = _liveCenter(); ShadowPositionSynchronizer.Sync( _shadows, entity.Id, entity.Position, entity.Rotation, record.FullCellId, centerX, centerY); _onShadowRestored?.Invoke(record.ServerGuid); return true; } private void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible) { if (!visible || record.WorldEntity is not { } entity || (record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0 || IsPlacementActive(record) || !_readyGenerationByGuid.TryGetValue(record.ServerGuid, out ushort readyGeneration) || readyGeneration != record.Generation || !_suspendedShadowGenerationByGuid.TryGetValue( record.ServerGuid, out ushort suspendedGeneration) || suspendedGeneration != record.Generation) { return; } if (RestoreShadow(record, entity)) _suspendedShadowGenerationByGuid.Remove(record.ServerGuid); } private bool IsPlacementActive(LiveEntityRecord record) => _activePlacementGenerationByGuid.TryGetValue( record.ServerGuid, out ushort generation) && generation == record.Generation; }