using System; using System.Collections.Generic; using Robust.Client.Animations; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Utility; namespace Robust.Client.GameObjects { public sealed class AnimationPlayerSystem : EntitySystem { private readonly List> _activeAnimations = new(); private EntityQuery _playerQuery; private EntityQuery _metaQuery; [Dependency] private readonly IComponentFactory _compFact = default!; public override void Initialize() { base.Initialize(); _playerQuery = GetEntityQuery(); _metaQuery = GetEntityQuery(); } public override void FrameUpdate(float frameTime) { // TODO: Active or something idk. for (var i = 0; i < _activeAnimations.Count; i++) { var anim = _activeAnimations[i]; var uid = anim.Owner; if (!_metaQuery.TryGetComponent(uid, out var metadata) || metadata.EntityPaused) { continue; } if (!Update(uid, anim.Comp, frameTime)) { continue; } _activeAnimations.RemoveSwap(i); i--; anim.Comp.HasPlayingAnimation = false; } } internal void AddComponent(Entity ent) { if (ent.Comp.HasPlayingAnimation) return; _activeAnimations.Add(ent); ent.Comp.HasPlayingAnimation = true; } private bool Update(EntityUid uid, AnimationPlayerComponent component, float frameTime) { if (component.PlayingAnimationCount == 0 || component.Deleted) { return true; } var remie = new RemQueue(); foreach (var (key, playback) in component.PlayingAnimations) { var keep = AnimationPlaybackShared.UpdatePlayback(uid, playback, frameTime); if (!keep) { remie.Add(key); } } foreach (var key in remie) { component.PlayingAnimations.Remove(key); var completedEvent = new AnimationCompletedEvent {Uid = uid, Key = key, Finished = true}; EntityManager.EventBus.RaiseLocalEvent(uid, completedEvent, true); } return false; } /// /// Start playing an animation. /// public void Play(EntityUid uid, Animation animation, string key) { var component = EnsureComp(uid); Play(new Entity(uid, component), animation, key); } [Obsolete("Use Play(EntityUid ent, Animation animation, string key) instead")] public void Play(EntityUid uid, AnimationPlayerComponent? component, Animation animation, string key) { component ??= EntityManager.EnsureComponent(uid); Play(new Entity(uid, component), animation, key); } public void Play(Entity ent, Animation animation, string key) { AddComponent(ent); var playback = new AnimationPlaybackShared.AnimationPlayback(animation); #if DEBUG // Component networking checks foreach (var track in animation.AnimationTracks) { if (track is not AnimationTrackComponentProperty compTrack) continue; if (compTrack.ComponentType == null) { Log.Error("Attempted to play a component animation without any component specified."); return; } if (!EntityManager.TryGetComponent(ent, compTrack.ComponentType, out var animatedComp)) { Log.Error( $"Attempted to play a component animation, but the entity {ToPrettyString(ent)} does not have the component to be animated: {compTrack.ComponentType}."); return; } if (IsClientSide(ent) || !animatedComp.NetSyncEnabled) continue; var reg = _compFact.GetRegistration(animatedComp); // In principle there is nothing wrong with this, as long as the property of the component being // animated is not part of the networked state and setting it does not dirty the component. Hence only a // warning in debug mode. if (reg.NetID != null && compTrack.Property != null) { if (animatedComp.GetType().GetProperty(compTrack.Property) is { } property && property.HasCustomAttribute()) { Log.Warning($"Playing a component animation on a networked component {reg.Name} belonging to {ToPrettyString(ent)}"); } } } #endif foreach (var track in animation.AnimationTracks) { if (track is not AnimationTrackSpriteFlick) continue; track.AdvancePlayback(ent.Owner, 0, 0, 0f); } ent.Comp.PlayingAnimations.Add(key, playback); } public bool HasRunningAnimation(EntityUid uid, string key) { return EntityManager.TryGetComponent(uid, out AnimationPlayerComponent? component) && component.PlayingAnimations.ContainsKey(key); } public bool HasRunningAnimation(EntityUid uid, AnimationPlayerComponent? component, string key) { if (component == null) TryComp(uid, out component); return component != null && component.PlayingAnimations.ContainsKey(key); } public bool HasRunningAnimation(AnimationPlayerComponent component, string key) { return component.PlayingAnimations.ContainsKey(key); } [Obsolete] public void Stop(AnimationPlayerComponent component, string key) { Stop((component.Owner, component), key); } public void Stop(Entity entity, string key) { if (!_playerQuery.Resolve(entity.Owner, ref entity.Comp, false) || !entity.Comp.PlayingAnimations.Remove(key)) { return; } var completedEvent = new AnimationCompletedEvent {Uid = entity.Owner, Key = key, Finished = false}; EntityManager.EventBus.RaiseLocalEvent(entity.Owner, completedEvent, true); } public void Stop(EntityUid uid, AnimationPlayerComponent? component, string key) { Stop((uid, component), key); } } /// /// Raised whenever an animation stops, either due to running its course or being stopped manually. /// public sealed class AnimationCompletedEvent : EntityEventArgs { public EntityUid Uid { get; init; } public string Key { get; init; } = string.Empty; /// /// If true, the animation finished by getting to its natural end. /// If false, it was removed prematurely via or similar overloads. /// public bool Finished { get; init; } } }