using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; 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; 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.Reflection; using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth; using static Robust.Shared.Serialization.TypeSerializers.Implementations.SpriteSpecifierSerializer; using Direction = Robust.Shared.Maths.Direction; #pragma warning disable CS0618 // Type or member is obsolete namespace Robust.Client.GameObjects { [RegisterComponent] public sealed partial class SpriteComponent : Component, IComponentDebug, ISerializationHooks, IComponentTreeEntry, IAnimationProperties { public const string LogCategory = "go.comp.sprite"; [Dependency] private IResourceCache resourceCache = default!; [Dependency] private IPrototypeManager prototypes = default!; [Dependency] private EntityManager entities = default!; [Dependency] private IReflectionManager reflection = default!; /// /// See . /// public static double DirectionBias = -0.05; /// /// 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] // TODO Sprite access restrict. public bool GranularLayersRendering = false; [DataField("visible")] internal bool _visible = true; // VV convenience variable to examine layer objects using layer keys // ReSharper disable once UnusedMember.Local [ViewVariables] private Dictionary MappedLayers => LayerMap.ToDictionary(x => x.Key, x => Layers[x.Value]); [ViewVariables(VVAccess.ReadWrite)] public bool Visible { get => _visible; [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))] internal int drawDepth = DrawDepthTag.Default; /// /// Z-index for drawing. /// [ViewVariables(VVAccess.ReadWrite)] public int DrawDepth { get => drawDepth; [Obsolete("Use SpriteSystem.SetDrawDepth() instead.")] set => Sys.SetDrawDepth((Owner, this), value); } [DataField("scale")] // Explicit name, in case this field ever gets renamed internal Vector2 scale = Vector2.One; /// /// A scale applied to all layers. /// [Animatable] [ViewVariables(VVAccess.ReadWrite)] public Vector2 Scale { get => scale; [Obsolete("Use SpriteSystem.SetScale() instead.")] set => Sys.SetScale((Owner, this), value); } [DataField("rotation")] // Explicit name, in case this field ever gets renamed internal Angle rotation = Angle.Zero; [Animatable] [ViewVariables(VVAccess.ReadWrite)] public Angle Rotation { get => rotation; [Obsolete("Use SpriteSystem.SetRotation() instead.")] set => Sys.SetRotation((Owner, this), value); } [DataField("offset")] // Explicit name, in case this field ever gets renamed internal Vector2 offset = Vector2.Zero; /// /// Offset applied to all layers. /// [Animatable] [ViewVariables(VVAccess.ReadWrite)] public Vector2 Offset { get => offset; [Obsolete("Use SpriteSystem.SetOffset() instead.")] set => Sys.SetOffset((Owner, this), value); } [DataField("color")] // Explicit name, in case this field ever gets renamed internal Color color = Color.White; [Animatable] [ViewVariables(VVAccess.ReadWrite)] public Color Color { get => color; [Obsolete("Use SpriteSystem.SetColor() instead.")] set => Sys.SetColor((Owner, this), value); } public Matrix3x2 LocalMatrix = Matrix3x2.Identity; [ViewVariables] public DynamicTree>? Tree { get; set; } public EntityUid? TreeUid { get; set; } public bool AddToTree => Visible && !ContainerOccluded && Layers.Count > 0; public bool TreeUpdateQueued { get; set; } internal RSI? _baseRsi; [ViewVariables(VVAccess.ReadWrite)] public RSI? BaseRSI { get => _baseRsi; [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(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? /// [DataField("overrideContainerOcclusion")] [ViewVariables(VVAccess.ReadWrite)] public bool OverrideContainerOcclusion; [ViewVariables(VVAccess.ReadWrite)] public bool ContainerOccluded { get => _containerOccluded && !OverrideContainerOcclusion; [Obsolete("Use SpriteSystem.SetContainerOccluded() instead.")] set => Sys.SetContainerOccluded((Owner, this), value); } internal bool _containerOccluded; /// /// Whether or not the sprite's local bounding box is dirty and need to be rebuilt. /// 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; /// /// Shader instance to use when drawing the final sprite to the world. /// [ViewVariables(VVAccess.ReadWrite)] 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 to pass the screen texture to the . /// /// /// Should be false unless you really need it. /// [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] public bool RaiseShaderEvent; [ViewVariables] internal Dictionary LayerMap { get; set; } = new(); [ViewVariables] internal List Layers = new(); [ViewVariables(VVAccess.ReadWrite)] public uint RenderOrder { get; set; } [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)) { 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) { if (state != null || texture != null) { layerDatums.Insert(0, new PrototypeLayerData { TexturePath = string.IsNullOrWhiteSpace(texture) ? null : texture, State = string.IsNullOrWhiteSpace(state) ? null : state, Color = Color.White, Scale = Vector2.One, Visible = true, RenderingStrategy = LayerRenderingStrategy.UseSpriteStrategy, Cycle = false, }); state = null; texture = null; } } if (layerDatums.Count != 0) { LayerMap.Clear(); Layers.Clear(); foreach (var datum in layerDatums) { var layer = new Layer((Owner, this), Layers.Count); Layers.Add(layer); LayerSetData(layer, datum); } } BoundsDirty = true; LocalMatrix = Matrix3Helpers.CreateTransform(in offset, in rotation, in scale); } /// /// If false, this will prevent any of this sprite's animated layers from looping their animation. /// This will set whenever any layer's animation finishes. /// /// /// If this is false, this effectively overrides each layer's own . /// [DataField] public bool Loop = true; /// /// Update this sprite component to visibly match the current state of other at the time /// 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) { 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) { throw new ArgumentOutOfRangeException(); } LayerMap.Add(key, layer); } [Obsolete("Use SpriteSystem.LayerMapRemove() instead.")] public void LayerMapRemove(object key) { 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); if (!result && logError) { Logger.ErrorS(LogCategory, "{0} - Layer with key '{1}' does not exist! Trace:\n{2}", entities.ToPrettyString(Owner), key, Environment.StackTrace); } return result; } [Obsolete("Use SpriteSystem.TryGetLayer() instead.")] public bool TryGetLayer(int index, [NotNullWhen(true)] out Layer? layer, bool logError = false) => Sys.TryGetLayer((Owner, this), index, out layer, logError); [Obsolete("Use SpriteSystem.LayerExists() instead.")] public bool LayerExists(int layer, bool logError = true) => Sys.LayerExists((Owner, this), layer); [Obsolete("Use SpriteSystem.LayerExists() instead.")] public bool LayerExists(object key, bool logError = false) => LayerMapTryGet(key, out _, logError); [Obsolete("Use SpriteSystem.LayerMapReserve() instead.")] public int LayerMapReserveBlank(object key) { if (LayerMapTryGet(key, out var index)) { return index; } index = AddBlankLayer(); LayerMapSet(key, index); return index; } [Obsolete("Use SpriteSystem.AddBlankLayer() instead.")] public int AddBlankLayer(int? newIndex = null) => Sys.AddBlankLayer((Owner, this), newIndex).Index; [Obsolete("Use SpriteSystem.AddLayer() instead.")] public int AddLayer(PrototypeLayerData layerDatum, int? newIndex = null) => 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) => Sys.AddTextureLayer((Owner, this), texturePath, newIndex); [Obsolete("Use SpriteSystem.AddTextureLayer() instead.")] public int AddLayer(Texture? texture, int? newIndex = null) => Sys.AddTextureLayer((Owner, this), texture, newIndex); [Obsolete("Use SpriteSystem.AddRsiLayer() instead.")] public int AddLayer(RSI.StateId stateId, int? newIndex = null) => 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) => 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) => 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) => Sys.AddLayer((Owner, this), specifier, newIndex); [Obsolete("Use SpriteSystem.RemoveLayer() instead.")] public void RemoveLayer(int layer) => Sys.RemoveLayer((Owner, this), layer); [Obsolete("Use SpriteSystem.RemoveLayer() instead.")] public void RemoveLayer(object layerKey) { if (!LayerMapTryGet(layerKey, out var layer, true)) return; RemoveLayer(layer); } [DataField("snapCardinals")] internal bool _snapCardinals = false; /// /// 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); } /// /// 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) { DebugTools.AssertEqual(layer, layer.Owner.Comp.Layers[layer.Index]); if (!string.IsNullOrWhiteSpace(layerDatum.RsiPath)) { var path = TextureRoot / layerDatum.RsiPath; if (resourceCache.TryGetResource(path, out RSIResource? resource)) { layer.RSI = resource.RSI; } else { Logger.ErrorS(LogCategory, "Unable to load layer RSI '{0}'.", path); } } if (!string.IsNullOrWhiteSpace(layerDatum.State)) { var theRsi = layer.RSI ?? BaseRSI; if (theRsi == null) { Logger.ErrorS(LogCategory, "Layer has no RSI to load states from. Cannot use 'state' property. ({0})", layerDatum.State); } else { var stateid = new RSI.StateId(layerDatum.State); layer.State = stateid; if (theRsi.TryGetState(stateid, out var state)) { // Always use south because this layer will be cached in the serializer. layer.AnimationTimeLeft = state.GetDelay(0); layer.AnimationTime = 0; layer.AnimationFrame = 0; } else { Logger.ErrorS(LogCategory, $"State '{stateid}' not found in RSI: '{theRsi.Path}'.", stateid); } } } if (!string.IsNullOrWhiteSpace(layerDatum.TexturePath)) { if (layer.State.IsValid) { Logger.ErrorS(LogCategory, "Cannot specify 'texture' on a layer if it has an RSI state specified." ); } else { layer.Texture = resourceCache.GetResource(TextureRoot / layerDatum.TexturePath); } } if (layerDatum.Shader != null) { if (layerDatum.Shader == string.Empty) { layer.ShaderPrototype = null; layer.UnShaded = false; layer.Shader = null; } else if (layerDatum.Shader == SpriteSystem.UnshadedId.Id) { layer.ShaderPrototype = SpriteSystem.UnshadedId; layer.UnShaded = true; layer.Shader = null; } else if (prototypes.TryIndex(layerDatum.Shader, out var prototype)) { layer.ShaderPrototype = layerDatum.Shader; layer.Shader = prototype.Instance(); layer.UnShaded = false; } else { Logger.ErrorS(LogCategory, "Shader prototype '{0}' does not exist.", layerDatum.Shader); } } if (layerDatum.MapKeys != null) { foreach (var keyString in layerDatum.MapKeys) { var key = ParseKey(keyString); if (LayerMap.TryGetValue(key, out var mappedIndex)) { if (mappedIndex != layer.Index) Logger.ErrorS(LogCategory, "Duplicate layer map key definition: {0}", key); continue; } LayerMap[key] = layer.Index; } } layer.RenderingStrategy = layerDatum.RenderingStrategy ?? layer.RenderingStrategy; layer.Cycle = layerDatum.Cycle; layer.Loop = layerDatum.Loop; layer.Color = layerDatum.Color ?? layer.Color; layer._rotation = layerDatum.Rotation ?? layer._rotation; layer._offset = layerDatum.Offset ?? layer._offset; layer._scale = layerDatum.Scale ?? layer._scale; layer.UpdateLocalMatrix(); // If neither state: nor texture: were provided we assume that they want a blank invisible layer. layer.Visible = layerDatum.Visible ?? layer.Visible; if (layerDatum.CopyToShaderParameters is { } copyParameters) { layer.CopyToShaderParameters = new CopyToShaderParameters(ParseKey(copyParameters.LayerKey)) { ParameterTexture = copyParameters.ParameterTexture, ParameterUV = copyParameters.ParameterUV }; } else { layer.CopyToShaderParameters = null; } BoundsDirty = true; layer.BoundsDirty = true; // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract if (Owner != EntityUid.Invalid) TreeSys?.QueueTreeUpdate((Owner, this)); } private object ParseKey(string keyString) { if (reflection.TryParseEnumReference(keyString, out var @enum)) return @enum; return keyString; } [Obsolete("Use SpriteSystem.LayerSetData() instead.")] public void LayerSetData(object layerKey, PrototypeLayerData data) { if (!LayerMapTryGet(layerKey, out var layer, true)) return; LayerSetData(layer, data); } [Obsolete("Use SpriteSystem.LayerSetSprite() instead.")] public void LayerSetSprite(int layer, SpriteSpecifier specifier) => 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)) return; LayerSetSprite(layer, specifier); } [Obsolete("Use SpriteSystem.LayerSetTexture() instead.")] public void LayerSetTexture(int layer, Texture? texture) => 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)) return; 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) => 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)) return; LayerSetTexture(layer, texturePath); } [Obsolete("Use SpriteSystem.LayerSetRsiState() instead.")] public void LayerSetState(int layer, RSI.StateId stateId) => 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)) return; LayerSetState(layer, stateId); } [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)) return; 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) => 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)) return; LayerSetState(layer, stateId, rsiPath); } [Obsolete("Use SpriteSystem.LayerSetRsi() instead.")] public void LayerSetRSI(int layer, RSI? rsi) => 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)) return; 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) => 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)) return; LayerSetRSI(layer, rsiPath); } [Obsolete("Use SpriteSystem.LayerSetScale() instead.")] public void LayerSetScale(int layer, Vector2 scale) => 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)) return; LayerSetScale(layer, scale); } [Obsolete("Use SpriteSystem.LayerSetRotation() instead.")] public void LayerSetRotation(int layer, Angle rotation) => 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)) return; LayerSetRotation(layer, rotation); } [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 (!LayerMapTryGet(layerKey, out var layer, true)) return; 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)) return; LayerSetVisible(layer, visible); } [Obsolete("Use SpriteSystem.LayerSetColor() instead.")] public void LayerSetColor(int layer, Color color) => 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)) return; LayerSetColor(layer, color); } [Obsolete("Use SpriteSystem.LayerSetDirOffset() instead.")] public void LayerSetDirOffset(int layer, DirectionOffset offset) => 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)) return; LayerSetDirOffset(layer, offset); } [Obsolete("Use SpriteSystem.LayerSetAnimationTime() instead.")] public void LayerSetAnimationTime(int layer, float 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)) return; LayerSetAnimationTime(layer, animationTime); } [Obsolete("Use SpriteSystem.LayerSetAutoAnimated() instead.")] public void LayerSetAutoAnimated(int layer, bool 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)) return; LayerSetAutoAnimated(layer, autoAnimated); } [Obsolete("Use SpriteSystem.LayerSetRenderingStrategy() instead.")] public void LayerSetRenderingStrategy(int layer, LayerRenderingStrategy 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)) return; LayerSetRenderingStrategy(layer, renderingStrategy); } [Obsolete("Use SpriteSystem.LayerGetRsiState() instead.")] public RSI.StateId LayerGetState(int layer) { if (!TryGetLayer(layer, out var theLayer, true)) return default; 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 void LayerSetShader(int layer, ShaderInstance? shader, string? prototype = null) { if (!TryGetLayer(layer, out var theLayer, true)) return; if (shader == null) { theLayer.UnShaded = false; theLayer.Shader = null; theLayer.ShaderPrototype = null; return; } if (prototype == SpriteSystem.UnshadedId.Id) { theLayer.UnShaded = true; theLayer.ShaderPrototype = SpriteSystem.UnshadedId; theLayer.Shader = null; return; } theLayer.UnShaded = false; 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)) { Logger.ErrorS(LogCategory, "Shader prototype '{0}' does not exist. Trace:\n{1}", shaderName, Environment.StackTrace); LayerSetShader(layer, null, null); return; } LayerSetShader(layer, prototype.Instance(), shaderName); } public void LayerSetShader(object layerKey, string shaderName) { if (!LayerMapTryGet(layerKey, out var layer, true)) return; LayerSetShader(layer, shaderName); } // 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) return 1; // Pull texture from RSI state instead. var rsi = layer.Rsi ?? BaseRSI; if (rsi == null || !rsi.TryGetState(layer.RsiState, out var state)) { state = Sys.GetFallbackState(); } return state.RsiDirections switch { RsiDirectionType.Dir1 => 1, RsiDirectionType.Dir4 => 4, RsiDirectionType.Dir8 => 8, _ => throw new ArgumentOutOfRangeException() }; } public string GetDebugString() { var builder = new StringBuilder(); builder.Append( $"vis/depth/scl/rot/ofs/col/norot/override: {Visible}/{DrawDepth}/{Scale}/{Rotation}/{Offset}/{Color}/{NoRotation}/{DirectionOverride}/\n" ); foreach (var layer in Layers) { builder.AppendFormat( "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.State, layer.AnimationTimeLeft, layer.AnimationFrame, layer.Scale, layer.Rotation, layer.Visible, layer.Color, layer.DirOffset, layer.RenderingStrategy ); } return builder.ToString(); } [Obsolete("Use SpriteSystem.CalculateBounds() instead.")] public Box2Rotated CalculateRotatedBoundingBox(Vector2 worldPosition, Angle worldRotation, Angle eyeRot) { return Sys.CalculateBounds((Owner, this), worldPosition, worldRotation, eyeRot); } /// /// Enum to "offset" a cardinal direction. /// public enum DirectionOffset : byte { /// /// No offset. /// None = 0, /// /// Rotate direction clockwise. (North -> East, etc...) /// Clockwise = 1, /// /// Rotate direction counter-clockwise. (North -> West, etc...) /// CounterClockwise = 2, /// /// Rotate direction 180 degrees, so flip. (North -> South, etc...) /// Flip = 3, } public sealed class Layer : ISpriteLayer, ISerializationHooks { 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 ProtoId? ShaderPrototype; [ViewVariables] public ShaderInstance? Shader; [ViewVariables] public Texture? Texture; internal RSI? _rsi; /// /// If true, then this layer is drawn without lighting applied. /// Unshaded layers are given special treatment and don't just use the unshaded-shader to avoid having to /// unnecessarily swap out the light texture. This helps the number of batches that need to be sent to the /// GPU while drawing sprites. /// [ViewVariables] internal bool UnShaded; [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(); } } internal RSI.StateId StateId; [ViewVariables] public RSI.StateId State { get => StateId; [Obsolete("Use SpriteSystem.LayerSetRsiState() instead.")] set { if (StateId == value) return; StateId = value; UpdateActualState(); } } [ViewVariables] public float AnimationTimeLeft; [ViewVariables] public float AnimationTime; [ViewVariables] public int AnimationFrame; /// /// Is the animation currently playing in reverse. /// [ViewVariables] public bool Reversed { get; internal set; } /// /// If every animation delay finishes do we reverse it. /// /// /// Only applies if the state is auto-animated. /// [ViewVariables] public bool Cycle; /// /// If false, this will prevent the layer's animation from looping. /// This will set to false once the animation finishes. /// /// /// This may be overriden by the parent's loop property. /// [ViewVariables] public bool Loop = true; // 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; 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: {_parent.entities.ToPrettyString(_parent.Owner)}. Scale: {value}"); return; } _scale = value; UpdateLocalMatrix(); BoundsDirty = true; Owner.Comp.BoundsDirty = true; Owner.Comp.TreeSys.QueueTreeUpdate(Owner); } } internal Vector2 _scale = Vector2.One; [ViewVariables(VVAccess.ReadWrite)] public Angle Rotation { get => _rotation; [Obsolete("Use SpriteSystem.LayerSetRotation() instead.")] set { if (_rotation.EqualsApprox(value)) return; _rotation = value; UpdateLocalMatrix(); BoundsDirty = true; Owner.Comp.BoundsDirty = true; Owner.Comp.TreeSys.QueueTreeUpdate(Owner); } } internal Angle _rotation = Angle.Zero; // Is the layer actually drawn / does it contribute to the sprites bounding box? internal bool Drawn => _visible && !Blank && CopyToShaderParameters == null; internal bool _visible = true; [ViewVariables(VVAccess.ReadWrite)] public bool Visible { get => _visible; [Obsolete("Use SpriteSystem.LayerSetVisible() instead.")] set { if (_visible == value) return; _visible = value; Owner.Comp.BoundsDirty = true; // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract if (_parent.Owner != EntityUid.Invalid) Owner.Comp.Sys?.QueueUpdateIsInert(Owner); if (_parent.Owner != EntityUid.Invalid) Owner.Comp.TreeSys.QueueTreeUpdate(Owner); } } [ViewVariables] public bool Blank => !State.IsValid && Texture == null; [ViewVariables(VVAccess.ReadWrite)] public Color Color { get; set; } = Color.White; internal bool _autoAnimated = true; [ViewVariables(VVAccess.ReadWrite)] public bool AutoAnimated { get => _autoAnimated; [Obsolete("Use SpriteSystem.LayerSetAutoAnimated() instead.")] set { if (_autoAnimated == value) return; _autoAnimated = value; // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract if (_parent.Owner != EntityUid.Invalid) _parent.Sys?.QueueUpdateIsInert((_parent.Owner, _parent)); } } [ViewVariables(VVAccess.ReadWrite)] 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(); // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract if (_parent.Owner != EntityUid.Invalid) Owner.Comp.TreeSys.QueueTreeUpdate(Owner); } } internal Vector2 _offset; [ViewVariables] public DirectionOffset DirOffset { get; set; } [ViewVariables] public RSI? ActualRsi => RSI ?? _parent.BaseRSI; /// /// 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] // 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 change the sprites bounds. [ViewVariables(VVAccess.ReadWrite)] public CopyToShaderParameters? CopyToShaderParameters; [Obsolete("Use SpriteSystem.AddBlankLayer")] public Layer(SpriteComponent parent) { Owner = (parent.Owner, parent); } internal Layer() { } 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) { if (toClone.Shader != null) Shader = toClone.Shader.Mutable ? toClone.Shader.Duplicate() : toClone.Shader; UnShaded = toClone.UnShaded; ShaderPrototype = toClone.ShaderPrototype; Texture = toClone.Texture; RSI = toClone.RSI; State = toClone.State; AnimationTimeLeft = toClone.AnimationTimeLeft; AnimationTime = toClone.AnimationTime; AnimationFrame = toClone.AnimationFrame; _scale = toClone.Scale; _rotation = toClone.Rotation; _offset = toClone.Offset; UpdateLocalMatrix(); _visible = toClone._visible; Color = toClone.Color; DirOffset = toClone.DirOffset; _autoAnimated = toClone._autoAnimated; RenderingStrategy = toClone.RenderingStrategy; Cycle = toClone.Cycle; Loop = toClone.Loop; if (toClone.CopyToShaderParameters is { } copyToShaderParameters) CopyToShaderParameters = new CopyToShaderParameters(copyToShaderParameters); } // TODO SPRITE // Is Layer even serializable? void ISerializationHooks.AfterDeserialization() { UpdateLocalMatrix(); } internal void UpdateLocalMatrix() { LocalMatrix = Matrix3Helpers.CreateTransform(in _offset, in _rotation, in _scale); } RSI? ISpriteLayer.Rsi { get => RSI; set => SetRsi(value); } RSI.StateId ISpriteLayer.RsiState { get => State; set => SetState(value); } Texture? ISpriteLayer.Texture { get => Texture; set => SetTexture(value); } public PrototypeLayerData ToPrototypeData() { return new PrototypeLayerData { Color = Color, Rotation = Rotation, Scale = Scale, Shader = ShaderPrototype, State = State.Name, Visible = Visible, RsiPath = RSI?.Path.CanonPath, RenderingStrategy = RenderingStrategy, //todo TexturePath = Textur //todo MapKeys }; } float ISpriteLayer.AnimationTime { get => AnimationTime; set => SetAnimationTime(value); } int ISpriteLayer.AnimationFrame => AnimationFrame; public RsiDirection EffectiveDirection(Angle worldRotation) { if (State == default) { return default; } var rsi = ActualRsi; if (rsi == null) { return default; } if (rsi.TryGetState(State, out var state)) { return EffectiveDirection(state, worldRotation, null); } return default; } public RsiDirection EffectiveDirection(RSI.State state, Angle worldRotation, Direction? overrideDirection) { if (state.RsiDirections == RsiDirectionType.Dir1) { return RsiDirection.South; } else { RsiDirection dir; if (overrideDirection != null) { dir = overrideDirection.Value.Convert(state.RsiDirections); } else { dir = worldRotation.ToRsiDirection(state.RsiDirections); } return dir.OffsetRsiDir(DirOffset); } } [Obsolete("Use SpriteSystem.LayerSetAnimationTime")] public void SetAnimationTime(float animationTime) { if (!State.IsValid) { return; } var theLayerRSI = ActualRsi; if (theLayerRSI == null) { return; } var state = theLayerRSI[State]; if (animationTime > AnimationTime) { // Handle advancing differently from going backwards. AnimationTimeLeft -= (animationTime - 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. AnimationTimeLeft = -animationTime + state.GetDelay(0); AnimationFrame = 0; } AnimationTime = animationTime; AdvanceFrameAnimation(state); } [Obsolete("Use SpriteSystem.LayerSetAutoAnimated")] public void SetAutoAnimated(bool value) { AutoAnimated = value; _parent.Sys.QueueUpdateIsInert((_parent.Owner, _parent)); } [Obsolete("Use SpriteSystem.LayerSetRsi")] public void SetRsi(RSI? rsi) { RSI = rsi; if (!State.IsValid) { return; } // Gotta do this because somebody might use null as argument (totally valid). var actualRsi = ActualRsi; if (actualRsi == null) { Logger.ErrorS(LogCategory, "No RSI to pull new state from! Trace:\n{0}", Environment.StackTrace); Texture = null; } else { if (actualRsi.TryGetState(State, out var state)) { AnimationTimeLeft = state.GetDelay(0); } else { Logger.ErrorS(LogCategory, "State '{0}' does not exist in set RSI ({1}). Trace:\n{2}", State, rsi?.Path.CanonPath ?? "null", Environment.StackTrace); Texture = null; } } 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) { return; } State = stateId; RSI.State? state; var rsi = ActualRsi; if (rsi == null) { 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 = _parent.Sys.GetFallbackState(); Logger.ErrorS(LogCategory, "State '{0}' does not exist in RSI. Trace:\n{1}", stateId, Environment.StackTrace); } } AnimationFrame = 0; AnimationTime = 0; AnimationTimeLeft = state.GetDelay(0); _parent.Sys.QueueUpdateIsInert((_parent.Owner, _parent)); } [Obsolete("Use SpriteSystem.LayerSetTexture")] public void SetTexture(Texture? texture) { State = default; Texture = texture; BoundsDirty = true; Owner.Comp.BoundsDirty = true; _parent.TreeSys.QueueTreeUpdate((_parent.Owner, _parent)); _parent.Sys.QueueUpdateIsInert((_parent.Owner, _parent)); } /// public Vector2i PixelSize { get { var pixelSize = Vector2i.Zero; if (Texture != null) { pixelSize = Texture.Size; } else if (ActualRsi != null) { pixelSize = ActualRsi.Size; } return pixelSize; } } /// /// Whether or not the layers's local bounding box is dirty and need to be rebuilt. /// internal bool BoundsDirty = true; internal Box2 Bounds; [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() { // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract _parent.Sys?.QueueUpdateIsInert((_parent.Owner, _parent)); if (!State.IsValid) { _actualState = null; return; } // Pull texture from RSI state var rsi = RSI ?? _parent.BaseRSI; if (rsi == null || !rsi.TryGetState(State, out _actualState)) { _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. /// internal void GetLayerDrawMatrix(RsiDirection dir, out Matrix3x2 layerDrawMatrix, bool noRot) { // 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); } 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, Matrix3Helpers.CreateRotation(-Direction.North.ToAngle()), Matrix3Helpers.CreateRotation(-Direction.East.ToAngle()), Matrix3Helpers.CreateRotation(-Direction.West.ToAngle()), Matrix3Helpers.CreateRotation(-Direction.SouthEast.ToAngle()), Matrix3Helpers.CreateRotation(-Direction.SouthWest.ToAngle()), Matrix3Helpers.CreateRotation(-Direction.NorthEast.ToAngle()), Matrix3Helpers.CreateRotation(-Direction.NorthWest.ToAngle()) }; /// /// Converts an angle (between 0 and 2pi) to an RSI direction. This will slightly bias the angle to avoid flickering for /// 4-directional sprites. /// public static RsiDirection GetDirection(RsiDirectionType dirType, Angle angle) { if (dirType == RsiDirectionType.Dir1) return RsiDirection.South; 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 // angle to avoid the sprite flickering. // mod is -0.5 for angles between 0-90 and 180-270, and +0.5 for 90-180 and 270-360 var mod = (Math.Floor(angle.Theta / MathHelper.PiOver2) % 2) - 0.5; var modTheta = angle.Theta + mod * DirectionBias; return ((int)Math.Round(modTheta / MathHelper.PiOver2) % 4) switch { 0 => RsiDirection.South, 1 => RsiDirection.East, 2 => RsiDirection.North, _ => RsiDirection.West, }; } internal void AdvanceFrameAnimation(RSI.State state) { var delayCount = state.DelayCount; while (AnimationTimeLeft < 0) { if (Reversed) { AnimationFrame -= 1; if (AnimationFrame < 0) { if (!Loop || !_parent.Loop) { // stop at first frame AnimationFrame = 0; AnimationTimeLeft = 0; AutoAnimated = false; return; } if (Cycle) { AnimationFrame = 1; Reversed = false; } else { AnimationFrame = delayCount - 1; } AnimationTime = -AnimationTimeLeft; } } else { AnimationFrame += 1; if (AnimationFrame >= delayCount) { if (!Loop || !_parent.Loop) { // stop at last frame AnimationFrame = delayCount - 1; AnimationTimeLeft = 0; AutoAnimated = false; return; } if (Cycle) { AnimationFrame = delayCount - 2; Reversed = true; } else { AnimationFrame = 0; } AnimationTime = -AnimationTimeLeft; } } AnimationTimeLeft += state.GetDelay(AnimationFrame); } } } /// /// Instantiated version of . /// Has actually resolved to a a real key. /// public sealed class CopyToShaderParameters(object layerKey) { public object LayerKey = layerKey; public string? ParameterTexture; public string? ParameterUV; public CopyToShaderParameters(CopyToShaderParameters toClone) : this(toClone.LayerKey) { ParameterTexture = toClone.ParameterTexture; ParameterUV = toClone.ParameterUV; } } void IAnimationProperties.SetAnimatableProperty(string name, object value) { if (!name.StartsWith("layer/")) { AnimationHelper.SetAnimatableProperty(this, name, value); return; } var delimiter = name.IndexOf("/", 6, StringComparison.Ordinal); var indexString = name.Substring(6, delimiter - 6); var index = int.Parse(indexString, CultureInfo.InvariantCulture); var layerProp = name.Substring(delimiter + 1); switch (layerProp) { case "texture": LayerSetTexture(index, (string)value); return; case "state": LayerSetState(index, (string)value); return; case "color": LayerSetColor(index, (Color)value); return; default: throw new ArgumentException($"Unknown layer property '{layerProp}'"); } } public IRsiStateLike? Icon { get { if (Layers.Count == 0) return null; var layer = Layers[0]; var texture = layer.Texture; if (!layer.State.IsValid) return texture; // Pull texture from RSI state instead. var rsi = layer.RSI ?? BaseRSI; if (rsi == null || !rsi.TryGetState(layer.State, out var state)) { 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 sys = IoCManager.Resolve().GetEntitySystem(); return sys.GetPrototypeTextures(prototype, out noRot); } [Obsolete("Use SpriteSystem.GetPrototypeIcon() instead")] public static IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache) { var sys = IoCManager.Resolve().GetEntitySystem(); return sys.GetPrototypeIcon(prototype); } } }