using System; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using Robust.Client.ComponentTrees; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; using Robust.Client.Utility; using Robust.Shared; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.Graphics.RSI; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations; using Robust.Shared.Timing; using Robust.Shared.Utility; 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 IConfigurationManager _cfg = default!; [Dependency] private IEyeManager _eye = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private IResourceCache _resourceCache = default!; [Dependency] private IPrototypeManager _prototypes = default!; // Note that any new system dependencies have to be added to RobustUnitTest.BaseSetup() [Dependency] private SharedTransformSystem _xforms = default!; [Dependency] private SpriteTreeSystem _tree = default!; [Dependency] private AppearanceSystem _appearance = default!; private HashSet _postShaderIds = new(); public static readonly ProtoId UnshadedId = "unshaded"; private readonly Queue _inertUpdateQueue = new(); public static readonly ResPath TextureRoot = SpriteSpecifierSerializer.TextureRoot; /// /// Entities that require a sprite frame update. /// private readonly HashSet _queuedFrameUpdate = new(); private EntityQuery _query; public override void Initialize() { base.Initialize(); UpdatesAfter.Add(typeof(SpriteTreeSystem)); SubscribeLocalEvent(OnPrototypesReloaded); SubscribeLocalEvent(OnInit); Subs.CVar(_cfg, CVars.RenderSpriteDirectionBias, OnBiasChanged, true); _query = GetEntityQuery(); } public bool IsVisible(Layer layer) { return layer.Visible && layer.CopyToShaderParameters == null; } private void OnInit(EntityUid uid, SpriteComponent component, ComponentInit args) { try { for (var i = 0; i < component.PostShaders.Count; i++) { var postShader = component.PostShaders[i]; if (!_postShaderIds.Add(postShader.Id)) { Log.Error("Duplicate post-shader id '{0}'.", postShader.Id); component.PostShaders.RemoveAt(i--); continue; } postShader.InsertionIndex = i; if (postShader.Shader != null) { Log.Warning("Post-shader '{0}' already has a shader instance during component initialization.", postShader.Id); continue; } if (!_prototypes.TryIndex(postShader.Prototype, out var prototype)) { Log.Error("Shader prototype '{0}' does not exist.", postShader.Prototype); component.PostShaders.RemoveAt(i--); continue; } postShader.Shader = postShader.Mutable ? prototype.InstanceUnique() : prototype.Instance(); } } finally { _postShaderIds.Clear(); } component.PostShaderOrderDirty = component.PostShaders.Count > 1; // I'm not 100% this is needed, but I CBF with this ATM. Somebody kill server sprite component please. QueueUpdateIsInert((uid, component)); } private void OnBiasChanged(double value) { SpriteComponent.DirectionBias = value; } private void DoUpdateIsInert(SpriteComponent component) { component._inertUpdateQueued = false; component.IsInert = true; foreach (var layer in component.Layers) { // Since StateId is a struct, we can't null-check it directly. if (!layer.State.IsValid || !layer.Visible || !layer.AutoAnimated || layer.Blank) { continue; } var rsi = layer.RSI ?? component.BaseRSI; if (rsi == null || !rsi.TryGetState(layer.State, out var state)) { state = GetFallbackState(); } if (state.IsAnimated) { component.IsInert = false; break; } } } /// public override void FrameUpdate(float frameTime) { while (_inertUpdateQueue.TryDequeue(out var sprite)) { DoUpdateIsInert(sprite); } var realtime = _timing.RealTime.TotalSeconds; var spriteQuery = GetEntityQuery(); var syncQuery = GetEntityQuery(); var metaQuery = GetEntityQuery(); foreach (var uid in _queuedFrameUpdate) { if (!spriteQuery.TryGetComponent(uid, out var sprite) || metaQuery.GetComponent(uid).EntityPaused) { 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); } /// /// Gets the specified frame for this sprite at the specified time. /// /// Should we clamp on the last frame and not loop public Texture GetFrame(SpriteSpecifier spriteSpec, TimeSpan curTime, bool loop = true) { Texture? sprite = null; switch (spriteSpec) { case SpriteSpecifier.Rsi rsi: var rsiActual = _resourceCache.GetResource(rsi.RsiPath).RSI; rsiActual.TryGetState(rsi.RsiState, out var state); var frames = state!.GetFrames(RsiDirection.South); var delays = state.GetDelays(); var totalDelay = delays.Sum(); // No looping if (!loop && curTime.TotalSeconds >= totalDelay) { sprite = frames[^1]; } // Loopable else { var time = curTime.TotalSeconds % totalDelay; var delaySum = 0f; for (var i = 0; i < delays.Length; i++) { var delay = delays[i]; delaySum += delay; if (time > delaySum) continue; sprite = frames[i]; break; } } sprite ??= Frame0(spriteSpec); break; case SpriteSpecifier.Texture texture: sprite = GetTexture(texture); break; default: throw new NotImplementedException(); } return sprite; } } /// /// This event gets raised before a sprite gets drawn using a post-shader that requests it. /// [ByRefEvent] public readonly record struct BeforePostShaderRenderEvent( string Id, ShaderInstance Shader, SpriteComponent Sprite, IClydeViewport Viewport); }