using System; using System.Collections.Generic; using System.Linq; using Robust.Shared.GameObjects; using Robust.Shared.Maths; using Robust.Shared.Utility; namespace Robust.Client.GameObjects; public sealed partial class SpriteSystem { private readonly List _postShaderSortEntries = new(); private readonly List> _postShaderSortNodes = new(); private readonly Dictionary> _postShaderSortNodeById = new(); /// /// Resets the sprite's animated layers to align with a given time (in seconds). /// public void SetAutoAnimateSync(SpriteComponent sprite, double time) { foreach (var layer in sprite.AllLayers) { if (layer is not SpriteComponent.Layer spriteLayer) continue; SetAutoAnimateSync(sprite, spriteLayer, time); } } /// /// Resets the layer's animation to align with a given time (in seconds). /// public void SetAutoAnimateSync(SpriteComponent sprite, SpriteComponent.Layer layer, double time) { if (!layer.AutoAnimated) return; var rsi = layer.RSI ?? sprite.BaseRSI; if (rsi == null || !rsi.TryGetState(layer.State, out var state)) { state = GetFallbackState(); } if (!state.IsAnimated) { return; } layer.AnimationTimeLeft = (float) -(time % state.TotalDelay); layer.AnimationFrame = 0; } public void CopySprite(Entity source, Entity target) { if (!Resolve(source.Owner, ref source.Comp)) return; if (!Resolve(target.Owner, ref target.Comp)) return; target.Comp._baseRsi = source.Comp._baseRsi; target.Comp.rsi = source.Comp.rsi; target.Comp.layerDatums = new List(source.Comp.layerDatums); target.Comp._bounds = source.Comp._bounds; target.Comp._visible = source.Comp._visible; target.Comp.color = source.Comp.color; target.Comp.offset = source.Comp.offset; target.Comp.rotation = source.Comp.rotation; target.Comp.scale = source.Comp.scale; target.Comp.LocalMatrix = Matrix3Helpers.CreateTransform( in target.Comp.offset, in target.Comp.rotation, in target .Comp.scale); target.Comp.drawDepth = source.Comp.drawDepth; target.Comp.NoRotation = source.Comp.NoRotation; target.Comp.DirectionOverride = source.Comp.DirectionOverride; target.Comp.EnableDirectionOverride = source.Comp.EnableDirectionOverride; if (source.Comp.Layers.Count == 0 && source.Comp.layerDatums.Count != 0) { LoadPrototypeData(target!); QueueUpdateIsInert(target!); } else { target.Comp.Layers = new List(source.Comp.Layers.Count); foreach (var otherLayer in source.Comp.Layers) { var layer = new SpriteComponent.Layer(otherLayer, target.Comp); layer.Index = target.Comp.Layers.Count; layer.Owner = target!; target.Comp.Layers.Add(layer); } target.Comp.LayerMap = source.Comp.LayerMap.ShallowClone(); } target.Comp.IsInert = source.Comp.IsInert; target.Comp.PostShaders = new List(source.Comp.PostShaders.Count); foreach (var postShader in source.Comp.PostShaders) { target.Comp.PostShaders.Add(new SpriteComponent.PostShaderEntry(postShader)); } target.Comp.PostShaderOrderDirty = source.Comp.PostShaderOrderDirty; target.Comp.RenderOrder = source.Comp.RenderOrder; target.Comp.GranularLayersRendering = source.Comp.GranularLayersRendering; target.Comp.Loop = source.Comp.Loop; DirtyBounds(target!); _tree.QueueTreeUpdate(target!); } /// /// Adds a sprite to a queue that will update next frame. /// public void QueueUpdateIsInert(Entity sprite) { if (sprite.Comp._inertUpdateQueued) return; sprite.Comp._inertUpdateQueued = true; _inertUpdateQueue.Enqueue(sprite); } [Obsolete("Use QueueUpdateIsInert")] public void QueueUpdateInert(EntityUid uid, SpriteComponent sprite) => QueueUpdateIsInert(new (uid, sprite)); /// /// Gets the sprite's ordered post-shader list, resolving and sorting it if required. /// public IReadOnlyList GetPostShaders(Entity sprite) { if (!_query.Resolve(sprite.Owner, ref sprite.Comp, false)) return Array.Empty(); return GetPostShaders(sprite.Comp); } /// /// Gets the sprite's ordered post-shader list, sorting it if required. /// public IReadOnlyList GetPostShaders(SpriteComponent sprite) { // Keep sorting lazy so repeatedly setting multiple shader properties during one update only pays once. if (sprite.PostShaderOrderDirty) SortPostShaders(sprite); return sprite.PostShaders; } /// /// Returns whether the sprite has a post-shader with the given id. /// public bool HasPostShader(Entity sprite, string id) { if (!_query.Resolve(sprite.Owner, ref sprite.Comp, false)) return false; return HasPostShader(sprite.Comp, id); } /// /// Returns whether the sprite has a post-shader with the given id. /// public bool HasPostShader(SpriteComponent sprite, string id) { // Could use sorted-dictionary but not expected to have a lot of these so this should be cheaper. foreach (var shader in sprite.PostShaders) { if (shader.Id == id) return true; } return false; } /// /// Tries to get a post-shader by id. /// public bool TryGetPostShader(Entity sprite, string id, out SpriteComponent.PostShaderEntry entry) { if (!_query.Resolve(sprite.Owner, ref sprite.Comp, false)) { entry = default!; return false; } return TryGetPostShader(sprite.Comp, id, out entry); } /// /// Tries to get a post-shader by id. /// public bool TryGetPostShader(SpriteComponent sprite, string id, out SpriteComponent.PostShaderEntry entry) { foreach (var shader in sprite.PostShaders) { if (shader.Id != id) continue; entry = shader; return true; } entry = default!; return false; } /// /// Adds or replaces a post-shader on the sprite. /// public void SetPostShader( Entity sprite, SpriteComponent.PostShaderArgs args) { if (!_query.Resolve(sprite.Owner, ref sprite.Comp, false)) return; SetPostShader(sprite.Comp, args); } /// /// Adds or replaces a post-shader on the sprite. /// public void SetPostShader( SpriteComponent sprite, SpriteComponent.PostShaderArgs args) { var beforeArray = ToDependencyArray(args.Before); var afterArray = ToDependencyArray(args.After); // IDs are stable handles for systems to replace/remove their own post-shader without disturbing others. for (var i = 0; i < sprite.PostShaders.Count; i++) { var existing = sprite.PostShaders[i]; if (existing.Id != args.Id) continue; existing.Prototype = default!; existing.Shader = args.Shader; existing.Mutable = args.Shader.Mutable; existing.GetScreenTexture = args.GetScreenTexture; existing.RaiseShaderEvent = args.RaiseShaderEvent; existing.Before = beforeArray; existing.After = afterArray; sprite.PostShaderOrderDirty = true; return; } sprite.PostShaders.Add(new SpriteComponent.PostShaderEntry( args.Id, args.Shader, args.GetScreenTexture, args.RaiseShaderEvent, beforeArray, afterArray) { InsertionIndex = sprite.PostShaders.Count, }); sprite.PostShaderOrderDirty = true; } /// /// Removes a post-shader from the sprite by id. /// public void RemovePostShader(Entity sprite, string id) { if (!_query.Resolve(sprite.Owner, ref sprite.Comp, false)) return; RemovePostShader(sprite.Comp, id); } /// /// Removes a post-shader from the sprite by id. /// public void RemovePostShader(SpriteComponent sprite, string id) { for (var i = 0; i < sprite.PostShaders.Count; i++) { if (sprite.PostShaders[i].Id != id) continue; sprite.PostShaders.RemoveAt(i); sprite.PostShaderOrderDirty = true; return; } } /// /// Removes all post-shaders from the sprite. /// public void ClearPostShaders(Entity sprite) { if (!_query.Resolve(sprite.Owner, ref sprite.Comp, false)) return; ClearPostShaders(sprite.Comp); } /// /// Removes all post-shaders from the sprite. /// public void ClearPostShaders(SpriteComponent sprite) { sprite.PostShaders.Clear(); sprite.PostShaderOrderDirty = false; } private void SortPostShaders(SpriteComponent sprite) { sprite.PostShaderOrderDirty = false; var shaders = sprite.PostShaders; if (shaders.Count < 2) return; _postShaderSortEntries.Clear(); _postShaderSortNodes.Clear(); _postShaderSortNodeById.Clear(); foreach (var shader in shaders) { _postShaderSortEntries.Add(shader); } _postShaderSortEntries.Sort(PostShaderInsertionComparison); foreach (var shader in _postShaderSortEntries) { var node = new TopologicalSort.GraphNode(shader); _postShaderSortNodes.Add(node); _postShaderSortNodeById[shader.Id] = node; } foreach (var node in _postShaderSortNodes) { foreach (var before in node.Value.Before) { if (_postShaderSortNodeById.TryGetValue(before, out var dependency)) AddPostShaderDependency(node, dependency); } foreach (var after in node.Value.After) { if (_postShaderSortNodeById.TryGetValue(after, out var dependency)) AddPostShaderDependency(dependency, node); } } try { var sorted = TopologicalSort.Sort(_postShaderSortNodes).ToArray(); shaders.Clear(); foreach (var shader in sorted) { shaders.Add(shader); } } catch (InvalidOperationException) { Log.Warning("Post-shader dependency cycle detected on sprite {0}; keeping insertion order.", sprite.Owner); shaders.Sort(PostShaderInsertionComparison); } } private static void AddPostShaderDependency( TopologicalSort.GraphNode before, TopologicalSort.GraphNode after) { if (before == after || before.Dependant.Contains(after)) return; before.Dependant.Add(after); } private static int PostShaderInsertionComparison(SpriteComponent.PostShaderEntry x, SpriteComponent.PostShaderEntry y) { var cmp = x.InsertionIndex.CompareTo(y.InsertionIndex); return cmp != 0 ? cmp : string.CompareOrdinal(x.Id, y.Id); } private static string[] ToDependencyArray(IEnumerable? dependencies) { if (dependencies == null) return Array.Empty(); if (dependencies is string[] array) return array; return new List(dependencies).ToArray(); } }