using System; using System.Collections.Generic; using JetBrains.Annotations; using Robust.Client.ComponentTrees; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; using Robust.Shared; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Physics; using Robust.Shared.Prototypes; using Robust.Shared.Timing; using static Robust.Client.GameObjects.SpriteComponent; namespace Robust.Client.GameObjects { /// /// Updates the layer animation for every visible sprite. /// [UsedImplicitly] public sealed partial class SpriteSystem : EntitySystem { [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly IResourceCache _resourceCache = default!; private readonly Queue _inertUpdateQueue = new(); /// /// Entities that require a sprite frame update. /// private readonly HashSet _queuedFrameUpdate = new(); internal void Render(EntityUid uid, SpriteComponent sprite, DrawingHandleWorld drawingHandle, Angle eyeRotation, in Angle worldRotation, in Vector2 worldPosition) { if (!sprite.IsInert) _queuedFrameUpdate.Add(uid); sprite.RenderInternal(drawingHandle, eyeRotation, worldRotation, worldPosition, sprite.EnableDirectionOverride ? sprite.DirectionOverride : null); } public override void Initialize() { base.Initialize(); UpdatesAfter.Add(typeof(SpriteTreeSystem)); _proto.PrototypesReloaded += OnPrototypesReloaded; SubscribeLocalEvent(QueueUpdateInert); SubscribeLocalEvent(OnInit); _cfg.OnValueChanged(CVars.RenderSpriteDirectionBias, OnBiasChanged, true); } private void OnInit(EntityUid uid, SpriteComponent component, ComponentInit args) { // I'm not 100% this is needed, but I CBF with this ATM. Somebody kill server sprite component please. QueueUpdateInert(uid, component); } public override void Shutdown() { base.Shutdown(); _proto.PrototypesReloaded -= OnPrototypesReloaded; _cfg.UnsubValueChanged(CVars.RenderSpriteDirectionBias, OnBiasChanged); } private void OnBiasChanged(double value) { SpriteComponent.DirectionBias = value; } private void QueueUpdateInert(EntityUid uid, SpriteComponent sprite, ref SpriteUpdateInertEvent ev) => QueueUpdateInert(uid, sprite); public void QueueUpdateInert(EntityUid uid, SpriteComponent sprite) { if (sprite._inertUpdateQueued) return; sprite._inertUpdateQueued = true; _inertUpdateQueue.Enqueue(sprite); } /// public override void FrameUpdate(float frameTime) { while (_inertUpdateQueue.TryDequeue(out var sprite)) { sprite.DoUpdateIsInert(); } var realtime = _timing.RealTime.TotalSeconds; var spriteQuery = GetEntityQuery(); var syncQuery = GetEntityQuery(); foreach (var uid in _queuedFrameUpdate) { if (!spriteQuery.TryGetComponent(uid, out var sprite)) continue; if (sprite.IsInert) continue; var sync = syncQuery.HasComponent(uid); foreach (var layer in sprite.Layers) { if (!layer.State.IsValid || !layer.Visible || !layer.AutoAnimated) continue; var rsi = layer.RSI ?? sprite.BaseRSI; if (rsi == null || !rsi.TryGetState(layer.State, out var state)) state = GetFallbackState(); if (!state.IsAnimated) continue; if (sync) { layer.AnimationTime = (float)(realtime % state.TotalDelay); layer.AnimationTimeLeft = -layer.AnimationTime; layer.AnimationFrame = 0; } else { layer.AnimationTime += frameTime; layer.AnimationTimeLeft -= frameTime; } layer.AdvanceFrameAnimation(state); } } _queuedFrameUpdate.Clear(); } /// /// Force update of the sprite component next frame /// public void ForceUpdate(EntityUid uid) { _queuedFrameUpdate.Add(uid); } } /// /// This event gets raised before a sprite gets drawn using it's post-shader. /// public sealed class BeforePostShaderRenderEvent : EntityEventArgs { public readonly SpriteComponent Sprite; public readonly IClydeViewport Viewport; public BeforePostShaderRenderEvent(SpriteComponent sprite, IClydeViewport viewport) { Sprite = sprite; Viewport = viewport; } } }