diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 93b7361a5db..ce040842009 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -47,7 +47,7 @@ END TEMPLATE--> ### Other -*None yet* +* Several SpriteComponent methods have been marked as obsolete, and should be replaced with new methods in SpriteSystem. ### Internal diff --git a/Robust.Client/ComponentTrees/SpriteTreeSystem.cs b/Robust.Client/ComponentTrees/SpriteTreeSystem.cs index 9a434bda100..eafbb23f545 100644 --- a/Robust.Client/ComponentTrees/SpriteTreeSystem.cs +++ b/Robust.Client/ComponentTrees/SpriteTreeSystem.cs @@ -2,6 +2,7 @@ using Robust.Client.GameObjects; using Robust.Shared.ComponentTrees; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; using Robust.Shared.Maths; using Robust.Shared.Physics; @@ -9,25 +10,7 @@ namespace Robust.Client.ComponentTrees; public sealed class SpriteTreeSystem : ComponentTreeSystem { - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnQueueUpdate); - } - - private void OnQueueUpdate(EntityUid uid, SpriteComponent component, ref QueueSpriteTreeUpdateEvent args) - => QueueTreeUpdate(uid, component, args.Xform); - - // TODO remove this when finally ECSing sprite components - [ByRefEvent] - internal readonly struct QueueSpriteTreeUpdateEvent - { - public readonly TransformComponent Xform; - public QueueSpriteTreeUpdateEvent(TransformComponent xform) - { - Xform = xform; - } - } + [Dependency] private readonly SpriteSystem _sprite = default!; #region Component Tree Overrides protected override bool DoFrameUpdate => true; @@ -36,6 +19,11 @@ public QueueSpriteTreeUpdateEvent(TransformComponent xform) protected override int InitialCapacity => 1024; protected override Box2 ExtractAabb(in ComponentTreeEntry entry, Vector2 pos, Angle rot) - => entry.Component.CalculateRotatedBoundingBox(pos, rot, default).CalcBoundingBox(); + { + // TODO SPRITE optimize this + // Because the just take the BB of the rotated BB, I'mt pretty sure we do a lot of unnecessary maths. + return _sprite.CalculateBounds((entry.Uid, entry.Component), pos, rot, default).CalcBoundingBox(); + } + #endregion } diff --git a/Robust.Client/GameObjects/Components/Renderable/IRenderableComponent.cs b/Robust.Client/GameObjects/Components/Renderable/IRenderableComponent.cs index 1f863359df0..65e1c2a03eb 100644 --- a/Robust.Client/GameObjects/Components/Renderable/IRenderableComponent.cs +++ b/Robust.Client/GameObjects/Components/Renderable/IRenderableComponent.cs @@ -1,9 +1,11 @@ -using Robust.Shared.GameObjects; +using System; +using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Maths; namespace Robust.Client.GameObjects { + [Obsolete] public partial interface IRenderableComponent : IComponent { int DrawDepth { get; set; } diff --git a/Robust.Client/GameObjects/Components/Renderable/ISpriteLayer.cs b/Robust.Client/GameObjects/Components/Renderable/ISpriteLayer.cs index a2d5134a5a0..9f8d0dae618 100644 --- a/Robust.Client/GameObjects/Components/Renderable/ISpriteLayer.cs +++ b/Robust.Client/GameObjects/Components/Renderable/ISpriteLayer.cs @@ -1,6 +1,5 @@ using System.Numerics; using Robust.Client.Graphics; -using Robust.Shared.Graphics; using Robust.Shared.Graphics.RSI; using Robust.Shared.Maths; diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs index a4bdcae91b7..ddbe6904258 100644 --- a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs +++ b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs @@ -5,10 +5,12 @@ using System.Linq; using System.Numerics; using System.Text; +using Robust.Client.ComponentTrees; using Robust.Client.Graphics; using Robust.Client.Graphics.Clyde; using Robust.Client.ResourceManagement; using Robust.Client.Utility; +using Robust.Shared; using Robust.Shared.Animations; using Robust.Shared.ComponentTrees; using Robust.Shared.GameObjects; @@ -25,23 +27,24 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; -using static Robust.Client.ComponentTrees.SpriteTreeSystem; using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth; using static Robust.Shared.Serialization.TypeSerializers.Implementations.SpriteSpecifierSerializer; using Direction = Robust.Shared.Maths.Direction; using Vector4 = Robust.Shared.Maths.Vector4; +#pragma warning disable CS0618 // Type or member is obsolete namespace Robust.Client.GameObjects { [RegisterComponent] public sealed partial class SpriteComponent : Component, IComponentDebug, ISerializationHooks, IComponentTreeEntry, IAnimationProperties { + #region ECSd + public const string LogCategory = "go.comp.sprite"; + [Dependency] private readonly IResourceCache resourceCache = default!; [Dependency] private readonly IPrototypeManager prototypes = default!; - [Dependency] private readonly IEntityManager entities = default!; + [Dependency] private readonly EntityManager entities = default!; [Dependency] private readonly IReflectionManager reflection = default!; - [Dependency] private readonly IEyeManager eyeManager = default!; - [Dependency] private readonly IComponentFactory factory = default!; /// /// See . @@ -52,11 +55,11 @@ public sealed partial class SpriteComponent : Component, IComponentDebug, ISeria /// Whether the layers have independant drawing strategies, e.g some may snap to cardinals while others won't. /// The sprite should still set its global rendering method (e.g NoRot or SnapCardinals), this only gives finer control over how layers are rendered internally. /// - [DataField("granularLayersRendering")] + [DataField] // TODO Sprite access restrict. public bool GranularLayersRendering = false; - [DataField("visible")] - private bool _visible = true; + [DataField] + internal bool _visible = true; // VV convenience variable to examine layer objects using layer keys [ViewVariables] @@ -66,17 +69,17 @@ public sealed partial class SpriteComponent : Component, IComponentDebug, ISeria public bool Visible { get => _visible; - set - { - if (_visible == value) return; - _visible = value; - - QueueUpdateRenderTree(); - } + [Obsolete("Use SpriteSystem.SetVisible() instead.")] + set => Sys.SetVisible((Owner, this), value); } + private SpriteSystem? _sys; + private SpriteTreeSystem? _treeSys; + private SpriteSystem Sys => _sys ??= (entities.Started ? entities.System() : null)!; + private SpriteTreeSystem TreeSys => _treeSys ??= (entities.Started ? entities.System() : null)!; + [DataField("drawdepth", customTypeSerializer: typeof(ConstantSerializer))] - private int drawDepth = DrawDepthTag.Default; + internal int drawDepth = DrawDepthTag.Default; /// /// Z-index for drawing. @@ -85,11 +88,12 @@ public bool Visible public int DrawDepth { get => drawDepth; - set => drawDepth = value; + [Obsolete("Use SpriteSystem.SetDrawDepth() instead.")] + set => Sys.SetDrawDepth((Owner, this), value); } - [DataField("scale")] - private Vector2 scale = Vector2.One; + [DataField] + internal Vector2 scale = Vector2.One; /// /// A scale applied to all layers. @@ -99,38 +103,24 @@ public int DrawDepth public Vector2 Scale { get => scale; - set - { - if (MathF.Abs(value.X) < 0.005f || MathF.Abs(value.Y) < 0.005f) - { - // Scales of ~0.0025 or lower can lead to singular matrices due to rounding errors. - Logger.Error($"Attempted to set layer sprite scale to very small values. Entity: {entities.ToPrettyString(Owner)}. Scale: {value}"); - return; - } - - _bounds = _bounds.Scale(value / scale); - scale = value; - UpdateLocalMatrix(); - } + [Obsolete("Use SpriteSystem.SetScale() instead.")] + set => Sys.SetScale((Owner, this), value); } - [DataField("rotation")] - private Angle rotation = Angle.Zero; + [DataField] + internal Angle rotation = Angle.Zero; [Animatable] [ViewVariables(VVAccess.ReadWrite)] public Angle Rotation { get => rotation; - set - { - rotation = value; - UpdateLocalMatrix(); - } + [Obsolete("Use SpriteSystem.SetRotation() instead.")] + set => Sys.SetRotation((Owner, this), value); } - [DataField("offset")] - private Vector2 offset = Vector2.Zero; + [DataField] + internal Vector2 offset = Vector2.Zero; /// /// Offset applied to all layers. @@ -140,26 +130,24 @@ public Angle Rotation public Vector2 Offset { get => offset; - set - { - offset = value; - UpdateLocalMatrix(); - } + [Obsolete("Use SpriteSystem.SetOffset() instead.")] + set => Sys.SetOffset((Owner, this), value); } - [DataField("color")] - private Color color = Color.White; - - public Matrix3x2 LocalMatrix = Matrix3x2.Identity; + [DataField] + internal Color color = Color.White; [Animatable] [ViewVariables(VVAccess.ReadWrite)] public Color Color { get => color; - set => color = value; + [Obsolete("Use SpriteSystem.SetColor() instead.")] + set => Sys.SetColor((Owner, this), value); } + public Matrix3x2 LocalMatrix = Matrix3x2.Identity; + [ViewVariables] public DynamicTree>? Tree { get; set; } @@ -169,51 +157,21 @@ public Color Color public bool TreeUpdateQueued { get; set; } - private RSI? _baseRsi; + internal RSI? _baseRsi; [ViewVariables(VVAccess.ReadWrite)] public RSI? BaseRSI { get => _baseRsi; - set - { - if (value == _baseRsi) - return; - - _baseRsi = value; - if (value == null) - return; - - for (var i = 0; i < Layers.Count; i++) - { - var layer = Layers[i]; - if (!layer.State.IsValid || layer.RSI != null) - { - continue; - } - - layer.UpdateActualState(); - - if (value.TryGetState(layer.State, out var state)) - { - layer.AnimationTimeLeft = state.GetDelay(0); - } - else - { - Logger.ErrorS(LogCategory, - "Layer '{0}'no longer has state '{1}' due to base RSI change. Trace:\n{2}", - i, layer.State, Environment.StackTrace); - layer.Texture = null; - } - } - } + [Obsolete("Use SpriteSystem.SetBaseRSI() instead.")] + set => Sys.SetBaseRsi((Owner, this), value); } [DataField("sprite", readOnly: true)] private string? rsi; [DataField("layers", readOnly: true)] private List layerDatums = new(); - [DataField("state", readOnly: true)] private string? state; - [DataField("texture", readOnly: true)] private string? texture; + [DataField(readOnly: true)] private string? state; + [DataField(readOnly: true)] private string? texture; /// /// Should this entity show up in containers regardless of whether the container can show contents? @@ -226,23 +184,21 @@ public RSI? BaseRSI public bool ContainerOccluded { get => _containerOccluded && !OverrideContainerOcclusion; - set - { - if (_containerOccluded == value) return; - _containerOccluded = value; - QueueUpdateRenderTree(); - } + [Obsolete("Use SpriteSystem.SetContainerOccluded() instead.")] + set => Sys.SetContainerOccluded((Owner, this), value); } - private bool _containerOccluded; - - private Box2 _bounds; + internal bool _containerOccluded; /// - /// The bounds of the sprite. This does factor in the sprite's but not the - /// and + /// Whether or not the sprite's local bounding box is dirty and need to be rebuilt. /// - public Box2 Bounds => _bounds; + internal bool BoundsDirty = true; + + internal Box2 _bounds; + + [Obsolete("Use SpriteSystem.GetLocalBounds() instead.")] + public Box2 Bounds => Sys.GetLocalBounds((Owner, this)); [ViewVariables(VVAccess.ReadWrite)] internal bool _inertUpdateQueued; @@ -250,61 +206,57 @@ public bool ContainerOccluded /// Shader instance to use when drawing the final sprite to the world. /// [ViewVariables(VVAccess.ReadWrite)] - public ShaderInstance? PostShader { get; set; } + public ShaderInstance? PostShader + { + get; + // This will get obsoleted, but I only want to mark it as obsolete when multi-shader support is added, so + // that people can use the appropriate method and don't migrate to an incorrect new method that wont + // be obsoleted. + set; + } /// - /// Whether or not to pass the screen texture to the . + /// Whether to pass the screen texture to the . /// /// /// Should be false unless you really need it. /// - [DataField("getScreenTexture")] - [ViewVariables(VVAccess.ReadWrite)] - private bool _getScreenTexture = false; - public bool GetScreenTexture - { - get => _getScreenTexture && PostShader != null; - set => _getScreenTexture = value; - } + [DataField] + public bool GetScreenTexture; /// /// If true, this raise a entity system event before rendering this sprite, allowing systems to modify the /// shader parameters. Usually this can just be done via a frame-update, but some shaders require /// information about the viewport / eye. /// - [DataField("raiseShaderEvent")] - [ViewVariables(VVAccess.ReadWrite)] - public bool RaiseShaderEvent = false; + [DataField] + public bool RaiseShaderEvent; - [ViewVariables] private Dictionary LayerMap = new(); - [ViewVariables] private bool _layerMapShared; + [ViewVariables] internal Dictionary LayerMap { get; set; } = new(); [ViewVariables] internal List Layers = new(); [ViewVariables(VVAccess.ReadWrite)] public uint RenderOrder { get; set; } - public const string LogCategory = "go.comp.sprite"; - [ViewVariables(VVAccess.ReadWrite)] public bool IsInert { get; internal set; } + public ISpriteLayer this[int layer] => Layers[layer]; + public ISpriteLayer this[Index layer] => Layers[layer]; + public ISpriteLayer this[object layerKey] => this[LayerMap[layerKey]]; + public IEnumerable AllLayers => Layers; + void ISerializationHooks.AfterDeserialization() { // Please somebody burn this to the ground. There is so much spaghetti. + // Why has no one answered my prayers. IoCManager.InjectDependencies(this); - + if (!string.IsNullOrWhiteSpace(rsi)) { - if (!string.IsNullOrWhiteSpace(rsi)) - { - var rsiPath = TextureRoot / rsi; - if(resourceCache.TryGetResource(rsiPath, out RSIResource? resource)) - { - BaseRSI = resource.RSI; - } - else - { - Logger.ErrorS(LogCategory, "Unable to load RSI '{0}'.", rsiPath); - } - } + var rsiPath = TextureRoot / rsi; + if (resourceCache.TryGetResource(rsiPath, out RSIResource? resource)) + _baseRsi = resource.RSI; + else + Logger.ErrorS(LogCategory, "Unable to load RSI '{0}'.", rsiPath); } if (layerDatums.Count == 0) @@ -332,16 +284,15 @@ void ISerializationHooks.AfterDeserialization() Layers.Clear(); foreach (var datum in layerDatums) { - AddLayer(datum); + var layer = new Layer((Owner, this), Layers.Count); + Layers.Add(layer); + LayerSetData(layer, datum); } - _layerMapShared = true; - - QueueUpdateRenderTree(); - QueueUpdateIsInert(); } - UpdateLocalMatrix(); + BoundsDirty = true; + LocalMatrix = Matrix3Helpers.CreateTransform(in offset, in rotation, in scale); } /// @@ -349,55 +300,19 @@ void ISerializationHooks.AfterDeserialization() /// this is called. Does not keep them perpetually in sync. /// This does some deep copying thus exerts some gc pressure, so avoid this for hot code paths. /// + [Obsolete("Use SpriteSystem.CopySprite() instead.")] public void CopyFrom(SpriteComponent other) { - //deep copying things to avoid entanglement - _baseRsi = other._baseRsi; - _bounds = other._bounds; - _visible = other._visible; - _layerMapShared = other._layerMapShared; - color = other.color; - offset = other.offset; - rotation = other.rotation; - scale = other.scale; - UpdateLocalMatrix(); - drawDepth = other.drawDepth; - _screenLock = other._screenLock; - DirectionOverride = other.DirectionOverride; - EnableDirectionOverride = other.EnableDirectionOverride; - Layers = new List(other.Layers.Count); - foreach (var otherLayer in other.Layers) - { - Layers.Add(new Layer(otherLayer, this)); - } - IsInert = other.IsInert; - LayerMap = other.LayerMap.ToDictionary(entry => entry.Key, - entry => entry.Value); - if (other.PostShader != null) - { - // only need to copy the shader if it's mutable - PostShader = other.PostShader.Mutable ? other.PostShader.Duplicate() : other.PostShader; - } - else - { - PostShader = null; - } - - RenderOrder = other.RenderOrder; - GranularLayersRendering = other.GranularLayersRendering; - } - - internal void UpdateLocalMatrix() - { - LocalMatrix = Matrix3Helpers.CreateTransform(in offset, in rotation, in scale); + Sys.CopySprite((other.Owner, other), (Owner, this)); } + [Obsolete("Use LocalMatrix")] public Matrix3x2 GetLocalMatrix() { return LocalMatrix; } - /// + [Obsolete("Use SpriteSystem.LayerMapSet() instead.")] public void LayerMapSet(object key, int layer) { if (layer < 0 || layer >= Layers.Count) @@ -405,24 +320,22 @@ public void LayerMapSet(object key, int layer) throw new ArgumentOutOfRangeException(); } - _layerMapEnsurePrivate(); LayerMap.Add(key, layer); } - /// + [Obsolete("Use SpriteSystem.LayerMapRemove() instead.")] public void LayerMapRemove(object key) { - _layerMapEnsurePrivate(); LayerMap.Remove(key); } - /// + [Obsolete("Use SpriteSystem.LayerMapGet() instead.")] public int LayerMapGet(object key) { return LayerMap[key]; } - /// + [Obsolete("Use SpriteSystem.LayerMapTryGet() instead.")] public bool LayerMapTryGet(object key, out int layer, bool logError = false) { var result = LayerMap.TryGetValue(key, out layer); @@ -436,38 +349,18 @@ public bool LayerMapTryGet(object key, out int layer, bool logError = false) return result; } + [Obsolete("Use SpriteSystem.TryGetLayer() instead.")] public bool TryGetLayer(int index, [NotNullWhen(true)] out Layer? layer, bool logError = false) - { - if (index < Layers.Count) - { - layer = Layers[index]; - return true; - } + => Sys.TryGetLayer((Owner, this), index, out layer, logError); - if (logError) - { - Logger.ErrorS(LogCategory, "{0} - Layer index '{1}' does not exist! Trace:\n{2}", - entities.ToPrettyString(Owner), index, Environment.StackTrace); - } + [Obsolete("Use SpriteSystem.LayerExists() instead.")] + public bool LayerExists(int layer, bool logError = true) + => Sys.LayerExists((Owner, this), layer); - layer = null; - return false; - } - - public bool LayerExists(int layer, bool logError = true) => TryGetLayer(layer, out _, logError); + [Obsolete("Use SpriteSystem.LayerExists() instead.")] public bool LayerExists(object key, bool logError = false) => LayerMapTryGet(key, out _, logError); - private void _layerMapEnsurePrivate() - { - if (!_layerMapShared) - { - return; - } - - LayerMap = LayerMap.ShallowClone(); - _layerMapShared = false; - } - + [Obsolete("Use SpriteSystem.LayerMapReserve() instead.")] public int LayerMapReserveBlank(object key) { if (LayerMapTryGet(key, out var index)) @@ -481,186 +374,79 @@ public int LayerMapReserveBlank(object key) return index; } + [Obsolete("Use SpriteSystem.AddBlankLayer() instead.")] public int AddBlankLayer(int? newIndex = null) - { - var layer = new Layer(this); - return AddLayer(layer, newIndex); - } + => Sys.AddBlankLayer((Owner, this), newIndex).Index; - /// - /// Add a new layer based on some . - /// + [Obsolete("Use SpriteSystem.AddLayer() instead.")] public int AddLayer(PrototypeLayerData layerDatum, int? newIndex = null) - { - var layer = new Layer(this); - - var index = AddLayer(layer, newIndex); - - LayerSetData(index, layerDatum); - return index; - } + => Sys.AddLayer((Owner, this), layerDatum, newIndex); + [Obsolete("Use SpriteSystem.AddTextureLayer() instead.")] public int AddLayer(string texturePath, int? newIndex = null) { return AddLayer(new ResPath(texturePath), newIndex); } + [Obsolete("Use SpriteSystem.AddTextureLayer() instead.")] public int AddLayer(ResPath texturePath, int? newIndex = null) - { - if (!resourceCache.TryGetResource(TextureRoot / texturePath, out var texture)) - { - if (texturePath.Extension == "rsi") - { - Logger.ErrorS(LogCategory, - "Expected texture but got rsi '{0}', did you mean 'sprite:' instead of 'texture:'?", - texturePath); - } - - Logger.ErrorS(LogCategory, "Unable to load texture '{0}'. Trace:\n{1}", texturePath, - Environment.StackTrace); - } - - return AddLayer(texture?.Texture, newIndex); - } + => Sys.AddTextureLayer((Owner, this), texturePath, newIndex); + [Obsolete("Use SpriteSystem.AddTextureLayer() instead.")] public int AddLayer(Texture? texture, int? newIndex = null) - { - var layer = new Layer(this) { Texture = texture }; - return AddLayer(layer, newIndex); - } + => Sys.AddTextureLayer((Owner, this), texture, newIndex); + [Obsolete("Use SpriteSystem.AddRsiLayer() instead.")] public int AddLayer(RSI.StateId stateId, int? newIndex = null) - { - var layer = new Layer(this) { State = stateId }; - if (BaseRSI != null && BaseRSI.TryGetState(stateId, out var state)) - { - layer.AnimationTimeLeft = state.GetDelay(0); - } - else - { - Logger.ErrorS(LogCategory, "State does not exist in RSI: '{0}'. Trace:\n{1}", stateId, - Environment.StackTrace); - } - - return AddLayer(layer, newIndex); - } + => Sys.AddRsiLayer((Owner, this), stateId, null, newIndex); + [Obsolete("Use SpriteSystem.AddRsiLayer() instead.")] public int AddLayerState(string stateId, int? newIndex = null) { return AddLayer(new RSI.StateId(stateId), newIndex); } + [Obsolete("Use SpriteSystem.AddRsiLayer() instead.")] public int AddLayer(RSI.StateId stateId, string rsiPath, int? newIndex = null) { return AddLayer(stateId, new ResPath(rsiPath), newIndex); } + [Obsolete("Use SpriteSystem.AddRsiLayer() instead.")] public int AddLayerState(string stateId, string rsiPath, int? newIndex = null) { return AddLayer(new RSI.StateId(stateId), rsiPath, newIndex); } + [Obsolete("Use SpriteSystem.AddRsiLayer() instead.")] public int AddLayer(RSI.StateId stateId, ResPath rsiPath, int? newIndex = null) - { - if (!resourceCache.TryGetResource(TextureRoot / rsiPath, out var res)) - { - Logger.ErrorS(LogCategory, "Unable to load RSI '{0}'. Trace:\n{1}", rsiPath, Environment.StackTrace); - } - - return AddLayer(stateId, res?.RSI, newIndex); - } + => Sys.AddRsiLayer((Owner, this), stateId, rsiPath, newIndex); + [Obsolete("Use SpriteSystem.AddRsiLayer() instead.")] public int AddLayerState(string stateId, ResPath rsiPath, int? newIndex = null) { return AddLayer(new RSI.StateId(stateId), rsiPath, newIndex); } + [Obsolete("Use SpriteSystem.AddRsiLayer() instead.")] public int AddLayer(RSI.StateId stateId, RSI? rsi, int? newIndex = null) - { - var layer = new Layer(this) { State = stateId, RSI = rsi }; - if (rsi != null && rsi.TryGetState(stateId, out var state)) - { - layer.AnimationTimeLeft = state.GetDelay(0); - } - else - { - Logger.ErrorS(LogCategory, "State does not exist in RSI: '{0}'. Trace:\n{1}", stateId, - Environment.StackTrace); - } - - return AddLayer(layer, newIndex); - } + => Sys.AddRsiLayer((Owner, this), stateId, rsi, newIndex); + [Obsolete("Use SpriteSystem.AddRsiLayer() instead.")] public int AddLayerState(string stateId, RSI rsi, int? newIndex = null) { return AddLayer(new RSI.StateId(stateId), rsi, newIndex); } + [Obsolete("Use SpriteSystem.AddLayer() instead.")] public int AddLayer(SpriteSpecifier specifier, int? newIndex = null) - { - switch (specifier) - { - case SpriteSpecifier.Texture tex: - return AddLayer(tex.TexturePath, newIndex); - - case SpriteSpecifier.Rsi rsi: - return AddLayerState(rsi.RsiState, rsi.RsiPath, newIndex); - - default: - throw new NotImplementedException(); - } - } - - private int AddLayer(Layer layer, int? newIndex) - { - int index; - if (newIndex.HasValue) - { - Layers.Insert(newIndex.Value, layer); - foreach (var kv in LayerMap) - { - if (kv.Value >= newIndex.Value) - { - LayerMap[kv.Key] = kv.Value + 1; - } - } - - index = newIndex.Value; - } - else - { - Layers.Add(layer); - index = Layers.Count - 1; - } - - RebuildBounds(); - QueueUpdateIsInert(); - return index; - } + => Sys.AddLayer((Owner, this), specifier, newIndex); + [Obsolete("Use SpriteSystem.RemoveLayer() instead.")] public void RemoveLayer(int layer) - { - if (!LayerExists(layer)) - return; - - Layers.RemoveAt(layer); - foreach (var kv in LayerMap) - { - if (kv.Value == layer) - { - LayerMap.Remove(kv.Key); - } - - else if (kv.Value > layer) - { - LayerMap[kv.Key] = kv.Value - 1; - } - } - - RebuildBounds(); - QueueUpdateIsInert(); - } + => Sys.RemoveLayer((Owner, this), layer); + [Obsolete("Use SpriteSystem.RemoveLayer() instead.")] public void RemoveLayer(object layerKey) { if (!LayerMapTryGet(layerKey, out var layer, true)) @@ -669,26 +455,47 @@ public void RemoveLayer(object layerKey) RemoveLayer(layer); } - private void RebuildBounds() - { - _bounds = new Box2(); - foreach (var layer in Layers) - { - if (!layer.Visible || layer.Blank) continue; + [DataField("snapCardinals")] + internal bool _snapCardinals = false; - _bounds = _bounds.Union(layer.CalculateBoundingBox()); - } - _bounds = _bounds.Scale(Scale); - QueueUpdateRenderTree(); + /// + /// If the sprite only has 1 direction should it snap at cardinals if rotated. + /// + [ViewVariables(VVAccess.ReadWrite)] + public bool SnapCardinals + { + get => _snapCardinals; + [Obsolete("Use SpriteSystem.SnapCardinals() instead.")] + set => Sys.SetSnapCardinals((Owner, this), value); } /// - /// Fills in a layer's values using some . + /// If true, the sprite will always be rendered as if its world rotation relative to the screen's eye is 0. + /// Note for 4- or 8- directional sprites, the relative rotation is still used to choose the RSI state. /// + /// + /// E.g., this is used to ensure that players/mobs are always standing upright, but the sprite will still change + /// based on the direction that a mob is looking in. + /// + [DataField("noRot")] + public bool NoRotation; + + // TODO SPRITE + // When refactoring, make this nullable and remove EnableDirectionOverride + [DataField("overrideDir")] + public Direction DirectionOverride = Direction.East; + + [DataField("enableOverrideDir")] + public bool EnableDirectionOverride; + + [Obsolete("Use SpriteSystem.LayerSetData() instead.")] public void LayerSetData(int index, PrototypeLayerData layerDatum) + => Sys.LayerSetData((Owner, this), index, layerDatum); + + [Obsolete("Use SpriteSystem.LayerSetData() instead.")] + internal void LayerSetData(Layer layer, PrototypeLayerData layerDatum) { - if (!TryGetLayer(index, out var layer)) - return; + DebugTools.AssertEqual(layer, layer.Owner.Comp.Layers[layer.Index]); if (!string.IsNullOrWhiteSpace(layerDatum.RsiPath)) { @@ -776,13 +583,12 @@ public void LayerSetData(int index, PrototypeLayerData layerDatum) if (LayerMap.TryGetValue(key, out var mappedIndex)) { - if (mappedIndex != index) + if (mappedIndex != layer.Index) Logger.ErrorS(LogCategory, "Duplicate layer map key definition: {0}", key); continue; } - _layerMapEnsurePrivate(); - LayerMap[key] = index; + LayerMap[key] = layer.Index; } } @@ -811,79 +617,35 @@ public void LayerSetData(int index, PrototypeLayerData layerDatum) layer.CopyToShaderParameters = null; } - RebuildBounds(); - } - - private object ParseKey(string keyString) - { - if (reflection.TryParseEnumReference(keyString, out var @enum)) - return @enum; - - return keyString; - } - - public void LayerSetData(object layerKey, PrototypeLayerData data) - { - if (!LayerMapTryGet(layerKey, out var layer, true)) - return; - - LayerSetData(layer, data); - } + BoundsDirty = true; + layer.BoundsDirty = true; + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract + if (Owner != EntityUid.Invalid) + TreeSys?.QueueTreeUpdate((Owner, this)); - public void LayerSetShader(int layer, ShaderInstance? shader, string? prototype = null) - { - if (!TryGetLayer(layer, out var theLayer, true)) - return; - - theLayer.Shader = shader; - theLayer.ShaderPrototype = prototype; - } - - public void LayerSetShader(object layerKey, ShaderInstance shader, string? prototype = null) - { - if (!LayerMapTryGet(layerKey, out var layer, true)) - return; - - LayerSetShader(layer, shader, prototype); - } - - public void LayerSetShader(int layer, string shaderName) - { - if (!prototypes.TryIndex(shaderName, out var prototype)) + object ParseKey(string keyString) { - Logger.ErrorS(LogCategory, "Shader prototype '{0}' does not exist. Trace:\n{1}", shaderName, - Environment.StackTrace); + if (reflection.TryParseEnumReference(keyString, out var @enum)) + return @enum; - LayerSetShader(layer, null, null); - return; + return keyString; } - - LayerSetShader(layer, prototype.Instance(), shaderName); } - public void LayerSetShader(object layerKey, string shaderName) + [Obsolete("Use SpriteSystem.LayerSetData() instead.")] + public void LayerSetData(object layerKey, PrototypeLayerData data) { if (!LayerMapTryGet(layerKey, out var layer, true)) return; - LayerSetShader(layer, shaderName); + LayerSetData(layer, data); } + [Obsolete("Use SpriteSystem.LayerSetSprite() instead.")] public void LayerSetSprite(int layer, SpriteSpecifier specifier) - { - switch (specifier) - { - case SpriteSpecifier.Texture tex: - LayerSetTexture(layer, tex.TexturePath); - break; - case SpriteSpecifier.Rsi rsi: - LayerSetState(layer, rsi.RsiState, rsi.RsiPath); - break; - default: - throw new NotImplementedException(); - } - } + => Sys.LayerSetSprite((Owner, this), layer, specifier); + [Obsolete("Use SpriteSystem.LayerSetSprite() instead.")] public void LayerSetSprite(object layerKey, SpriteSpecifier specifier) { if (!LayerMapTryGet(layerKey, out var layer, true)) @@ -892,16 +654,11 @@ public void LayerSetSprite(object layerKey, SpriteSpecifier specifier) LayerSetSprite(layer, specifier); } + [Obsolete("Use SpriteSystem.LayerSetTexture() instead.")] public void LayerSetTexture(int layer, Texture? texture) - { - if (!TryGetLayer(layer, out var theLayer, true)) - return; - theLayer.SetTexture(texture); - - QueueUpdateIsInert(); - RebuildBounds(); - } + => Sys.LayerSetTexture((Owner, this), layer, texture); + [Obsolete("Use SpriteSystem.LayerSetTexture() instead.")] public void LayerSetTexture(object layerKey, Texture texture) { if (!LayerMapTryGet(layerKey, out var layer, true)) @@ -910,34 +667,23 @@ public void LayerSetTexture(object layerKey, Texture texture) LayerSetTexture(layer, texture); } + [Obsolete("Use SpriteSystem.LayerSetTexture() instead.")] public void LayerSetTexture(int layer, string texturePath) { LayerSetTexture(layer, new ResPath(texturePath)); } + [Obsolete("Use SpriteSystem.LayerSetTexture() instead.")] public void LayerSetTexture(object layerKey, string texturePath) { LayerSetTexture(layerKey, new ResPath(texturePath)); } + [Obsolete("Use SpriteSystem.LayerSetTexture() instead.")] public void LayerSetTexture(int layer, ResPath texturePath) - { - if (!resourceCache.TryGetResource(TextureRoot / texturePath, out var texture)) - { - if (texturePath.Extension == "rsi") - { - Logger.ErrorS(LogCategory, - "Expected texture but got rsi '{0}', did you mean 'sprite:' instead of 'texture:'?", - texturePath); - } - - Logger.ErrorS(LogCategory, "Unable to load texture '{0}'. Trace:\n{1}", texturePath, - Environment.StackTrace); - } - - LayerSetTexture(layer, texture?.Texture); - } + => Sys.LayerSetTexture((Owner, this), layer, texturePath); + [Obsolete("Use SpriteSystem.LayerSetTexture() instead.")] public void LayerSetTexture(object layerKey, ResPath texturePath) { if (!LayerMapTryGet(layerKey, out var layer, true)) @@ -946,14 +692,11 @@ public void LayerSetTexture(object layerKey, ResPath texturePath) LayerSetTexture(layer, texturePath); } + [Obsolete("Use SpriteSystem.LayerSetRsiState() instead.")] public void LayerSetState(int layer, RSI.StateId stateId) - { - if (!TryGetLayer(layer, out var theLayer, true)) - return; - theLayer.SetState(stateId); - RebuildBounds(); - } + => Sys.LayerSetRsiState((Owner, this), layer, stateId); + [Obsolete("Use SpriteSystem.LayerSetRsiState() instead.")] public void LayerSetState(object layerKey, RSI.StateId stateId) { if (!LayerMapTryGet(layerKey, out var layer, true)) @@ -962,38 +705,11 @@ public void LayerSetState(object layerKey, RSI.StateId stateId) LayerSetState(layer, stateId); } - public void LayerSetState(int layer, RSI.StateId stateId, RSI? rsi) - { - if (!TryGetLayer(layer, out var theLayer, true)) - return; - theLayer.State = stateId; - theLayer.RSI = rsi; - var actualRsi = theLayer.RSI ?? BaseRSI; - if (actualRsi == null) - { - Logger.ErrorS(LogCategory, "No RSI to pull new state from! Trace:\n{0}", Environment.StackTrace); - theLayer.Texture = null; - } - else - { - if (actualRsi.TryGetState(stateId, out var state)) - { - theLayer.AnimationFrame = 0; - theLayer.AnimationTime = 0; - theLayer.AnimationTimeLeft = state.GetDelay(0); - } - else - { - Logger.ErrorS(LogCategory, "State '{0}' does not exist in RSI {1}. Trace:\n{2}", stateId, - actualRsi.Path, Environment.StackTrace); - theLayer.Texture = null; - } - } - - QueueUpdateIsInert(); - RebuildBounds(); - } - + [Obsolete("Use SpriteSystem.LayerSetRsi() instead.")] + public void LayerSetState(int layer, RSI.StateId stateId, RSI? rsi) + => Sys.LayerSetRsi((Owner, this), layer, rsi, stateId); + + [Obsolete("Use SpriteSystem.LayerSetRsi() instead.")] public void LayerSetState(object layerKey, RSI.StateId stateId, RSI rsi) { if (!LayerMapTryGet(layerKey, out var layer, true)) @@ -1002,26 +718,23 @@ public void LayerSetState(object layerKey, RSI.StateId stateId, RSI rsi) LayerSetState(layer, stateId, rsi); } + [Obsolete("Use SpriteSystem.LayerSetRsiState() instead.")] public void LayerSetState(int layer, RSI.StateId stateId, string rsiPath) { LayerSetState(layer, stateId, new ResPath(rsiPath)); } + [Obsolete("Use SpriteSystem.LayerSetRsi() instead.")] public void LayerSetState(object layerKey, RSI.StateId stateId, string rsiPath) { LayerSetState(layerKey, stateId, new ResPath(rsiPath)); } + [Obsolete("Use SpriteSystem.LayerSetRsi() instead.")] public void LayerSetState(int layer, RSI.StateId stateId, ResPath rsiPath) - { - if (!resourceCache.TryGetResource(TextureRoot / rsiPath, out var res)) - { - Logger.ErrorS(LogCategory, "Unable to load RSI '{0}'. Trace:\n{1}", rsiPath, Environment.StackTrace); - } - - LayerSetState(layer, stateId, res?.RSI); - } + => Sys.LayerSetRsi((Owner, this), layer, rsiPath, stateId); + [Obsolete("Use SpriteSystem.LayerSetRsi() instead.")] public void LayerSetState(object layerKey, RSI.StateId stateId, ResPath rsiPath) { if (!LayerMapTryGet(layerKey, out var layer, true)) @@ -1030,14 +743,11 @@ public void LayerSetState(object layerKey, RSI.StateId stateId, ResPath rsiPath) LayerSetState(layer, stateId, rsiPath); } + [Obsolete("Use SpriteSystem.LayerSetRsi() instead.")] public void LayerSetRSI(int layer, RSI? rsi) - { - if (!TryGetLayer(layer, out var theLayer, true)) - return; - theLayer.SetRsi(rsi); - RebuildBounds(); - } + => Sys.LayerSetRsi((Owner, this), layer, rsi); + [Obsolete("Use SpriteSystem.LayerSetRsi() instead.")] public void LayerSetRSI(object layerKey, RSI rsi) { if (!LayerMapTryGet(layerKey, out var layer, true)) @@ -1046,26 +756,23 @@ public void LayerSetRSI(object layerKey, RSI rsi) LayerSetRSI(layer, rsi); } + [Obsolete("Use SpriteSystem.LayerSetRsi() instead.")] public void LayerSetRSI(int layer, string rsiPath) { LayerSetRSI(layer, new ResPath(rsiPath)); } + [Obsolete("Use SpriteSystem.LayerSetRsi() instead.")] public void LayerSetRSI(object layerKey, string rsiPath) { LayerSetRSI(layerKey, new ResPath(rsiPath)); } + [Obsolete("Use SpriteSystem.LayerSetRsi() instead.")] public void LayerSetRSI(int layer, ResPath rsiPath) - { - if (!resourceCache.TryGetResource(TextureRoot / rsiPath, out var res)) - { - Logger.ErrorS(LogCategory, "Unable to load RSI '{0}'. Trace:\n{1}", rsiPath, Environment.StackTrace); - } - - LayerSetRSI(layer, res?.RSI); - } + => Sys.LayerSetRsi((Owner, this), layer, rsiPath); + [Obsolete("Use SpriteSystem.LayerSetRsi() instead.")] public void LayerSetRSI(object layerKey, ResPath rsiPath) { if (!LayerMapTryGet(layerKey, out var layer, true)) @@ -1074,14 +781,11 @@ public void LayerSetRSI(object layerKey, ResPath rsiPath) LayerSetRSI(layer, rsiPath); } + [Obsolete("Use SpriteSystem.LayerSetScale() instead.")] public void LayerSetScale(int layer, Vector2 scale) - { - if (!TryGetLayer(layer, out var theLayer, true)) - return; - theLayer.Scale = scale; - RebuildBounds(); - } + => Sys.LayerSetScale((Owner, this), layer, scale); + [Obsolete("Use SpriteSystem.LayerSetScale() instead.")] public void LayerSetScale(object layerKey, Vector2 scale) { if (!LayerMapTryGet(layerKey, out var layer, true)) @@ -1090,15 +794,11 @@ public void LayerSetScale(object layerKey, Vector2 scale) LayerSetScale(layer, scale); } - + [Obsolete("Use SpriteSystem.LayerSetRotation() instead.")] public void LayerSetRotation(int layer, Angle rotation) - { - if (!TryGetLayer(layer, out var theLayer, true)) - return; - theLayer.Rotation = rotation; - RebuildBounds(); - } + => Sys.LayerSetRotation((Owner, this), layer, rotation); + [Obsolete("Use SpriteSystem.LayerSetRotation() instead.")] public void LayerSetRotation(object layerKey, Angle rotation) { if (!LayerMapTryGet(layerKey, out var layer, true)) @@ -1107,14 +807,24 @@ public void LayerSetRotation(object layerKey, Angle rotation) LayerSetRotation(layer, rotation); } - public void LayerSetVisible(int layer, bool visible) + [Obsolete("Use SpriteSystem.LayerSetOffset() instead.")] + public void LayerSetOffset(int layer, Vector2 layerOffset) + => Sys.LayerSetOffset((Owner, this), layer, layerOffset); + + [Obsolete("Use SpriteSystem.LayerSetOffset() instead.")] + public void LayerSetOffset(object layerKey, Vector2 layerOffset) { - if (!TryGetLayer(layer, out var theLayer, true)) + if (!LayerMapTryGet(layerKey, out var layer, true)) return; - theLayer.Visible = visible; + LayerSetOffset(layer, layerOffset); } + [Obsolete("Use SpriteSystem.LayerSetVisible() instead.")] + public void LayerSetVisible(int layer, bool visible) + => Sys.LayerSetVisible((Owner, this), layer, visible); + + [Obsolete("Use SpriteSystem.LayerSetVisible() instead.")] public void LayerSetVisible(object layerKey, bool visible) { if (!LayerMapTryGet(layerKey, out var layer, true)) @@ -1123,16 +833,11 @@ public void LayerSetVisible(object layerKey, bool visible) LayerSetVisible(layer, visible); } + [Obsolete("Use SpriteSystem.LayerSetColor() instead.")] public void LayerSetColor(int layer, Color color) - { - if (!TryGetLayer(layer, out var theLayer, true)) - return; - - theLayer.Color = color; - - RebuildBounds(); - } + => Sys.LayerSetColor((Owner, this), layer, color); + [Obsolete("Use SpriteSystem.LayerSetColor() instead.")] public void LayerSetColor(object layerKey, Color color) { if (!LayerMapTryGet(layerKey, out var layer, true)) @@ -1141,16 +846,11 @@ public void LayerSetColor(object layerKey, Color color) LayerSetColor(layer, color); } + [Obsolete("Use SpriteSystem.LayerSetDirOffset() instead.")] public void LayerSetDirOffset(int layer, DirectionOffset offset) - { - if (!TryGetLayer(layer, out var theLayer, true)) - return; - - theLayer.DirOffset = offset; - - RebuildBounds(); - } + => Sys.LayerSetDirOffset((Owner, this), layer, offset); + [Obsolete("Use SpriteSystem.LayerSetDirOffset() instead.")] public void LayerSetDirOffset(object layerKey, DirectionOffset offset) { if (!LayerMapTryGet(layerKey, out var layer, true)) @@ -1159,14 +859,11 @@ public void LayerSetDirOffset(object layerKey, DirectionOffset offset) LayerSetDirOffset(layer, offset); } + [Obsolete("Use SpriteSystem.LayerSetAnimationTime() instead.")] public void LayerSetAnimationTime(int layer, float animationTime) - { - if (!TryGetLayer(layer, out var theLayer, true)) - return; - - theLayer.SetAnimationTime(animationTime); - } + => Sys.LayerSetAnimationTime((Owner, this), layer, animationTime); + [Obsolete("Use SpriteSystem.LayerSetAnimationTime() instead.")] public void LayerSetAnimationTime(object layerKey, float animationTime) { if (!LayerMapTryGet(layerKey, out var layer, true)) @@ -1175,14 +872,11 @@ public void LayerSetAnimationTime(object layerKey, float animationTime) LayerSetAnimationTime(layer, animationTime); } + [Obsolete("Use SpriteSystem.LayerSetAutoAnimated() instead.")] public void LayerSetAutoAnimated(int layer, bool autoAnimated) - { - if (!TryGetLayer(layer, out var theLayer, true)) - return; - - theLayer.AutoAnimated = autoAnimated; - } + => Sys.LayerSetAutoAnimated((Owner, this), layer, autoAnimated); + [Obsolete("Use SpriteSystem.LayerSetAutoAnimated() instead.")] public void LayerSetAutoAnimated(object layerKey, bool autoAnimated) { if (!LayerMapTryGet(layerKey, out var layer, true)) @@ -1191,32 +885,11 @@ public void LayerSetAutoAnimated(object layerKey, bool autoAnimated) LayerSetAutoAnimated(layer, autoAnimated); } - public void LayerSetOffset(int layer, Vector2 layerOffset) - { - if (!TryGetLayer(layer, out var theLayer, true)) - return; - - theLayer.Offset = layerOffset; - - RebuildBounds(); - } - - public void LayerSetOffset(object layerKey, Vector2 layerOffset) - { - if (!LayerMapTryGet(layerKey, out var layer, true)) - return; - - LayerSetOffset(layer, layerOffset); - } - + [Obsolete("Use SpriteSystem.LayerSetRenderingStrategy() instead.")] public void LayerSetRenderingStrategy(int layer, LayerRenderingStrategy renderingStrategy) - { - if (!TryGetLayer(layer, out var theLayer, true)) - return; - - theLayer.RenderingStrategy = renderingStrategy; - } + => Sys.LayerSetRenderingStrategy((Owner, this), layer, renderingStrategy); + [Obsolete("Use SpriteSystem.LayerSetRenderingStrategy() instead.")] public void LayerSetRenderingStrategy(object layerKey, LayerRenderingStrategy renderingStrategy) { if (!LayerMapTryGet(layerKey, out var layer, true)) @@ -1225,7 +898,7 @@ public void LayerSetRenderingStrategy(object layerKey, LayerRenderingStrategy re LayerSetRenderingStrategy(layer, renderingStrategy); } - /// + [Obsolete("Use SpriteSystem.LayerGetRsiState() instead.")] public RSI.StateId LayerGetState(int layer) { if (!TryGetLayer(layer, out var theLayer, true)) @@ -1234,123 +907,67 @@ public RSI.StateId LayerGetState(int layer) return theLayer.State; } + [Obsolete("Use SpriteSystem.LayerGetEffectiveRsi() instead.")] public RSI? LayerGetActualRSI(int layer) { return this[layer].ActualRsi; } + [Obsolete("Use SpriteSystem.LayerGetEffectiveRsi() instead.")] public RSI? LayerGetActualRSI(object layerKey) { return this[layerKey].ActualRsi; } - public ISpriteLayer this[int layer] => Layers[layer]; - public ISpriteLayer this[Index layer] => Layers[layer]; - public ISpriteLayer this[object layerKey] => this[LayerMap[layerKey]]; - public IEnumerable AllLayers => Layers; - - // Lobby SpriteView rendering path - public void Render(DrawingHandleWorld drawingHandle, Angle eyeRotation, Angle worldRotation, Direction? overrideDirection = null, Vector2 position = default) + public void LayerSetShader(int layer, ShaderInstance? shader, string? prototype = null) { - RenderInternal(drawingHandle, eyeRotation, worldRotation, position, overrideDirection); - } + if (!TryGetLayer(layer, out var theLayer, true)) + return; - [DataField("noRot")] private bool _screenLock = false; + theLayer.Shader = shader; + theLayer.ShaderPrototype = prototype; + } - /// - /// If the sprite only has 1 direction should it snap at cardinals if rotated. - /// - [ViewVariables(VVAccess.ReadWrite)] - public bool SnapCardinals + public void LayerSetShader(object layerKey, ShaderInstance shader, string? prototype = null) { - get => _snapCardinals; - set - { - if (value == _snapCardinals) - return; + if (!LayerMapTryGet(layerKey, out var layer, true)) + return; - _snapCardinals = value; - RebuildBounds(); - } + LayerSetShader(layer, shader, prototype); } - [DataField("snapCardinals")] - private bool _snapCardinals = false; - - [DataField("overrideDir")] - [ViewVariables(VVAccess.ReadWrite)] - public Direction DirectionOverride = Direction.East; - - [DataField("enableOverrideDir")] - [ViewVariables(VVAccess.ReadWrite)] - public bool EnableDirectionOverride; - - /// - [ViewVariables(VVAccess.ReadWrite)] - public bool NoRotation { get => _screenLock; set => _screenLock = value; } - - internal void RenderInternal(DrawingHandleWorld drawingHandle, Angle eyeRotation, Angle worldRotation, Vector2 worldPosition, Direction? overrideDirection) + public void LayerSetShader(int layer, string shaderName) { - var angle = worldRotation + eyeRotation; // angle on-screen. Used to decide the direction of 4/8 directional RSIs - angle = angle.Reduced().FlipPositive(); // Reduce the angles to fix math shenanigans - - var cardinal = Angle.Zero; - - // If we have a 1-directional sprite then snap it to try and always face it south if applicable. - if (!NoRotation && SnapCardinals) + if (!prototypes.TryIndex(shaderName, out var prototype)) { - cardinal = angle.GetCardinalDir().ToAngle(); + Logger.ErrorS(LogCategory, + "Shader prototype '{0}' does not exist. Trace:\n{1}", + shaderName, + Environment.StackTrace); + + LayerSetShader(layer, null, null); + return; } - // worldRotation + eyeRotation should be the angle of the entity on-screen. If no-rot is enabled this is just set to zero. - // However, at some point later the eye-matrix is applied separately, so we subtract -eye rotation for now: - var entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, NoRotation ? -eyeRotation : worldRotation - cardinal); + LayerSetShader(layer, prototype.Instance(), shaderName); + } - var transformSprite = Matrix3x2.Multiply(LocalMatrix, entityMatrix); + public void LayerSetShader(object layerKey, string shaderName) + { + if (!LayerMapTryGet(layerKey, out var layer, true)) + return; - if (GranularLayersRendering) - { - //Default rendering - entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, worldRotation); - var transformDefault = Matrix3x2.Multiply(LocalMatrix, entityMatrix); - //Snap to cardinals - entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, worldRotation - angle.GetCardinalDir().ToAngle()); - var transformSnap = Matrix3x2.Multiply(LocalMatrix, entityMatrix); - //No rotation - entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, -eyeRotation); - var transformNoRot = Matrix3x2.Multiply(LocalMatrix, entityMatrix); - - foreach (var layer in Layers) { - switch (layer.RenderingStrategy) - { - case LayerRenderingStrategy.NoRotation: - layer.Render(drawingHandle, ref transformNoRot, angle, overrideDirection); - break; - case LayerRenderingStrategy.SnapToCardinals: - layer.Render(drawingHandle, ref transformSnap, angle, overrideDirection); - break; - case LayerRenderingStrategy.Default: - layer.Render(drawingHandle, ref transformDefault, angle, overrideDirection); - break; - case LayerRenderingStrategy.UseSpriteStrategy: - layer.Render(drawingHandle, ref transformSprite, angle, overrideDirection); - break; - default: - Logger.Error($"Tried to render a layer with unknown rendering stragegy: {layer.RenderingStrategy}"); - break; - } - } - } + LayerSetShader(layer, shaderName); + } - else - { - foreach (var layer in Layers) - { - layer.Render(drawingHandle, ref transformSprite, angle, overrideDirection); - } - } + // Lobby SpriteView rendering path + [Obsolete("Use SpriteSystem.Render() instead.")] + public void Render(DrawingHandleWorld drawingHandle, Angle eyeRotation, Angle worldRotation, Direction? overrideDirection = null, Vector2 position = default) + { + Sys.RenderSprite((Owner, this), drawingHandle, eyeRotation, worldRotation, position, overrideDirection); } + [Obsolete("Use SpriteSystem.LayerGetDirectionCount() instead.")] public int GetLayerDirectionCount(ISpriteLayer layer) { if (!layer.RsiState.IsValid) @@ -1360,7 +977,7 @@ public int GetLayerDirectionCount(ISpriteLayer layer) var rsi = layer.Rsi ?? BaseRSI; if (rsi == null || !rsi.TryGetState(layer.RsiState, out var state)) { - state = GetFallbackState(resourceCache); + state = Sys.GetFallbackState(); } return state.RsiDirections switch @@ -1372,41 +989,11 @@ public int GetLayerDirectionCount(ISpriteLayer layer) }; } - private void QueueUpdateRenderTree() - { - if (TreeUpdateQueued || !Owner.IsValid()) - return; - - // TODO whenever sprite comp gets ECS'd , just make this a direct method call. - var ev = new QueueSpriteTreeUpdateEvent(entities.GetComponent(Owner)); - entities.EventBus.RaiseComponentEvent(Owner, this, ref ev); - } - - private void QueueUpdateIsInert() - { - if (_inertUpdateQueued || !Owner.IsValid()) - return; - - // TODO whenever sprite comp gets ECS'd , just make this a direct method call. - var ev = new SpriteUpdateInertEvent(); - entities.EventBus.RaiseComponentEvent(Owner, this, ref ev); - } - - [Obsolete("Use SpriteSystem instead.")] - internal static RSI.State GetFallbackState(IResourceCache cache) - { - var rsi = cache.GetResource("/Textures/error.rsi").RSI; - return rsi["error"]; - } - public string GetDebugString() { var builder = new StringBuilder(); - builder.AppendFormat( - "vis/depth/scl/rot/ofs/col/norot/override/dir: {0}/{1}/{2}/{3}/{4}/{5}/{6}/{8}/{7}\n", - Visible, DrawDepth, Scale, Rotation, Offset, - Color, NoRotation, entities.GetComponent(Owner).WorldRotation.ToRsiDirection(RsiDirectionType.Dir8), - DirectionOverride + builder.Append( + $"vis/depth/scl/rot/ofs/col/norot/override: {Visible}/{DrawDepth}/{Scale}/{Rotation}/{Offset}/{Color}/{NoRotation}/{DirectionOverride}/\n" ); foreach (var layer in Layers) @@ -1415,54 +1002,32 @@ public string GetDebugString() "shad/tex/rsi/state/ant/anf/scl/rot/vis/col/dofs/renderstrat: {0}/{1}/{2}/{3}/{4}/{5}/{6}/{7}/{8}/{9}/{10}/{11}\n", // These are references and don't include useful data for knowing where they came from, sadly. // "is one set" is better than nothing at least. - layer.Shader != null, layer.Texture != null, layer.RSI != null, + layer.Shader != null, + layer.Texture != null, + layer.RSI != null, layer.State, - layer.AnimationTimeLeft, layer.AnimationFrame, layer.Scale, layer.Rotation, layer.Visible, - layer.Color, layer.DirOffset, layer.RenderingStrategy + layer.AnimationTimeLeft, + layer.AnimationFrame, + layer.Scale, + layer.Rotation, + layer.Visible, + layer.Color, + layer.DirOffset, + layer.RenderingStrategy ); } return builder.ToString(); } - /// + [Obsolete("Use SpriteSystem.GetBoundingBox() instead.")] public Box2Rotated CalculateRotatedBoundingBox(Vector2 worldPosition, Angle worldRotation, Angle eyeRot) { - // fast check for empty sprites - if (!Visible || Layers.Count == 0) - { - return new Box2Rotated(new Box2(worldPosition, worldPosition), Angle.Zero, worldPosition); - } - - // We need to modify world rotation so that it lies between 0 and 2pi. - // This matters for 4 or 8 directional sprites deciding which quadrant (octant?) they lie in. - // the 0->2pi convention is set by the sprite-rendering code that selects the layers. - // See RenderInternal(). - - worldRotation = worldRotation.Reduced(); - if (worldRotation.Theta < 0) - worldRotation = new Angle(worldRotation.Theta + Math.Tau); - - // Next, what we do is take the box2 and apply the sprite's transform, and then the entity's transform. We - // could do this via Matrix3.TransformBox, but that only yields bounding boxes. So instead we manually - // transform our box by the combination of these matrices: - - Angle finalRotation = NoRotation - ? Rotation - eyeRot - : Rotation + worldRotation; - - // slightly faster path if offset == 0 (true for 99.9% of sprites) - if (Offset == Vector2.Zero) - return new Box2Rotated(Bounds.Translated(worldPosition), finalRotation, worldPosition); - - var adjustedOffset = NoRotation - ? (-eyeRot).RotateVec(Offset) - : worldRotation.RotateVec(Offset); - - Vector2 position = adjustedOffset + worldPosition; - return new Box2Rotated(Bounds.Translated(position), finalRotation, position); + return Sys.CalculateBounds((Owner, this), worldPosition, worldRotation, eyeRot); } + #endregion + /// /// Enum to "offset" a cardinal direction. /// @@ -1491,36 +1056,55 @@ public enum DirectionOffset : byte public sealed class Layer : ISpriteLayer, ISerializationHooks { - [ViewVariables] private readonly SpriteComponent _parent; + internal SpriteComponent _parent => Owner.Comp; + + /// + /// The entity that this layer belongs to. + /// + [Access(typeof(SpriteSystem), typeof(SpriteComponent))] + [ViewVariables] internal Entity Owner; + // Internal, because I might want to change this in future W/O breaking changes. + // Also, it's possible for SpriteComponent to be null if it is not currently attached to a sprite. + + /// + /// The index of the layer within its layer collection (usually a SpriteComponent). + /// + [Access(typeof(SpriteSystem), typeof(SpriteComponent))] + [ViewVariables] internal int Index; + // Internal, because I might want to change this in future W/O breaking changes. [ViewVariables] public string? ShaderPrototype; [ViewVariables] public ShaderInstance? Shader; [ViewVariables] public Texture? Texture; - private RSI? _rsi; + internal RSI? _rsi; [ViewVariables] public RSI? RSI { get => _rsi; + [Obsolete("Use SpriteSystem.LayerSetRsi() instead.")] set { if (_rsi == value) return; + BoundsDirty = true; + Owner.Comp.BoundsDirty = true; _rsi = value; UpdateActualState(); } } - private RSI.StateId _state; + internal RSI.StateId StateId; [ViewVariables] public RSI.StateId State { - get => _state; + get => StateId; + [Obsolete("Use SpriteSystem.LayerSetRsiState() instead.")] set { - if (_state == value) + if (StateId == value) return; - _state = value; + StateId = value; UpdateActualState(); } } @@ -1542,15 +1126,18 @@ [ViewVariables] public RSI.StateId State /// [ViewVariables] public bool Cycle; - private RSI.State? _actualState; + // TODO SPRITE ACCESS + internal RSI.State? _actualState; [ViewVariables] public RSI.State? ActualState => _actualState; + // TODO SPRITE ACCESS public Matrix3x2 LocalMatrix = Matrix3x2.Identity; [ViewVariables(VVAccess.ReadWrite)] public Vector2 Scale { get => _scale; + [Obsolete("Use SpriteSystem.LayerSetScale() instead.")] set { if (_scale.EqualsApprox(value)) return; @@ -1564,7 +1151,9 @@ public Vector2 Scale _scale = value; UpdateLocalMatrix(); - _parent.RebuildBounds(); + BoundsDirty = true; + Owner.Comp.BoundsDirty = true; + Owner.Comp.TreeSys.QueueTreeUpdate(Owner); } } internal Vector2 _scale = Vector2.One; @@ -1573,30 +1162,38 @@ public Vector2 Scale public Angle Rotation { get => _rotation; + [Obsolete("Use SpriteSystem.LayerSetRotation() instead.")] set { if (_rotation.EqualsApprox(value)) return; _rotation = value; UpdateLocalMatrix(); - _parent.RebuildBounds(); + BoundsDirty = true; + Owner.Comp.BoundsDirty = true; + Owner.Comp.TreeSys.QueueTreeUpdate(Owner); } } internal Angle _rotation = Angle.Zero; - private bool _visible = true; + internal bool _visible = true; [ViewVariables(VVAccess.ReadWrite)] public bool Visible { get => _visible; + [Obsolete("Use SpriteSystem.LayerSetVisible() instead.")] set { if (_visible == value) return; _visible = value; - _parent.QueueUpdateIsInert(); - _parent.RebuildBounds(); + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract + if (_parent.Owner != EntityUid.Invalid) + Owner.Comp.Sys?.QueueUpdateIsInert(Owner); + + if (_parent.Owner != EntityUid.Invalid) + Owner.Comp.TreeSys.QueueTreeUpdate(Owner); } } @@ -1606,17 +1203,20 @@ public bool Visible [ViewVariables(VVAccess.ReadWrite)] public Color Color { get; set; } = Color.White; - private bool _autoAnimated = true; + internal bool _autoAnimated = true; [ViewVariables(VVAccess.ReadWrite)] public bool AutoAnimated { get => _autoAnimated; + [Obsolete("Use SpriteSystem.LayerSetAutoAnimated() instead.")] set { if (_autoAnimated == value) return; _autoAnimated = value; - _parent.QueueUpdateIsInert(); + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract + if (_parent.Owner != EntityUid.Invalid) + _parent.Sys?.QueueUpdateIsInert((_parent.Owner, _parent)); } } @@ -1624,13 +1224,19 @@ public bool AutoAnimated public Vector2 Offset { get => _offset; + [Obsolete("Use SpriteSystem.LayerSetOffset() instead.")] set { if (_offset.EqualsApprox(value)) return; + BoundsDirty = true; + Owner.Comp.BoundsDirty = true; + _offset = value; UpdateLocalMatrix(); - _parent.RebuildBounds(); + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract + if (_parent.Owner != EntityUid.Invalid) + Owner.Comp.TreeSys.QueueTreeUpdate(Owner); } } @@ -1646,20 +1252,33 @@ public Vector2 Offset /// Whether the current layer have a specific rendering method (e.g no rotation or snap to cardinal) /// The sprite GranularLayersRendering var must be set to true for this to have any effect. /// - [ViewVariables] + [ViewVariables] // TODO SPRITE ACCESS public LayerRenderingStrategy RenderingStrategy = LayerRenderingStrategy.UseSpriteStrategy; + // TODO SPRITE ACCESS + // If someone sets this, it stops the actual layer from being drawn, which should chage the sprites bounds. [ViewVariables(VVAccess.ReadWrite)] public CopyToShaderParameters? CopyToShaderParameters; + [Obsolete("Use SpriteSystem.AddBlankLayer")] public Layer(SpriteComponent parent) { - _parent = parent; + Owner = (parent.Owner, parent); + } + + internal Layer() + { } - public Layer(Layer toClone, SpriteComponent parentSprite) + internal Layer(Entity owner, int index) + { + Owner = owner; + Index = index; + } + + [Obsolete] // This should be internal to SpriteSystem + public Layer(Layer toClone, SpriteComponent parent) : this(parent) { - _parent = parentSprite; if (toClone.Shader != null) { Shader = toClone.Shader.Mutable ? toClone.Shader.Duplicate() : toClone.Shader; @@ -1684,6 +1303,8 @@ public Layer(Layer toClone, SpriteComponent parentSprite) CopyToShaderParameters = new CopyToShaderParameters(copyToShaderParameters); } + // TODO SPRITE + // Is Layer even serializable? void ISerializationHooks.AfterDeserialization() { UpdateLocalMatrix(); @@ -1767,6 +1388,7 @@ public RsiDirection EffectiveDirection(RSI.State state, Angle worldRotation, } } + [Obsolete("Use SpriteSystem.LayerSetAnimationTime")] public void SetAnimationTime(float animationTime) { if (!State.IsValid) @@ -1798,13 +1420,15 @@ public void SetAnimationTime(float animationTime) AdvanceFrameAnimation(state); } + [Obsolete("Use SpriteSystem.LayerSetAutoAnimated")] public void SetAutoAnimated(bool value) { AutoAnimated = value; - _parent.QueueUpdateIsInert(); + _parent.Sys.QueueUpdateIsInert((_parent.Owner, _parent)); } + [Obsolete("Use SpriteSystem.LayerSetRsi")] public void SetRsi(RSI? rsi) { RSI = rsi; @@ -1834,10 +1458,14 @@ public void SetRsi(RSI? rsi) } } - _parent.QueueUpdateRenderTree(); - _parent.QueueUpdateIsInert(); + BoundsDirty = true; + Owner.Comp.BoundsDirty = true; + + _parent.TreeSys.QueueTreeUpdate((_parent.Owner, _parent)); + _parent.Sys.QueueUpdateIsInert((_parent.Owner, _parent)); } + [Obsolete("Use SpriteSystem.LayerSetRsiState")] public void SetState(RSI.StateId stateId) { if (State == stateId) @@ -1850,14 +1478,14 @@ public void SetState(RSI.StateId stateId) var rsi = ActualRsi; if (rsi == null) { - state = GetFallbackState(_parent.resourceCache); + state = _parent.Sys.GetFallbackState(); Logger.ErrorS(LogCategory, "No RSI to pull new state from! Trace:\n{0}", Environment.StackTrace); } else { if (!rsi.TryGetState(stateId, out state)) { - state = GetFallbackState(_parent.resourceCache); + state = _parent.Sys.GetFallbackState(); Logger.ErrorS(LogCategory, "State '{0}' does not exist in RSI. Trace:\n{1}", stateId, Environment.StackTrace); } @@ -1867,16 +1495,20 @@ public void SetState(RSI.StateId stateId) AnimationTime = 0; AnimationTimeLeft = state.GetDelay(0); - _parent.QueueUpdateIsInert(); + _parent.Sys.QueueUpdateIsInert((_parent.Owner, _parent)); } + [Obsolete("Use SpriteSystem.LayerSetTexture")] public void SetTexture(Texture? texture) { State = default; Texture = texture; - _parent.QueueUpdateRenderTree(); - _parent.QueueUpdateIsInert(); + BoundsDirty = true; + Owner.Comp.BoundsDirty = true; + + _parent.TreeSys.QueueTreeUpdate((_parent.Owner, _parent)); + _parent.Sys.QueueUpdateIsInert((_parent.Owner, _parent)); } /// @@ -1898,53 +1530,22 @@ public Vector2i PixelSize } } - /// - public Box2 CalculateBoundingBox() - { - var textureSize = (Vector2) PixelSize / EyeManager.PixelsPerMeter; - var longestSide = MathF.Max(textureSize.X, textureSize.Y); - var longestRotatedSide = Math.Max(longestSide, (textureSize.X + textureSize.Y) / MathF.Sqrt(2)); - - Vector2 size; - - // If this layer has any form of arbitrary rotation, return a bounding box big enough to cover - // any possible rotation. - if (_rotation != 0) - { - size = new Vector2(longestRotatedSide, longestRotatedSide); - } - else if (_parent.SnapCardinals && (!_parent.GranularLayersRendering || RenderingStrategy == LayerRenderingStrategy.UseSpriteStrategy) - || _parent.GranularLayersRendering && RenderingStrategy == LayerRenderingStrategy.SnapToCardinals) - { - DebugTools.Assert(_actualState == null || _actualState.RsiDirections == RsiDirectionType.Dir1); - size = new Vector2(longestSide, longestSide); - } - else - { - // Build the bounding box based on how many directions the sprite has - size = (_actualState?.RsiDirections) switch - { - // If we have four cardinal directions, take the longest side of our texture and square it, then turn that into our bounding box. - // This accounts for all possible rotations. - RsiDirectionType.Dir4 => new Vector2(longestSide, longestSide), - - // If we have eight directions, find the maximum length of the texture (accounting for rotation), then square it to make - RsiDirectionType.Dir8 => new Vector2(longestRotatedSide, longestRotatedSide), - - // If we have only one direction or an invalid RSI state, create a simple bounding box with the size of the texture. - _ => textureSize - }; - } + /// + /// Whether or not the layers's local bounding box is dirty and need to be rebuilt. + /// + internal bool BoundsDirty = true; + internal Box2 Bounds; - return Box2.CenteredAround(Offset, size * _scale); - } + [Obsolete("Use SpriteSystem.GetLocalBounds()")] + public Box2 CalculateBoundingBox() => Owner.Comp.Sys.GetLocalBounds(this); /// /// Update Cached RSI state. State is cached to avoid calling this every time an entity gets drawn. /// internal void UpdateActualState() { - _parent.QueueUpdateIsInert(); + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract + _parent.Sys?.QueueUpdateIsInert((_parent.Owner, _parent)); if (!State.IsValid) { _actualState = null; @@ -1955,28 +1556,36 @@ internal void UpdateActualState() var rsi = RSI ?? _parent.BaseRSI; if (rsi == null || !rsi.TryGetState(State, out _actualState)) { - _actualState = GetFallbackState(_parent.resourceCache); + _actualState = _parent.Sys?.GetFallbackState(); } } + public void GetLayerDrawMatrix(RsiDirection dir, out Matrix3x2 layerDrawMatrix) + { + GetLayerDrawMatrix(dir, out layerDrawMatrix, Owner.Comp.NoRotation); + } + /// - /// Given the apparent rotation of an entity on screen (world + eye rotation), get layer's matrix for drawing & - /// relevant RSI direction. + /// Given the apparent rotation of an entity on screen (world + eye rotation), get layer's matrix for drawing & + /// relevant RSI direction. /// - public void GetLayerDrawMatrix(RsiDirection dir, out Matrix3x2 layerDrawMatrix) + internal void GetLayerDrawMatrix(RsiDirection dir, out Matrix3x2 layerDrawMatrix, bool noRot) { - if (_parent.NoRotation || dir == RsiDirection.South) + // TODO RENDERING + // Consider changing the RSI format (or at least modify the loaded textures) to remove this + // unnecessary matrix transformation. This transform is completely unnecessary for 4- and + // 1-directional sprites. Its only really required for 8-directional sprites. + + if (dir == RsiDirection.South || noRot) layerDrawMatrix = LocalMatrix; else - { - layerDrawMatrix = Matrix3x2.Multiply(_rsiDirectionMatrices[(int)dir], LocalMatrix); - } + layerDrawMatrix = Matrix3x2.Multiply(_rsiDirectionMatrices[(int) dir], LocalMatrix); } private static Matrix3x2[] _rsiDirectionMatrices = new Matrix3x2[] { // array order chosen such that this array can be indexed by casing an RSI direction to an int - Matrix3x2.Identity, // should probably just avoid matrix multiplication altogether if the direction is south. + Matrix3x2.Identity, Matrix3Helpers.CreateRotation(-Direction.North.ToAngle()), Matrix3Helpers.CreateRotation(-Direction.East.ToAngle()), Matrix3Helpers.CreateRotation(-Direction.West.ToAngle()), @@ -1994,7 +1603,8 @@ public static RsiDirection GetDirection(RsiDirectionType dirType, Angle angle) { if (dirType == RsiDirectionType.Dir1) return RsiDirection.South; - else if (dirType == RsiDirectionType.Dir8) + + if (dirType == RsiDirectionType.Dir8) return angle.GetDir().Convert(dirType); // For 4-directional sprites, as entities are often moving & facing diagonally, we will slightly bias the @@ -2014,87 +1624,6 @@ public static RsiDirection GetDirection(RsiDirectionType dirType, Angle angle) }; } - /// - /// Render a layer. This assumes that the input angle is between 0 and 2pi. - /// - internal void Render(DrawingHandleWorld drawingHandle, ref Matrix3x2 spriteMatrix, Angle angle, Direction? overrideDirection) - { - if (!Visible || Blank) - return; - - var dir = _actualState == null ? RsiDirection.South : GetDirection(_actualState.RsiDirections, angle); - - // Set the drawing transform for this layer - GetLayerDrawMatrix(dir, out var layerMatrix); - - // The direction used to draw the sprite can differ from the one that the angle would naively suggest, - // due to direction overrides or offsets. - if (overrideDirection != null && _actualState != null) - dir = overrideDirection.Value.Convert(_actualState.RsiDirections); - dir = dir.OffsetRsiDir(DirOffset); - - // Get the correct directional texture from the state, and draw it! - var texture = GetRenderTexture(_actualState, dir); - - if (CopyToShaderParameters == null) - { - // Set the drawing transform for this layer - var transformMatrix = Matrix3x2.Multiply(layerMatrix, spriteMatrix); - drawingHandle.SetTransform(in transformMatrix); - - RenderTexture(drawingHandle, texture); - } - else - { - // Multiple atrocities to god being committed right here. - var otherLayerIdx = _parent.LayerMap[CopyToShaderParameters.LayerKey!]; - var otherLayer = _parent.Layers[otherLayerIdx]; - if (otherLayer.Shader is not { } shader) - { - // No shader set apparently..? - return; - } - - if (!shader.Mutable) - otherLayer.Shader = shader = shader.Duplicate(); - - var clydeTexture = Clyde.RenderHandle.ExtractTexture(texture, null, out var csr); - var sr = Clyde.RenderHandle.WorldTextureBoundsToUV(clydeTexture, csr); - - if (CopyToShaderParameters.ParameterTexture is { } paramTexture) - shader.SetParameter(paramTexture, clydeTexture); - - if (CopyToShaderParameters.ParameterUV is { } paramUV) - { - var uv = new Vector4(sr.Left, sr.Bottom, sr.Right, sr.Top); - shader.SetParameter(paramUV, uv); - } - } - } - - private void RenderTexture(DrawingHandleWorld drawingHandle, Texture texture) - { - if (Shader != null) - drawingHandle.UseShader(Shader); - - var layerColor = _parent.color * Color; - var textureSize = texture.Size / (float)EyeManager.PixelsPerMeter; - var quad = Box2.FromDimensions(textureSize/-2, textureSize); - - drawingHandle.DrawTextureRectRegion(texture, quad, layerColor); - - if (Shader != null) - drawingHandle.UseShader(null); - } - - private Texture GetRenderTexture(RSI.State? state, RsiDirection dir) - { - if (state == null) - return Texture ?? _parent.resourceCache.GetFallback().Texture; - - return state.GetFrame(dir, AnimationFrame); - } - internal void AdvanceFrameAnimation(RSI.State state) { // Can't advance frames without more than 1 delay which is already checked above. @@ -2209,18 +1738,20 @@ public IRsiStateLike? Icon var rsi = layer.RSI ?? BaseRSI; if (rsi == null || !rsi.TryGetState(layer.State, out var state)) { - state = GetFallbackState(resourceCache); + state = Sys?.GetFallbackState(); } return state; } } + [Obsolete("Use SpriteSystem.GetPrototypeTextures() instead")] public static IEnumerable GetPrototypeTextures(EntityPrototype prototype, IResourceCache resourceCache) { return GetPrototypeTextures(prototype, resourceCache, out var _); } + [Obsolete("Use SpriteSystem.GetPrototypeTextures() instead")] public static IEnumerable GetPrototypeTextures(EntityPrototype prototype, IResourceCache resourceCache, out bool noRot) { var results = new List(); @@ -2275,34 +1806,24 @@ public static IEnumerable GetPrototypeTextures(Enti return results; } - [Obsolete("Use SpriteSystem")] + [Obsolete("Use SpriteSystem.GetPrototypeIcon() instead")] public static IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache) { + var sys = IoCManager.Resolve().GetEntitySystem(); // TODO when moving to a non-static method in a system, pass in IComponentFactory if (prototype.TryGetComponent(out IconComponent? icon)) - { - var sys = IoCManager.Resolve().GetEntitySystem(); return sys.GetIcon(icon); - } if (!prototype.Components.ContainsKey("Sprite")) - { - return GetFallbackState(resourceCache); - } + return sys.GetFallbackState(); var entityManager = IoCManager.Resolve(); var dummy = entityManager.SpawnEntity(prototype.ID, MapCoordinates.Nullspace); var spriteComponent = entityManager.EnsureComponent(dummy); - var result = spriteComponent.Icon ?? GetFallbackState(resourceCache); + var result = spriteComponent.Icon ?? sys.GetFallbackState(); entityManager.DeleteEntity(dummy); return result; } } - - - [ByRefEvent] - internal struct SpriteUpdateInertEvent - { - } } diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Bounds.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Bounds.cs new file mode 100644 index 00000000000..3d8e0c45b6f --- /dev/null +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Bounds.cs @@ -0,0 +1,149 @@ +using System; +using System.Linq; +using System.Numerics; +using Robust.Client.Graphics; +using Robust.Shared.GameObjects; +using Robust.Shared.Graphics.RSI; +using Robust.Shared.Maths; +using Robust.Shared.Utility; +using static Robust.Client.GameObjects.SpriteComponent; + +namespace Robust.Client.GameObjects; + +// This partial class contains code related to updating a sprites bounding boxes and its position in the sprite tree. +public sealed partial class SpriteSystem +{ + /// + /// Get a sprite's local bounding box. The returned bounds do factor in the sprite's scale but not the rotation or + /// offset. + /// + public Box2 GetLocalBounds(Entity sprite) + { + if (!sprite.Comp.BoundsDirty) + { + DebugTools.Assert(sprite.Comp.Layers.All(x => !x.BoundsDirty || x.Blank || !x.Visible)); + return sprite.Comp._bounds; + } + + var bounds = new Box2(); + foreach (var layer in sprite.Comp.Layers) + { + if (layer is {Visible: true, Blank: false, CopyToShaderParameters: null}) + bounds = bounds.Union(GetLocalBounds(layer)); + } + + sprite.Comp._bounds = bounds; + sprite.Comp.BoundsDirty = false; + return sprite.Comp._bounds; + } + + /// + /// Get a layer's local bounding box relative to its owning sprite. Unlike the sprite variant of this method, this + /// does account for the layer's rotation and offset. + /// + public Box2 GetLocalBounds(Layer layer) + { + if (!layer.BoundsDirty) + return layer.Bounds; + + layer.Bounds = CalculateLocalBounds(layer); + layer.BoundsDirty = false; + return layer.Bounds; + } + + internal Box2 CalculateLocalBounds(Layer layer) + { + if (layer.Blank || layer.CopyToShaderParameters == null) + return Box2.Empty; + + var textureSize = (Vector2) layer.PixelSize / EyeManager.PixelsPerMeter; + var longestSide = MathF.Max(textureSize.X, textureSize.Y); + var longestRotatedSide = Math.Max(longestSide, (textureSize.X + textureSize.Y) / MathF.Sqrt(2)); + + Vector2 size; + var sprite = layer.Owner.Comp; + + // If this layer has any form of arbitrary rotation, return a bounding box big enough to cover + // any possible rotation. + if (layer._rotation != 0) + { + size = new Vector2(longestRotatedSide, longestRotatedSide); + return Box2.CenteredAround(layer.Offset, size * layer._scale); + } + + var snapToCardinals = sprite.SnapCardinals; + if (sprite.GranularLayersRendering && layer.RenderingStrategy != LayerRenderingStrategy.UseSpriteStrategy) + { + snapToCardinals = layer.RenderingStrategy == LayerRenderingStrategy.SnapToCardinals; + } + + if (snapToCardinals) + { + // Snapping to cardinals only makes sense for 1-directional layers/sprites + DebugTools.Assert(layer._actualState == null || layer._actualState.RsiDirections == RsiDirectionType.Dir1); + + // We won't know the actual direction it snaps to, so we ahve to assume the box is given by the longest side. + size = new Vector2(longestSide, longestSide); + return Box2.CenteredAround(layer.Offset, size * layer._scale); + } + + // Build the bounding box based on how many directions the sprite has + size = (layer._actualState?.RsiDirections) switch + { + RsiDirectionType.Dir4 => new Vector2(longestSide, longestSide), + RsiDirectionType.Dir8 => new Vector2(longestRotatedSide, longestRotatedSide), + _ => textureSize + }; + + return Box2.CenteredAround(layer.Offset, size * layer._scale); + } + + /// + /// Gets a sprite's bounding box in world coordinates. + /// + public Box2Rotated CalculateBounds(Entity sprite, Vector2 worldPos, Angle worldRot, Angle eyeRot) + { + // fast check for invisible sprites + if (!sprite.Comp.Visible || sprite.Comp.Layers.Count == 0) + return new Box2Rotated(new Box2(worldPos, worldPos), Angle.Zero, worldPos); + + // We need to modify world rotation so that it lies between 0 and 2pi. + // This matters for 4 or 8 directional sprites deciding which quadrant (octant?) they lie in. + // the 0->2pi convention is set by the sprite-rendering code that selects the layers. + // See RenderInternal(). + + worldRot = worldRot.Reduced(); + if (worldRot.Theta < 0) + worldRot = new Angle(worldRot.Theta + Math.Tau); + + // Next, what we do is take the box2 and apply the sprite's transform, and then the entity's transform. We + // could do this via Matrix3.TransformBox, but that only yields bounding boxes. So instead we manually + // transform our box by the combination of these matrices: + + var finalRotation = sprite.Comp.NoRotation + ? sprite.Comp.Rotation - eyeRot + : sprite.Comp.Rotation + worldRot; + + var bounds = GetLocalBounds(sprite); + + // slightly faster path if offset == 0 (true for 99.9% of sprites) + if (sprite.Comp.Offset == Vector2.Zero) + return new Box2Rotated(bounds.Translated(worldPos), finalRotation, worldPos); + + var adjustedOffset = sprite.Comp.NoRotation + ? (-eyeRot).RotateVec(sprite.Comp.Offset) + : worldRot.RotateVec(sprite.Comp.Offset); + + var position = adjustedOffset + worldPos; + return new Box2Rotated(bounds.Translated(position), finalRotation, position); + } + + private void DirtyBounds(Entity sprite) + { + sprite.Comp.BoundsDirty = true; + foreach (var layer in sprite.Comp.Layers) + { + layer.BoundsDirty = true; + } + } +} diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs index de02891ecc3..8d8f43b42d6 100644 --- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs @@ -1,4 +1,8 @@ -using System.Linq; +using System; +using System.Collections.Generic; +using Robust.Shared.GameObjects; +using Robust.Shared.Maths; +using Robust.Shared.Utility; namespace Robust.Client.GameObjects; @@ -41,4 +45,66 @@ public void SetAutoAnimateSync(SpriteComponent sprite, SpriteComponent.Layer lay 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._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; + 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.IsInert = source.Comp.IsInert; + target.Comp.LayerMap = source.Comp.LayerMap.ShallowClone(); + target.Comp.PostShader = source.Comp.PostShader is {Mutable: true} + ? source.Comp.PostShader.Duplicate() + : source.Comp.PostShader; + + target.Comp.RenderOrder = source.Comp.RenderOrder; + target.Comp.GranularLayersRendering = source.Comp.GranularLayersRendering; + + 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)); } diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Helpers.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Helpers.cs index 2cbf9fcbb59..fcf3b58f7fc 100644 --- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Helpers.cs +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Helpers.cs @@ -1,11 +1,153 @@ +using System; +using System.Collections.Generic; using System.Numerics; +using JetBrains.Annotations; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; using Robust.Shared.GameObjects; using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; namespace Robust.Client.GameObjects; +// This partial class contains various public helper methods, including methods for extracting textures/icons from +// sprite specifiers and entity prototypes. public sealed partial class SpriteSystem { + private readonly Dictionary _cachedPrototypeIcons = new(); + + public Texture Frame0(EntityPrototype prototype) + { + return GetPrototypeIcon(prototype).Default; + } + + public Texture Frame0(SpriteSpecifier specifier) + { + return RsiStateLike(specifier).Default; + } + + public IRsiStateLike RsiStateLike(SpriteSpecifier specifier) + { + switch (specifier) + { + case SpriteSpecifier.Texture tex: + return GetTexture(tex); + + case SpriteSpecifier.Rsi rsi: + return GetState(rsi); + + case SpriteSpecifier.EntityPrototype prototypeIcon: + return GetPrototypeIcon(prototypeIcon.EntityPrototypeId); + + default: + throw new NotSupportedException(); + } + } + + public Texture GetIcon(IconComponent icon) + { + return GetState(icon.Icon).Frame0; + } + + /// + /// Returns an icon for a given ID, or a fallback in case of an error. + /// This method caches the result based on the prototype identifier. + /// + public IRsiStateLike GetPrototypeIcon(string prototype) + { + // Check if this prototype has been cached before, and if so return the result. + if (_cachedPrototypeIcons.TryGetValue(prototype, out var cachedResult)) + return cachedResult; + + if (!_proto.TryIndex(prototype, out var entityPrototype)) + { + // The specified prototype doesn't exist, return the fallback "error" sprite. + _sawmill.Error("Failed to load PrototypeIcon {0}", prototype); + return GetFallbackState(); + } + + // Generate the icon and cache it in case it's ever needed again. + var result = GetPrototypeIcon(entityPrototype); + _cachedPrototypeIcons[prototype] = result; + + return result; + } + + /// + /// Returns an icon for a given ID, or a fallback in case of an error. + /// This method does NOT cache the result. + /// + public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype) + { + // IconComponent takes precedence. If it has a valid icon, return that. Otherwise, continue as normal. + if (prototype.Components.TryGetValue("Icon", out var compData) + && compData.Component is IconComponent icon) + { + return GetIcon(icon); + } + + // If the prototype doesn't have a SpriteComponent, then there's nothing we can do but return the fallback. + if (!prototype.Components.ContainsKey("Sprite")) + { + return GetFallbackState(); + } + + // Finally, we use spawn a dummy entity to get its icon. + var dummy = Spawn(prototype.ID, MapCoordinates.Nullspace); + var spriteComponent = EnsureComp(dummy); + var result = spriteComponent.Icon ?? GetFallbackState(); + Del(dummy); + + return result; + } + + [Pure] + public RSI.State GetFallbackState() + { + return _resourceCache.GetFallback().RSI["error"]; + } + + public Texture GetFallbackTexture() + { + return _resourceCache.GetFallback().Texture; + } + + [Pure] + public RSI.State GetState(SpriteSpecifier.Rsi rsiSpecifier) + { + if (_resourceCache.TryGetResource( + TextureRoot / rsiSpecifier.RsiPath, + out var theRsi) && + theRsi.RSI.TryGetState(rsiSpecifier.RsiState, out var state)) + { + return state; + } + + _sawmill.Error("Failed to load RSI {0}", rsiSpecifier.RsiPath); + return GetFallbackState(); + } + + public Texture GetTexture(SpriteSpecifier.Texture texSpecifier) + { + return _resourceCache + .GetResource(TextureRoot / texSpecifier.TexturePath) + .Texture; + } + + private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) + { + if (!args.TryGetModified(out var modified)) + return; + + // Remove all changed prototypes from the cache, if they're there. + foreach (var prototype in modified) + { + // Let's be lazy and not regenerate them until something needs them again. + _cachedPrototypeIcons.Remove(prototype); + } + } + /// /// Gets an entity's sprite position in world terms. /// diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Layer.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Layer.cs new file mode 100644 index 00000000000..449151fc406 --- /dev/null +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Layer.cs @@ -0,0 +1,257 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Shared.GameObjects; +using Robust.Shared.Utility; +using static Robust.Client.GameObjects.SpriteComponent; + +namespace Robust.Client.GameObjects; + +// This partial class contains various public methods for managing a sprite's layers. +// This setter methods for modifying a layer's properties are in a separate file. +public sealed partial class SpriteSystem +{ + public bool LayerExists(Entity sprite, int index) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return false; + + return index > 0 && index < sprite.Comp.Layers.Count; + } + + public bool TryGetLayer( + Entity sprite, + int index, + [NotNullWhen(true)] out Layer? layer, + bool logMissing) + { + layer = null; + + if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing)) + return false; + + if (index >= 0 && index < sprite.Comp.Layers.Count) + { + layer = sprite.Comp.Layers[index]; + DebugTools.AssertEqual(layer.Owner, sprite!); + DebugTools.AssertEqual(layer.Index, index); + return true; + } + + if (logMissing) + Log.Error($"Layer index '{index}' on entity {ToPrettyString(sprite)} does not exist! Trace:\n{Environment.StackTrace}"); + + return false; + } + + public bool RemoveLayer(Entity sprite, int index, bool logMissing = true) + { + return RemoveLayer(sprite.Owner, index, out _, logMissing); + } + + public bool RemoveLayer( + Entity sprite, + int index, + [NotNullWhen(true)] out Layer? layer, + bool logMissing = true) + { + layer = null; + if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing)) + return false; + + if (!TryGetLayer(sprite, index, out layer, logMissing)) + return false; + + sprite.Comp.Layers.RemoveAt(index); + + foreach (var otherLayer in sprite.Comp.Layers[index..]) + { + otherLayer.Index--; + } + + // TODO SPRITE track inverse-mapping? + foreach (var (key, value) in sprite.Comp.LayerMap) + { + if (value == index) + sprite.Comp.LayerMap.Remove(key); + else if (value > index) + { + sprite.Comp.LayerMap[key]--; + } + } + + layer.Owner = default; + layer.Index = -1; + +#if DEBUG + foreach (var otherLayer in sprite.Comp.Layers) + { + DebugTools.AssertEqual(otherLayer, sprite.Comp.Layers[otherLayer.Index]); + } +#endif + + sprite.Comp.BoundsDirty = true; + _tree.QueueTreeUpdate(sprite!); + QueueUpdateIsInert(sprite!); + return true; + } + + #region AddLayer + + /// + /// Add the given sprite layer. If an index is specified, this will insert the layer with the given index, resulting + /// in all other layers being reshuffled. + /// + public int AddLayer(Entity sprite, Layer layer, int? index = null) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + { + layer.Index = -1; + layer.Owner = default; + return -1; + } + + layer.Owner = sprite!; + + if (index is { } i && i != sprite.Comp.Layers.Count) + { + foreach (var otherLayer in sprite.Comp.Layers[i..]) + { + otherLayer.Index++; + } + + // TODO SPRITE track inverse-mapping? + sprite.Comp.Layers.Insert(i, layer); + layer.Index = i; + + foreach (var (key, value) in sprite.Comp.LayerMap) + { + if (value >= i) + sprite.Comp.LayerMap[key]++; + } + } + else + { + layer.Index = sprite.Comp.Layers.Count; + sprite.Comp.Layers.Add(layer); + } + +#if DEBUG + foreach (var otherLayer in sprite.Comp.Layers) + { + DebugTools.AssertEqual(otherLayer, sprite.Comp.Layers[otherLayer.Index]); + } +#endif + + if (!layer.Blank) + { + layer.BoundsDirty = true; + sprite.Comp.BoundsDirty = true; + _tree.QueueTreeUpdate(sprite!); + QueueUpdateIsInert(sprite!); + } + return layer.Index; + } + + /// + /// Add a layer corresponding to the given RSI state. + /// + /// The sprite + /// The RSI state + /// The RSI to use. If not specified, it will default to using + /// The layer index to use for the new sprite. + /// + public int AddRsiLayer(Entity sprite, RSI.StateId stateId, RSI? rsi = null, int? index = null) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return -1; + + var layer = AddBlankLayer(sprite!, index); + + if (rsi != null) + LayerSetRsi(layer, rsi, stateId); + else + LayerSetRsiState(layer, stateId); + + return layer.Index; + } + + /// + /// Add a layer corresponding to the given RSI state. + /// + /// The sprite + /// The RSI state + /// The path to the RSI. + /// The layer index to use for the new sprite. + /// + public int AddRsiLayer(Entity sprite, RSI.StateId state, ResPath path, int? index = null) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return -1; + + if (!_resourceCache.TryGetResource(TextureRoot / path, out var res)) + Log.Error($"Unable to load RSI '{path}'. Trace:\n{Environment.StackTrace}"); + + if (path.Extension != "rsi") + Log.Error($"Expected rsi path but got '{path}'?"); + + return AddRsiLayer(sprite, state, res?.RSI, index); + } + + public int AddTextureLayer(Entity sprite, ResPath path, int? index = null) + { + if (_resourceCache.TryGetResource(TextureRoot / path, out var texture)) + return AddTextureLayer(sprite, texture?.Texture, index); + + if (path.Extension == "rsi") + Log.Error($"Expected texture but got rsi '{path}', did you mean 'sprite:' instead of 'texture:'?"); + + Log.Error($"Unable to load texture '{path}'. Trace:\n{Environment.StackTrace}"); + return AddTextureLayer(sprite, texture?.Texture, index); + } + + public int AddTextureLayer(Entity sprite, Texture? texture, int? index = null) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return -1; + + var layer = new Layer {Texture = texture}; + return AddLayer(sprite, layer, index); + } + + public int AddLayer(Entity sprite, SpriteSpecifier specifier, int? newIndex = null) + { + return specifier switch + { + SpriteSpecifier.Texture tex => AddTextureLayer(sprite, tex.TexturePath, newIndex), + SpriteSpecifier.Rsi rsi => AddRsiLayer(sprite, rsi.RsiState, rsi.RsiPath, newIndex), + _ => throw new NotImplementedException() + }; + } + + /// + /// Add a new sprite layer and populate it using the provided layer data. + /// + public int AddLayer(Entity sprite, PrototypeLayerData layerDatum, int? index) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return -1; + + var layer = AddBlankLayer(sprite!, index); + LayerSetData(layer, layerDatum); + return layer.Index; + } + + /// + /// Add a blank sprite layer. + /// + public Layer AddBlankLayer(Entity sprite, int? index = null) + { + var layer = new Layer(); + AddLayer(sprite!, layer, index); + return layer; + } + + #endregion +} diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.LayerGetters.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.LayerGetters.cs new file mode 100644 index 00000000000..bdc0eb5e8a5 --- /dev/null +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.LayerGetters.cs @@ -0,0 +1,150 @@ +using System; +using Robust.Client.Graphics; +using Robust.Shared.GameObjects; +using Robust.Shared.Graphics.RSI; +using static Robust.Client.GameObjects.SpriteComponent; +using static Robust.Client.Graphics.RSI; + +namespace Robust.Client.GameObjects; + +// This partial class contains various public methods for reading a layer's properties +public sealed partial class SpriteSystem +{ + #region RsiState + + /// + /// Get the RSI state being used by the current layer. Note that the return value may be an invalid state. E.g., + /// this might be a texture layer that does not use RSIs. + /// + public StateId LayerGetRsiState(Entity sprite, int index) + { + if (TryGetLayer(sprite, index, out var layer, true)) + return layer.StateId; + + return StateId.Invalid; + } + + /// + /// Get the RSI state being used by the current layer. Note that the return value may be an invalid state. E.g., + /// this might be a texture layer that does not use RSIs. + /// + public StateId LayerGetRsiState(Entity sprite, string key, StateId state) + { + if (TryGetLayer(sprite, key, out var layer, true)) + return layer.StateId; + + return StateId.Invalid; + } + + /// + /// Get the RSI state being used by the current layer. Note that the return value may be an invalid state. E.g., + /// this might be a texture layer that does not use RSIs. + /// + public StateId LayerGetRsiState(Entity sprite, Enum key, StateId state) + { + if (TryGetLayer(sprite, key, out var layer, true)) + return layer.StateId; + + return StateId.Invalid; + } + + #endregion + + #region RsiState + + /// + /// Returns the RSI being used by the layer to resolve it's RSI state. If the layer does not specify an RSI, this + /// will just be the base RSI of the owning sprite (). + /// + public RSI? LayerGetEffectiveRsi(Entity sprite, int index) + { + TryGetLayer(sprite, index, out var layer, true); + return layer?.ActualRsi; + } + + /// + /// Returns the RSI being used by the layer to resolve it's RSI state. If the layer does not specify an RSI, this + /// will just be the base RSI of the owning sprite (). + /// + public RSI? LayerGetEffectiveRsi(Entity sprite, string key, StateId state) + { + TryGetLayer(sprite, key, out var layer, true); + return layer?.ActualRsi; + } + + /// + /// Returns the RSI being used by the layer to resolve it's RSI state. If the layer does not specify an RSI, this + /// will just be the base RSI of the owning sprite (). + /// + public RSI? LayerGetEffectiveRsi(Entity sprite, Enum key, StateId state) + { + TryGetLayer(sprite, key, out var layer, true); + return layer?.ActualRsi; + } + + #endregion + + #region Directions + + public RsiDirectionType LayerGetDirections(Entity sprite, int index) + { + return TryGetLayer(sprite, index, out var layer, true) + ? LayerGetDirections(layer) + : RsiDirectionType.Dir1; + } + + + public RsiDirectionType LayerGetDirections(Entity sprite, Enum key) + { + return TryGetLayer(sprite, key, out var layer, true) + ? LayerGetDirections(layer) + : RsiDirectionType.Dir1; + } + + public RsiDirectionType LayerGetDirections(Entity sprite, string key) + { + return TryGetLayer(sprite, key, out var layer, true) + ? LayerGetDirections(layer) + : RsiDirectionType.Dir1; + } + + public RsiDirectionType LayerGetDirections(Layer layer) + { + if (!layer.StateId.IsValid) + return RsiDirectionType.Dir1; + + // Pull texture from RSI state instead. + if (layer.ActualRsi is not {} rsi || !rsi.TryGetState(layer.StateId, out var state)) + return RsiDirectionType.Dir1; + + return state.RsiDirections; + } + + public int LayerGetDirectionCount(Entity sprite, int index) + { + return TryGetLayer(sprite, index, out var layer, true) ? LayerGetDirectionCount(layer) : 1; + } + + public int LayerGetDirectionCount(Entity sprite, Enum key) + { + return TryGetLayer(sprite, key, out var layer, true) ? LayerGetDirectionCount(layer) : 1; + } + + public int LayerGetDirectionCount(Entity sprite, string key) + { + return TryGetLayer(sprite, key, out var layer, true) ? LayerGetDirectionCount(layer) : 1; + } + + public int LayerGetDirectionCount(Layer layer) + { + return LayerGetDirections(layer) switch + { + RsiDirectionType.Dir1 => 1, + RsiDirectionType.Dir4 => 4, + RsiDirectionType.Dir8 => 8, + _ => throw new ArgumentOutOfRangeException() + }; + } + + #endregion +} diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.LayerMap.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.LayerMap.cs new file mode 100644 index 00000000000..d5239aa533d --- /dev/null +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.LayerMap.cs @@ -0,0 +1,301 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Robust.Shared.GameObjects; +using static Robust.Client.GameObjects.SpriteComponent; + +namespace Robust.Client.GameObjects; + +// This partial class contains various public methods for manipulating layer mappings. +public sealed partial class SpriteSystem +{ + /// + /// Map an enum to a layer index. + /// + public void LayerMapSet(Entity sprite, Enum key, int index) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return; + + if (index < 0 || index >= sprite.Comp.Layers.Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + sprite.Comp.LayerMap[key] = index; + } + + /// + /// Map string to a layer index. If possible, it is preferred to use an enum key. + /// string keys mainly exist to make it easier to define custom layer keys in yaml. + /// + public void LayerMapSet(Entity sprite, string key, int index) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return; + + if (index < 0 || index >= sprite.Comp.Layers.Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + sprite.Comp.LayerMap[key] = index; + } + + /// + /// Map an enum to a layer index. + /// + public void LayerMapAdd(Entity sprite, Enum key, int index) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return; + + if (index < 0 || index >= sprite.Comp.Layers.Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + sprite.Comp.LayerMap.Add(key, index); + } + + /// + /// Map a string to a layer index. If possible, it is preferred to use an enum key. + /// string keys mainly exist to make it easier to define custom layer keys in yaml. + /// + public void LayerMapAdd(Entity sprite, string key, int index) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return; + + if (index < 0 || index >= sprite.Comp.Layers.Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + sprite.Comp.LayerMap.Add(key, index); + } + + /// + /// Remove an enum mapping. + /// + public bool LayerMapRemove(Entity sprite, Enum key) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return false; + + return sprite.Comp.LayerMap.Remove(key); + } + + /// + /// Remove a string mapping. + /// + public bool LayerMapRemove(Entity sprite, string key) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return false; + + return sprite.Comp.LayerMap.Remove(key); + } + + /// + /// Remove an enum mapping. + /// + public bool LayerMapRemove(Entity sprite, Enum key, out int index) + { + if (_query.Resolve(sprite.Owner, ref sprite.Comp)) + return sprite.Comp.LayerMap.Remove(key, out index); + + index = 0; + return false; + } + + /// + /// Remove a string mapping. + /// + public bool LayerMapRemove(Entity sprite, string key, out int index) + { + if (_query.Resolve(sprite.Owner, ref sprite.Comp)) + return sprite.Comp.LayerMap.Remove(key, out index); + + index = 0; + return false; + } + + /// + /// Attempt to resolve an enum mapping. + /// + public bool LayerMapTryGet(Entity sprite, Enum key, out int index, bool logMissing) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing)) + { + index = 0; + return false; + } + + if (sprite.Comp.LayerMap.TryGetValue(key, out index)) + return true; + + if (logMissing) + Log.Error($"Layer with key '{key}' does not exist on entity {ToPrettyString(sprite)}! Trace:\n{Environment.StackTrace}"); + + return false; + } + + /// + /// Attempt to resolve a string mapping. + /// + public bool LayerMapTryGet(Entity sprite, string key, out int index, bool logMissing) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing)) + { + index = 0; + return false; + } + + if (sprite.Comp.LayerMap.TryGetValue(key, out index)) + return true; + + if (logMissing) + Log.Error($"Layer with key '{key}' does not exist on entity {ToPrettyString(sprite)}! Trace:\n{Environment.StackTrace}"); + + return false; + } + + /// + /// Attempt to resolve an enum mapping. + /// + public bool TryGetLayer(Entity sprite, Enum key, [NotNullWhen(true)] out Layer? layer, bool logMissing) + { + layer = null; + if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing)) + return false; + + return LayerMapTryGet(sprite, key, out var index, logMissing) + && TryGetLayer(sprite, index, out layer, logMissing); + } + + /// + /// Attempt to resolve a string mapping. + /// + public bool TryGetLayer(Entity sprite, string key, [NotNullWhen(true)] out Layer? layer, bool logMissing) + { + layer = null; + if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing)) + return false; + + return LayerMapTryGet(sprite, key, out var index, logMissing) + && TryGetLayer(sprite, index, out layer, logMissing); + } + + public int LayerMapGet(Entity sprite, Enum key) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return -1; + + return sprite.Comp.LayerMap[key]; + } + + public int LayerMapGet(Entity sprite, string key) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return -1; + + return sprite.Comp.LayerMap[key]; + } + + public bool LayerExists(Entity sprite, string key) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return false; + + return sprite.Comp.LayerMap.TryGetValue(key, out var index) + && LayerExists(sprite, index); + } + + public bool LayerExists(Entity sprite, Enum key) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return false; + + return sprite.Comp.LayerMap.TryGetValue(key, out var index) + && LayerExists(sprite, index); + } + + /// + /// Create a new blank layer and map the given key to it. + /// + public int LayerMapReserve(Entity sprite, Enum key) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return -1; + + if (LayerExists(sprite, key)) + throw new Exception("Layer already exists"); + + var layer = AddBlankLayer(sprite!); + LayerMapSet(sprite, key, layer.Index); + return layer.Index; + } + + /// + /// A create a new blank layer and map the given key to it. If possible, it is preferred to use an enum key. + /// string keys mainly exist to make it easier to define custom layer keys in yaml. + /// + public int LayerMapReserve(Entity sprite, string key) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return -1; + + if (LayerExists(sprite, key)) + throw new Exception("Layer already exists"); + + var layer = AddBlankLayer(sprite!); + LayerMapSet(sprite, key, layer.Index); + return layer.Index; + } + + public bool RemoveLayer(Entity sprite, string key, bool logMissing = true) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing)) + return false; + + if (!LayerMapTryGet(sprite, key, out var index, logMissing)) + return false; + + return RemoveLayer(sprite, index, logMissing); + } + + public bool RemoveLayer(Entity sprite, Enum key, bool logMissing = true) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing)) + return false; + + if (!LayerMapTryGet(sprite, key, out var index, logMissing)) + return false; + + return RemoveLayer(sprite, index, logMissing); + } + + public bool RemoveLayer( + Entity sprite, + string key, + [NotNullWhen(true)] out Layer? layer, + bool logMissing = true) + { + layer = null; + if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing)) + return false; + + if (!LayerMapTryGet(sprite, key, out var index, logMissing)) + return false; + + return RemoveLayer(sprite, index, out layer, logMissing); + } + + public bool RemoveLayer( + Entity sprite, + Enum key, + [NotNullWhen(true)] out Layer? layer, + bool logMissing = true) + { + layer = null; + if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing)) + return false; + + if (!LayerMapTryGet(sprite, key, out var index, logMissing)) + return false; + + return RemoveLayer(sprite, index, out layer, logMissing); + } +} diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.LayerSetters.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.LayerSetters.cs new file mode 100644 index 00000000000..dd1c7e1520c --- /dev/null +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.LayerSetters.cs @@ -0,0 +1,605 @@ +using System; +using System.Numerics; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Shared.GameObjects; +using Robust.Shared.Maths; +using Robust.Shared.Utility; +using static Robust.Client.GameObjects.SpriteComponent; +using static Robust.Client.Graphics.RSI; + +#pragma warning disable CS0618 // Type or member is obsolete + +namespace Robust.Client.GameObjects; + +// This partial class contains various public methods for modifying a layer's properties. +public sealed partial class SpriteSystem +{ + #region SetData + + public void LayerSetData(Entity sprite, int index, PrototypeLayerData data) + { + if (TryGetLayer(sprite, index, out var layer, true)) + LayerSetData(layer, data); + } + + public void LayerSetData(Entity sprite, string key, PrototypeLayerData data) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetData(layer, data); + } + + public void LayerSetData(Entity sprite, Enum key, PrototypeLayerData data) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetData(layer, data); + } + + public void LayerSetData(Layer layer, PrototypeLayerData data) + { + DebugTools.Assert(layer.Owner != default); + DebugTools.AssertNotNull(layer.Owner.Comp); + DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer); + // TODO SPRITE ECS + layer._parent.LayerSetData(layer, data); + } + + #endregion + + #region SpriteSpecifier + + public void LayerSetSprite(Entity sprite, int index, SpriteSpecifier specifier) + { + if (TryGetLayer(sprite, index, out var layer, true)) + LayerSetSprite(layer, specifier); + } + + public void LayerSetSprite(Entity sprite, string key, SpriteSpecifier specifier) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetSprite(layer, specifier); + } + + public void LayerSetSprite(Entity sprite, Enum key, SpriteSpecifier specifier) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetSprite(layer, specifier); + } + + public void LayerSetSprite(Layer layer, SpriteSpecifier specifier) + { + switch (specifier) + { + case SpriteSpecifier.Texture tex: + LayerSetTexture(layer, tex.TexturePath); + break; + + case SpriteSpecifier.Rsi rsi: + LayerSetRsi(layer, rsi.RsiPath, rsi.RsiState); + break; + + default: + throw new NotImplementedException(); + } + } + + #endregion + + #region Texture + + public void LayerSetTexture(Entity sprite, int index, Texture? texture) + { + if (TryGetLayer(sprite, index, out var layer, true)) + LayerSetTexture(layer, texture); + } + + public void LayerSetTexture(Entity sprite, string key, Texture? texture) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetTexture(layer, texture); + } + + public void LayerSetTexture(Entity sprite, Enum key, Texture? texture) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetTexture(layer, texture); + } + + public void LayerSetTexture(Layer layer, Texture? texture) + { + LayerSetRsiState(layer, StateId.Invalid, refresh: true); + layer.Texture = texture; + } + + public void LayerSetTexture(Entity sprite, int index, ResPath path) + { + if (TryGetLayer(sprite, index, out var layer, true)) + LayerSetTexture(layer, path); + } + + public void LayerSetTexture(Entity sprite, string key, ResPath path) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetTexture(layer, path); + } + + public void LayerSetTexture(Entity sprite, Enum key, ResPath path) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetTexture(layer, path); + } + + private void LayerSetTexture(Layer layer, ResPath path) + { + if (!_resourceCache.TryGetResource(TextureRoot / path, out var texture)) + { + if (path.Extension == "rsi") + Log.Error($"Expected texture but got rsi '{path}', did you mean 'sprite:' instead of 'texture:'?"); + Log.Error($"Unable to load texture '{path}'. Trace:\n{Environment.StackTrace}"); + } + + LayerSetTexture(layer, texture?.Texture); + } + + #endregion + + #region RsiState + + public void LayerSetRsiState(Entity sprite, int index, StateId state) + { + if (TryGetLayer(sprite, index, out var layer, true)) + LayerSetRsiState(layer, state); + } + + public void LayerSetRsiState(Entity sprite, string key, StateId state) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetRsiState(layer, state); + } + + public void LayerSetRsiState(Entity sprite, Enum key, StateId state) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetRsiState(layer, state); + } + + public void LayerSetRsiState(Layer layer, StateId state, bool refresh = false) + { + DebugTools.Assert(layer.Owner != default); + DebugTools.AssertNotNull(layer.Owner.Comp); + DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer); + + if (layer.StateId == state && !refresh) + return; + + layer.StateId = state; + RefreshCachedState(layer, true, null); + _tree.QueueTreeUpdate(layer.Owner); + QueueUpdateIsInert(layer.Owner); + layer.BoundsDirty = true; + layer.Owner.Comp.BoundsDirty = true; + } + + #endregion + + #region Rsi + + public void LayerSetRsi(Entity sprite, int index, RSI? rsi, StateId? state = null) + { + if (TryGetLayer(sprite, index, out var layer, true)) + LayerSetRsi(layer, rsi, state); + } + + public void LayerSetRsi(Entity sprite, string key, RSI? rsi, StateId? state = null) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetRsi(layer, rsi, state); + } + + public void LayerSetRsi(Entity sprite, Enum key, RSI? rsi, StateId? state = null) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetRsi(layer, rsi, state); + } + + public void LayerSetRsi(Layer layer, RSI? rsi, StateId? state = null) + { + layer._rsi = rsi; + LayerSetRsiState(layer, state ?? layer.StateId, refresh: true); + } + + public void LayerSetRsi(Entity sprite, int index, ResPath rsi, StateId? state = null) + { + if (TryGetLayer(sprite, index, out var layer, true)) + LayerSetRsi(layer, rsi, state); + } + + public void LayerSetRsi(Entity sprite, string key, ResPath rsi, StateId? state = null) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetRsi(layer, rsi, state); + } + + public void LayerSetRsi(Entity sprite, Enum key, ResPath rsi, StateId? state = null) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetRsi(layer, rsi, state); + } + + public void LayerSetRsi(Layer layer, ResPath rsi, StateId? state = null) + { + if (!_resourceCache.TryGetResource(TextureRoot / rsi, out var res)) + Log.Error($"Unable to load RSI '{rsi}' for entity {ToPrettyString(layer.Owner)}. Trace:\n{Environment.StackTrace}"); + + LayerSetRsi(layer, res?.RSI, state); + } + + #endregion + + #region Scale + + public void LayerSetScale(Entity sprite, int index, Vector2 value) + { + if (TryGetLayer(sprite, index, out var layer, true)) + LayerSetScale(layer, value); + } + + public void LayerSetScale(Entity sprite, string key, Vector2 value) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetScale(layer, value); + } + + public void LayerSetScale(Entity sprite, Enum key, Vector2 value) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetScale(layer, value); + } + + public void LayerSetScale(Layer layer, Vector2 value) + { + DebugTools.Assert(layer.Owner != default); + DebugTools.AssertNotNull(layer.Owner.Comp); + DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer); + + if (layer._scale.EqualsApprox(value)) + return; + + if (!ValidateScale(layer.Owner, value)) + return; + + layer._scale = value; + layer.UpdateLocalMatrix(); + _tree.QueueTreeUpdate(layer.Owner); + layer.BoundsDirty = true; + layer.Owner.Comp.BoundsDirty = true; + } + + #endregion + + #region Rotation + + public void LayerSetRotation(Entity sprite, int index, Angle value) + { + if (TryGetLayer(sprite, index, out var layer, true)) + LayerSetRotation(layer, value); + } + + public void LayerSetRotation(Entity sprite, string key, Angle value) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetRotation(layer, value); + } + + public void LayerSetRotation(Entity sprite, Enum key, Angle value) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetRotation(layer, value); + } + + public void LayerSetRotation(Layer layer, Angle value) + { + DebugTools.Assert(layer.Owner != default); + DebugTools.AssertNotNull(layer.Owner.Comp); + DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer); + + if (layer._rotation.EqualsApprox(value)) + return; + + layer._rotation = value; + layer.UpdateLocalMatrix(); + _tree.QueueTreeUpdate(layer.Owner); + layer.BoundsDirty = true; + layer.Owner.Comp.BoundsDirty = true; + } + + #endregion + + #region Offset + + public void LayerSetOffset(Entity sprite, int index, Vector2 value) + { + if (TryGetLayer(sprite, index, out var layer, true)) + LayerSetOffset(layer, value); + } + + public void LayerSetOffset(Entity sprite, string key, Vector2 value) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetOffset(layer, value); + } + + public void LayerSetOffset(Entity sprite, Enum key, Vector2 value) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetOffset(layer, value); + } + + public void LayerSetOffset(Layer layer, Vector2 value) + { + DebugTools.Assert(layer.Owner != default); + DebugTools.AssertNotNull(layer.Owner.Comp); + DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer); + + if (layer._offset.EqualsApprox(value)) + return; + + layer._offset = value; + layer.UpdateLocalMatrix(); + _tree.QueueTreeUpdate(layer.Owner); + layer.BoundsDirty = true; + layer.Owner.Comp.BoundsDirty = true; + } + + #endregion + + #region Visible + + public void LayerSetVisible(Entity sprite, int index, bool value) + { + if (TryGetLayer(sprite, index, out var layer, true)) + LayerSetVisible(layer, value); + } + + public void LayerSetVisible(Entity sprite, string key, bool value) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetVisible(layer, value); + } + + public void LayerSetVisible(Entity sprite, Enum key, bool value) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetVisible(layer, value); + } + + public void LayerSetVisible(Layer layer, bool value) + { + DebugTools.Assert(layer.Owner != default); + DebugTools.AssertNotNull(layer.Owner.Comp); + DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer); + + if (layer._visible == value) + return; + + layer._visible = value; + QueueUpdateIsInert(layer.Owner); + _tree.QueueTreeUpdate(layer.Owner); + layer.Owner.Comp.BoundsDirty = true; + } + + #endregion + + #region Color + + public void LayerSetColor(Entity sprite, int index, Color value) + { + if (TryGetLayer(sprite, index, out var layer, true)) + LayerSetColor(layer, value); + } + + public void LayerSetColor(Entity sprite, string key, Color value) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetColor(layer, value); + } + + public void LayerSetColor(Entity sprite, Enum key, Color value) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetColor(layer, value); + } + + public void LayerSetColor(Layer layer, Color value) + { + DebugTools.Assert(layer.Owner != default); + DebugTools.AssertNotNull(layer.Owner.Comp); + DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer); + + layer.Color = value; + } + + #endregion + + #region DirOffset + + public void LayerSetDirOffset(Entity sprite, int index, DirectionOffset value) + { + if (TryGetLayer(sprite, index, out var layer, true)) + LayerSetDirOffset(layer, value); + } + + public void LayerSetDirOffset(Entity sprite, string key, DirectionOffset value) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetDirOffset(layer, value); + } + + public void LayerSetDirOffset(Entity sprite, Enum key, DirectionOffset value) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetDirOffset(layer, value); + } + + public void LayerSetDirOffset(Layer layer, DirectionOffset value) + { + DebugTools.Assert(layer.Owner != default); + DebugTools.AssertNotNull(layer.Owner.Comp); + DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer); + + layer.DirOffset = value; + } + + #endregion + + #region AnimationTime + + public void LayerSetAnimationTime(Entity sprite, int index, float value) + { + if (TryGetLayer(sprite, index, out var layer, true)) + LayerSetAnimationTime(layer, value); + } + + public void LayerSetAnimationTime(Entity sprite, string key, float value) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetAnimationTime(layer, value); + } + + public void LayerSetAnimationTime(Entity sprite, Enum key, float value) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetAnimationTime(layer, value); + } + + public void LayerSetAnimationTime(Layer layer, float value) + { + DebugTools.Assert(layer.Owner != default); + DebugTools.AssertNotNull(layer.Owner.Comp); + DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer); + + if (!layer.StateId.IsValid) + return; + + if (layer.ActualRsi is not { } rsi) + return; + + var state = rsi[layer.StateId]; + if (value > layer.AnimationTime) + { + // Handle advancing differently from going backwards. + layer.AnimationTimeLeft -= (value - layer.AnimationTime); + } + else + { + // Going backwards we re-calculate from zero. + // Definitely possible to optimize this for going backwards but I'm too lazy to figure that out. + layer.AnimationTimeLeft = -value + state.GetDelay(0); + layer.AnimationFrame = 0; + } + + layer.AnimationTime = value; + layer.AdvanceFrameAnimation(state); + layer.SetAnimationTime(value); + } + + #endregion + + #region AutoAnimated + + public void LayerSetAutoAnimated(Entity sprite, int index, bool value) + { + if (TryGetLayer(sprite, index, out var layer, true)) + LayerSetAutoAnimated(layer, value); + } + + public void LayerSetAutoAnimated(Entity sprite, string key, bool value) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetAutoAnimated(layer, value); + } + + public void LayerSetAutoAnimated(Entity sprite, Enum key, bool value) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetAutoAnimated(layer, value); + } + + public void LayerSetAutoAnimated(Layer layer, bool value) + { + DebugTools.Assert(layer.Owner != default); + DebugTools.AssertNotNull(layer.Owner.Comp); + DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer); + + if (layer._autoAnimated == value) + return; + + layer._autoAnimated = value; + QueueUpdateIsInert(layer.Owner); + } + + #endregion + + #region LayerSetRenderingStrategy + + public void LayerSetRenderingStrategy(Entity sprite, int index, LayerRenderingStrategy value) + { + if (TryGetLayer(sprite, index, out var layer, true)) + LayerSetRenderingStrategy(layer, value); + } + + public void LayerSetRenderingStrategy(Entity sprite, string key, LayerRenderingStrategy value) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetRenderingStrategy(layer, value); + } + + public void LayerSetRenderingStrategy(Entity sprite, Enum key, LayerRenderingStrategy value) + { + if (TryGetLayer(sprite, key, out var layer, true)) + LayerSetRenderingStrategy(layer, value); + } + + public void LayerSetRenderingStrategy(Layer layer, LayerRenderingStrategy value) + { + DebugTools.Assert(layer.Owner != default); + DebugTools.AssertNotNull(layer.Owner.Comp); + DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer); + + layer.RenderingStrategy = value; + layer.BoundsDirty = true; + layer.Owner.Comp.BoundsDirty = true; + _tree.QueueTreeUpdate(layer.Owner); + } + + #endregion + + /// + /// Refreshes an RSI layer's cached RSI state. + /// + private void RefreshCachedState(Layer layer, bool logErrors, RSI.State? fallback) + { + if (!layer.StateId.IsValid) + { + layer._actualState = null; + } + else if (layer.ActualRsi is not { } rsi) + { + layer._actualState = fallback ?? GetFallbackState(); + if (logErrors) + Log.Error( + $"{ToPrettyString(layer.Owner)} has no RSI to pull new state from! Trace:\n{Environment.StackTrace}"); + } + else if (!rsi.TryGetState(layer.StateId, out layer._actualState)) + { + layer._actualState = fallback ?? GetFallbackState(); + if (logErrors) + Log.Error( + $"{ToPrettyString(layer.Owner)}'s state '{layer.StateId}' does not exist in RSI {rsi.Path}. Trace:\n{Environment.StackTrace}"); + } + + layer.AnimationFrame = 0; + layer.AnimationTime = 0; + layer.AnimationTimeLeft = layer._actualState?.GetDelay(0) ?? 0f; + } +} diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Render.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Render.cs new file mode 100644 index 00000000000..d7772cf6626 --- /dev/null +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Render.cs @@ -0,0 +1,183 @@ +using System.Numerics; +using Robust.Client.Graphics; +using Robust.Client.Graphics.Clyde; +using Robust.Client.Utility; +using Robust.Shared.GameObjects; +using Robust.Shared.Graphics.RSI; +using Robust.Shared.Maths; +using static Robust.Client.GameObjects.SpriteComponent; +using Vector4 = Robust.Shared.Maths.Vector4; + +namespace Robust.Client.GameObjects; + +// This partial class contains code related to actually rendering sprites. +public sealed partial class SpriteSystem +{ + public void RenderSprite( + Entity sprite, + DrawingHandleWorld drawingHandle, + Angle eyeRotation, + Angle worldRotation, + Vector2 worldPosition) + { + RenderSprite(sprite, + drawingHandle, + eyeRotation, + worldRotation, + worldPosition, + sprite.Comp.EnableDirectionOverride ? sprite.Comp.DirectionOverride : null); + } + + public void RenderSprite( + Entity sprite, + DrawingHandleWorld drawingHandle, + Angle eyeRotation, + Angle worldRotation, + Vector2 worldPosition, + Direction? overrideDirection) + { + // TODO SPRITE RENDERING + // Add fast path for simple sprites. + // I.e., when a sprite is modified, check if it is "simple". If it is. cache texture information in a struct + // and use a fast path here. + // E.g., simple 1-directional, 1-layer sprites can basically become a direct texture draw call. (most in game items). + // Similarly, 1-directional multi-layer sprites can become a sequence of direct draw calls (most in game walls). + + if (!sprite.Comp.IsInert) + _queuedFrameUpdate.Add(sprite); + + var angle = worldRotation + eyeRotation; // angle on-screen. Used to decide the direction of 4/8 directional RSIs + angle = angle.Reduced().FlipPositive(); // Reduce the angles to fix math shenanigans + + var cardinal = Angle.Zero; + + // If we have a 1-directional sprite then snap it to try and always face it south if applicable. + if (sprite.Comp is {NoRotation: false, SnapCardinals: true}) + cardinal = angle.RoundToCardinalAngle(); + + // worldRotation + eyeRotation should be the angle of the entity on-screen. If no-rot is enabled this is just set to zero. + // However, at some point later the eye-matrix is applied separately, so we subtract -eye rotation for now: + var entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, sprite.Comp.NoRotation ? -eyeRotation : worldRotation - cardinal); + var spriteMatrix = Matrix3x2.Multiply(sprite.Comp.LocalMatrix, entityMatrix); + + // Fast path for when all sprites use the same transform matrix + if (!sprite.Comp.GranularLayersRendering) + { + foreach (var layer in sprite.Comp.Layers) + { + RenderLayer(layer, drawingHandle, ref spriteMatrix, angle, overrideDirection); + } + return; + } + + //Default rendering (NoRotation = false) + entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, worldRotation); + var transformDefault = Matrix3x2.Multiply(sprite.Comp.LocalMatrix, entityMatrix); + + //Snap to cardinals + entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, worldRotation - angle.RoundToCardinalAngle()); + var transformSnap = Matrix3x2.Multiply(sprite.Comp.LocalMatrix, entityMatrix); + + //No rotation + entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, -eyeRotation); + var transformNoRot = Matrix3x2.Multiply(sprite.Comp.LocalMatrix, entityMatrix); + + foreach (var layer in sprite.Comp.Layers) + { + switch (layer.RenderingStrategy) + { + case LayerRenderingStrategy.UseSpriteStrategy: + RenderLayer(layer, drawingHandle, ref spriteMatrix, angle, overrideDirection); + break; + case LayerRenderingStrategy.Default: + RenderLayer(layer, drawingHandle, ref transformDefault, angle, overrideDirection); + break; + case LayerRenderingStrategy.NoRotation: + RenderLayer(layer, drawingHandle, ref transformNoRot, angle, overrideDirection); + break; + case LayerRenderingStrategy.SnapToCardinals: + RenderLayer(layer, drawingHandle, ref transformSnap, angle, overrideDirection); + break; + default: + Log.Error($"Tried to render a layer with unknown rendering stragegy: {layer.RenderingStrategy}"); + break; + } + } + } + + /// + /// Render a layer. This assumes that the input angle is between 0 and 2pi. + /// + private void RenderLayer(Layer layer, DrawingHandleWorld drawingHandle, ref Matrix3x2 spriteMatrix, Angle angle, Direction? overrideDirection) + { + if (!layer.Visible || layer.Blank) + return; + + var state = layer._actualState; + var dir = state == null ? RsiDirection.South : Layer.GetDirection(state.RsiDirections, angle); + + // Set the drawing transform for this layer + layer.GetLayerDrawMatrix(dir, out var layerMatrix, layer.Owner.Comp.NoRotation); + + // The direction used to draw the sprite can differ from the one that the angle would naively suggest, + // due to direction overrides or offsets. + if (overrideDirection != null && state != null) + dir = overrideDirection.Value.Convert(state.RsiDirections); + dir = dir.OffsetRsiDir(layer.DirOffset); + + var texture = state?.GetFrame(dir, layer.AnimationFrame) ?? layer.Texture ?? GetFallbackTexture(); + + // TODO SPRITE + // Refactor shader-param-layers to a separate layer type after layers are split into types & collections. + // I.e., separate Layer -> RsiLayer, TextureLayer, LayerCollection, SpriteLayer, and ShaderLayer + if (layer.CopyToShaderParameters != null) + { + HandleShaderLayer(layer, texture, layer.CopyToShaderParameters); + return; + } + + // Set the drawing transform for this layer + var transformMatrix = Matrix3x2.Multiply(layerMatrix, spriteMatrix); + drawingHandle.SetTransform(in transformMatrix); + + if (layer.Shader != null) + drawingHandle.UseShader(layer.Shader); + + var layerColor = layer.Owner.Comp.color * layer.Color; + var textureSize = texture.Size / (float) EyeManager.PixelsPerMeter; + var quad = Box2.FromDimensions(textureSize / -2, textureSize); + + drawingHandle.DrawTextureRectRegion(texture, quad, layerColor); + + if (layer.Shader != null) + drawingHandle.UseShader(null); + } + + /// + /// Handle a a "fake layer" that just exists to modify the parameters of a shader being used by some other + /// layer. + /// + private void HandleShaderLayer(Layer layer, Texture texture, CopyToShaderParameters @params) + { + // Multiple atrocities to god being committed right here. + var otherLayerIdx = layer._parent.LayerMap[@params.LayerKey!]; + var otherLayer = layer._parent.Layers[otherLayerIdx]; + if (otherLayer.Shader is not { } shader) + return; + + if (!shader.Mutable) + otherLayer.Shader = shader = shader.Duplicate(); + + var clydeTexture = Clyde.RenderHandle.ExtractTexture(texture, null, out var csr); + + if (@params.ParameterTexture is { } paramTexture) + shader.SetParameter(paramTexture, clydeTexture); + + if (@params.ParameterUV is not { } paramUV) + return; + + var sr = Clyde.RenderHandle.WorldTextureBoundsToUV(clydeTexture, csr); + var uv = new Vector4(sr.Left, sr.Bottom, sr.Right, sr.Top); + shader.SetParameter(paramUV, uv); + } +} diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Setters.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Setters.cs new file mode 100644 index 00000000000..75b2571cfea --- /dev/null +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Setters.cs @@ -0,0 +1,166 @@ +using System; +using System.Numerics; +using Robust.Client.Graphics; +using Robust.Shared.GameObjects; +using Robust.Shared.Maths; + +namespace Robust.Client.GameObjects; + +// This partial class contains various public methods for setting sprite component data. +public sealed partial class SpriteSystem +{ + private bool ValidateScale(Entity sprite, Vector2 scale) + { + if (!(MathF.Abs(scale.X) < 0.005f) && !(MathF.Abs(scale.Y) < 0.005f)) + return true; + + // Scales of ~0.0025 or lower can lead to singular matrices due to rounding errors. + Log.Error( + $"Attempted to set layer sprite scale to very small values. Entity: {ToPrettyString(sprite)}. Scale: {scale}"); + + return false; + } + + #region Transform + public void SetScale(Entity sprite, Vector2 value) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return; + + if (!ValidateScale(sprite!, value)) + return; + + sprite.Comp._bounds = sprite.Comp._bounds.Scale(value / sprite.Comp.scale); + sprite.Comp.scale = value; + sprite.Comp.LocalMatrix = Matrix3Helpers.CreateTransform( + in sprite.Comp.offset, + in sprite.Comp.rotation, + in sprite.Comp.scale); + } + + public void SetRotation(Entity sprite, Angle value) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return; + + sprite.Comp.rotation = value; + sprite.Comp.LocalMatrix = Matrix3Helpers.CreateTransform( + in sprite.Comp.offset, + in sprite.Comp.rotation, + in sprite.Comp.scale); + } + + public void SetOffset(Entity sprite, Vector2 value) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return; + + sprite.Comp.offset = value; + sprite.Comp.LocalMatrix = Matrix3Helpers.CreateTransform( + in sprite.Comp.offset, + in sprite.Comp.rotation, + in sprite.Comp.scale); + } + #endregion + + public void SetVisible(Entity sprite, bool value) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return; + + if (sprite.Comp.Visible == value) + return; + + sprite.Comp._visible = value; + _tree.QueueTreeUpdate(sprite!); + } + + public void SetDrawDepth(Entity sprite, int value) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return; + + sprite.Comp.drawDepth = value; + } + + public void SetColor(Entity sprite, Color value) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return; + + sprite.Comp.color = value; + } + + /// + /// Modify a sprites base RSI. This is the RSI that is used by any RSI layers that do not specify their own. + /// Note that changing the base RSI may result in existing layers having an invalid state. This will not log errors + /// under the assumption that the states of each layers will be updated after the base RSI has changed. + /// + public void SetBaseRsi(Entity sprite, RSI? value) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return; + + if (value == sprite.Comp._baseRsi) + return; + + sprite.Comp._baseRsi = value; + if (value == null) + return; + + var fallback = GetFallbackState(); + for (var i = 0; i < sprite.Comp.Layers.Count; i++) + { + var layer = sprite.Comp.Layers[i]; + if (!layer.State.IsValid || layer.RSI != null) + continue; + + RefreshCachedState(layer, logErrors: false, fallback); + + if (value.TryGetState(layer.State, out var state)) + { + layer.AnimationTimeLeft = state.GetDelay(0); + } + else + { + Log.Error($"Layer {i} no longer has state '{layer.State}' due to base RSI change. Trace:\n{Environment.StackTrace}"); + layer.Texture = null; + } + } + } + + public void SetContainerOccluded(Entity sprite, bool value) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return; + + sprite.Comp._containerOccluded = value; + _tree.QueueTreeUpdate(sprite!); + } + + public void SetSnapCardinals(Entity sprite, bool value) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return; + + if (value == sprite.Comp._snapCardinals) + return; + + sprite.Comp._snapCardinals = value; + _tree.QueueTreeUpdate(sprite!); + DirtyBounds(sprite!); + } + + public void SetGranularLayersRendering(Entity sprite, bool value) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) + return; + + if (value == sprite.Comp.GranularLayersRendering) + return; + + sprite.Comp.GranularLayersRendering = value; + _tree.QueueTreeUpdate(sprite!); + DirtyBounds(sprite!); + } +} diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Specifier.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Specifier.cs deleted file mode 100644 index 1e4f27b9006..00000000000 --- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Specifier.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using System.Collections.Generic; -using JetBrains.Annotations; -using Robust.Client.Graphics; -using Robust.Client.ResourceManagement; -using Robust.Client.Utility; -using Robust.Shared.Graphics; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations; -using Robust.Shared.Utility; - -namespace Robust.Client.GameObjects; - -public sealed partial class SpriteSystem -{ - private readonly Dictionary _cachedPrototypeIcons = new(); - - public Texture Frame0(EntityPrototype prototype) - { - return GetPrototypeIcon(prototype).Default; - } - - public Texture Frame0(SpriteSpecifier specifier) - { - return RsiStateLike(specifier).Default; - } - - public IRsiStateLike RsiStateLike(SpriteSpecifier specifier) - { - switch (specifier) - { - case SpriteSpecifier.Texture tex: - return tex.GetTexture(_resourceCache); - - case SpriteSpecifier.Rsi rsi: - return GetState(rsi); - - case SpriteSpecifier.EntityPrototype prototypeIcon: - return GetPrototypeIcon(prototypeIcon.EntityPrototypeId); - - default: - throw new NotSupportedException(); - } - } - - public Texture GetIcon(IconComponent icon) - { - return GetState(icon.Icon).Frame0; - } - - /// - /// Returns an icon for a given ID, or a fallback in case of an error. - /// This method caches the result based on the prototype identifier. - /// - public IRsiStateLike GetPrototypeIcon(string prototype) - { - // Check if this prototype has been cached before, and if so return the result. - if (_cachedPrototypeIcons.TryGetValue(prototype, out var cachedResult)) - return cachedResult; - - if (!_proto.TryIndex(prototype, out var entityPrototype)) - { - // The specified prototype doesn't exist, return the fallback "error" sprite. - _sawmill.Error("Failed to load PrototypeIcon {0}", prototype); - return GetFallbackState(); - } - - // Generate the icon and cache it in case it's ever needed again. - var result = GetPrototypeIcon(entityPrototype); - _cachedPrototypeIcons[prototype] = result; - - return result; - } - - /// - /// Returns an icon for a given ID, or a fallback in case of an error. - /// This method does NOT cache the result. - /// - public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype) - { - // IconComponent takes precedence. If it has a valid icon, return that. Otherwise, continue as normal. - if (prototype.Components.TryGetValue("Icon", out var compData) - && compData.Component is IconComponent icon) - { - return GetIcon(icon); - } - - // If the prototype doesn't have a SpriteComponent, then there's nothing we can do but return the fallback. - if (!prototype.Components.ContainsKey("Sprite")) - { - return GetFallbackState(); - } - - // Finally, we use spawn a dummy entity to get its icon. - var dummy = Spawn(prototype.ID, MapCoordinates.Nullspace); - var spriteComponent = EnsureComp(dummy); - var result = spriteComponent.Icon ?? GetFallbackState(); - Del(dummy); - - return result; - } - - [Pure] - public RSI.State GetFallbackState() - { - return _resourceCache.GetFallback().RSI["error"]; - } - - [Pure] - public RSI.State GetState(SpriteSpecifier.Rsi rsiSpecifier) - { - if (_resourceCache.TryGetResource( - SpriteSpecifierSerializer.TextureRoot / rsiSpecifier.RsiPath, - out var theRsi) && - theRsi.RSI.TryGetState(rsiSpecifier.RsiState, out var state)) - { - return state; - } - - _sawmill.Error("Failed to load RSI {0}", rsiSpecifier.RsiPath); - return GetFallbackState(); - } - - private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) - { - if (!args.TryGetModified(out var modified)) - return; - - // Remove all changed prototypes from the cache, if they're there. - foreach (var prototype in modified) - { - // Let's be lazy and not regenerate them until something needs them again. - _cachedPrototypeIcons.Remove(prototype); - } - } -} diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs index e92f0df258c..d3e0d9ae9fb 100644 --- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs @@ -13,10 +13,9 @@ using Robust.Shared.Graphics.RSI; using Robust.Shared.IoC; using Robust.Shared.Log; -using Robust.Shared.Map; using Robust.Shared.Maths; -using Robust.Shared.Physics; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations; using Robust.Shared.Timing; using Robust.Shared.Utility; using static Robust.Client.GameObjects.SpriteComponent; @@ -36,23 +35,19 @@ public sealed partial class SpriteSystem : EntitySystem [Dependency] private readonly IResourceCache _resourceCache = default!; [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly SharedTransformSystem _xforms = default!; + [Dependency] private readonly SpriteTreeSystem _tree = default!; 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 ISawmill _sawmill = default!; - - 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); - } + private EntityQuery _query; public override void Initialize() { @@ -61,11 +56,11 @@ public override void Initialize() UpdatesAfter.Add(typeof(SpriteTreeSystem)); SubscribeLocalEvent(OnPrototypesReloaded); - SubscribeLocalEvent(QueueUpdateInert); SubscribeLocalEvent(OnInit); Subs.CVar(_cfg, CVars.RenderSpriteDirectionBias, OnBiasChanged, true); _sawmill = _logManager.GetSawmill("sprite"); + _query = GetEntityQuery(); } public bool IsVisible(Layer layer) @@ -84,18 +79,6 @@ 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); - } - private void DoUpdateIsInert(SpriteComponent component) { component._inertUpdateQueued = false; diff --git a/Robust.Client/Graphics/Clyde/Clyde.HLR.cs b/Robust.Client/Graphics/Clyde/Clyde.HLR.cs index ea9271562a3..92ce6e6975f 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.HLR.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.HLR.cs @@ -304,7 +304,7 @@ private void DrawEntities(Viewport viewport, Box2Rotated worldBounds, Box2 world screenSpriteSize.Y++; bool exit = false; - if (entry.Sprite.GetScreenTexture) + if (entry.Sprite.GetScreenTexture && entry.Sprite.PostShader != null) { FlushRenderQueue(); var tex = CopyScreenTexture(viewport.RenderTarget); @@ -355,7 +355,7 @@ private void DrawEntities(Viewport viewport, Box2Rotated worldBounds, Box2 world } } - spriteSystem.Render(entry.Uid, entry.Sprite, _renderHandle.DrawingHandleWorld, eye.Rotation, in entry.WorldRot, in entry.WorldPos); + spriteSystem.RenderSprite(new(entry.Uid, entry.Sprite), _renderHandle.DrawingHandleWorld, eye.Rotation, entry.WorldRot, entry.WorldPos); if (entry.Sprite.PostShader != null && entityPostRenderTarget != null) { diff --git a/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs b/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs index af19a2c1a2e..bed45811eb0 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs @@ -574,6 +574,8 @@ private void DrawTexture(ClydeHandle texture, Vector2 bl, Vector2 br, Vector2 tl EnsureBatchSpaceAvailable(4, GetQuadBatchIndexCount()); EnsureBatchState(texture, true, GetQuadBatchPrimitiveType(), _queuedShader); + // TODO RENDERING + // It's probably better to do this on the GPU. bl = Vector2.Transform(bl, _currentMatrixModel); br = Vector2.Transform(br, _currentMatrixModel); tr = Vector2.Transform(tr, _currentMatrixModel); diff --git a/Robust.Client/Placement/PlacementMode.cs b/Robust.Client/Placement/PlacementMode.cs index f3e6ea67c76..e9371d13279 100644 --- a/Robust.Client/Placement/PlacementMode.cs +++ b/Robust.Client/Placement/PlacementMode.cs @@ -126,7 +126,7 @@ public virtual void Render(in OverlayDrawArgs args) sprite.Color = IsValidPosition(coordinate) ? ValidPlaceColor : InvalidPlaceColor; var rot = args.Viewport.Eye?.Rotation ?? default; - spriteSys.Render(uid.Value, sprite, args.WorldHandle, rot, worldRot, worldPos); + spriteSys.RenderSprite((uid.Value, sprite), args.WorldHandle, rot, worldRot, worldPos); } } diff --git a/Robust.Client/Utility/SpriteSpecifierExt.cs b/Robust.Client/Utility/SpriteSpecifierExt.cs index 412857a41ef..5909e9fc929 100644 --- a/Robust.Client/Utility/SpriteSpecifierExt.cs +++ b/Robust.Client/Utility/SpriteSpecifierExt.cs @@ -17,6 +17,7 @@ namespace Robust.Client.Utility /// public static class SpriteSpecifierExt { + [Obsolete("Use SpriteSystem.GetTexture() instead")] public static Texture GetTexture(this SpriteSpecifier.Texture texSpecifier, IResourceCache cache) { return cache @@ -24,13 +25,14 @@ public static Texture GetTexture(this SpriteSpecifier.Texture texSpecifier, IRes .Texture; } - [Obsolete("Use SpriteSystem")] + [Obsolete("Use SpriteSystem.GetState() instead")] public static RSI.State GetState(this SpriteSpecifier.Rsi rsiSpecifier, IResourceCache cache) { if (!cache.TryGetResource(SpriteSpecifierSerializer.TextureRoot / rsiSpecifier.RsiPath, out var theRsi)) { + var sys = IoCManager.Resolve().GetEntitySystem(); Logger.Error("SpriteSpecifier failed to load RSI {0}", rsiSpecifier.RsiPath); - return SpriteComponent.GetFallbackState(cache); + return sys.GetFallbackState(); } if (theRsi.RSI.TryGetState(rsiSpecifier.RsiState, out var state)) @@ -39,21 +41,22 @@ public static RSI.State GetState(this SpriteSpecifier.Rsi rsiSpecifier, IResourc } Logger.Error($"SpriteSpecifier has invalid RSI state '{rsiSpecifier.RsiState}' for RSI: {rsiSpecifier.RsiPath}"); - return SpriteComponent.GetFallbackState(cache); + return IoCManager.Resolve().GetEntitySystem().GetFallbackState(); } - [Obsolete("Use SpriteSystem")] + [Obsolete("Use SpriteSystem.Frame0() instead")] public static Texture Frame0(this SpriteSpecifier specifier) { return specifier.RsiStateLike().Default; } + [Obsolete("Use SpriteSystem.RsiStateLike() instead")] public static IDirectionalTextureProvider DirFrame0(this SpriteSpecifier specifier) { return specifier.RsiStateLike(); } - [Obsolete("Use SpriteSystem")] + [Obsolete("Use SpriteSystem.RsiStateLike() instead")] public static IRsiStateLike RsiStateLike(this SpriteSpecifier specifier) { var resC = IoCManager.Resolve(); @@ -67,10 +70,11 @@ public static IRsiStateLike RsiStateLike(this SpriteSpecifier specifier) case SpriteSpecifier.EntityPrototype prototypeIcon: var protMgr = IoCManager.Resolve(); + var sys = IoCManager.Resolve().GetEntitySystem(); if (!protMgr.TryIndex(prototypeIcon.EntityPrototypeId, out var prototype)) { Logger.Error("Failed to load PrototypeIcon {0}", prototypeIcon.EntityPrototypeId); - return SpriteComponent.GetFallbackState(resC); + return sys.GetFallbackState(); } return SpriteComponent.GetPrototypeIcon(prototype, resC); diff --git a/Robust.Shared.Maths/Angle.cs b/Robust.Shared.Maths/Angle.cs index eb99fc5cea6..49cf7bb0b4e 100644 --- a/Robust.Shared.Maths/Angle.cs +++ b/Robust.Shared.Maths/Angle.cs @@ -104,6 +104,16 @@ public readonly Direction GetCardinalDir() return (Direction) (Math.Floor((ang + CardinalOffset) / CardinalSegment) * 2 % 8); } + /// + /// Rounds the angle to the nearest cardinal direction. This behaves similarly to a combination of + /// and Direction.ToAngle(), however this may return an angle outside of the range + /// returned by those methods (-pi to pi). + /// + public Angle RoundToCardinalAngle() + { + return new Angle(CardinalSegment * Math.Floor((Theta + CardinalOffset) / CardinalSegment)); + } + /// /// Rotates the vector counter-clockwise around its origin by the value of Theta. /// diff --git a/Robust.Shared/ComponentTrees/ComponentTreeSystem.cs b/Robust.Shared/ComponentTrees/ComponentTreeSystem.cs index 45a418d71a6..6a737675257 100644 --- a/Robust.Shared/ComponentTrees/ComponentTreeSystem.cs +++ b/Robust.Shared/ComponentTrees/ComponentTreeSystem.cs @@ -111,6 +111,11 @@ public void QueueTreeUpdate(EntityUid uid, TComp component, TransformComponent? component.TreeUpdateQueued = true; _updateQueue.Enqueue((component, xform)); } + + public void QueueTreeUpdate(Entity entity, TransformComponent? xform = null) + { + QueueTreeUpdate(entity.Owner, entity.Comp, xform); + } #endregion #region Component Management diff --git a/Robust.Shared/GameObjects/Components/Renderable/SpriteLayerData.cs b/Robust.Shared/GameObjects/Components/Renderable/SpriteLayerData.cs index 531a28a19c3..00e3ff38b33 100644 --- a/Robust.Shared/GameObjects/Components/Renderable/SpriteLayerData.cs +++ b/Robust.Shared/GameObjects/Components/Renderable/SpriteLayerData.cs @@ -80,4 +80,8 @@ public enum LayerRenderingStrategy SnapToCardinals, NoRotation, UseSpriteStrategy + // TODO SPRITE + // Refactor this make the sprites strategy the actual default. + // That way layers have to opt in to having a custom strategy, instead of opt out. + // Also rename default to make it clear that its not actually the default, instead I guess its "WithRotation"? } diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/SpriteSpecifierSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/SpriteSpecifierSerializer.cs index a9831acba4b..92c1f42ca1e 100644 --- a/Robust.Shared/Serialization/TypeSerializers/Implementations/SpriteSpecifierSerializer.cs +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/SpriteSpecifierSerializer.cs @@ -25,7 +25,7 @@ public abstract class SpriteSpecifierSerializer : ITypeCopier, ITypeCopier { - // Should probably be in SpriteComponent, but is needed for server to validate paths. + // Should probably be in SpriteSystem, but is needed for server to validate paths. // So I guess it might as well go here? public static readonly ResPath TextureRoot = new("/Textures");