diff --git a/Robust.Client/Animations/AnimationTrackSpriteFlick.cs b/Robust.Client/Animations/AnimationTrackSpriteFlick.cs index 951f85837c..d20254a614 100644 --- a/Robust.Client/Animations/AnimationTrackSpriteFlick.cs +++ b/Robust.Client/Animations/AnimationTrackSpriteFlick.cs @@ -50,8 +50,7 @@ namespace Robust.Client.Animations var rsi = sprite.LayerGetActualRSI(LayerKey); if (rsi.TryGetState(keyFrame.State, out var state)) { - DebugTools.Assert(state.AnimationLength != null, "state.AnimationLength != null"); - var animationTime = Math.Min(state.AnimationLength.Value - 0.01f, playingTime); + var animationTime = Math.Min(state.AnimationLength - 0.01f, playingTime); sprite.LayerSetAutoAnimated(LayerKey, false); // TODO: Doesn't setting the state explicitly reset the animation // so it's slightly more inefficient? diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs index 14014a64cc..4d83a172c4 100644 --- a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs +++ b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs @@ -135,7 +135,7 @@ namespace Robust.Client.GameObjects if (value.TryGetState(layer.State, out var state)) { - (layer.Texture, layer.AnimationTimeLeft) = state.GetFrame(CorrectLayerDir(ref layer, state), 0); + layer.AnimationTimeLeft = state.GetDelay(0); } else { @@ -160,12 +160,8 @@ namespace Robust.Client.GameObjects [Dependency] private readonly IReflectionManager reflectionManager; #pragma warning restore 649 - [ViewVariables(VVAccess.ReadWrite)] RSI.State.Direction LastDir; - [ViewVariables(VVAccess.ReadWrite)] private bool _recalcDirections = false; - - public uint _renderOrder; [ViewVariables(VVAccess.ReadWrite)] - public uint RenderOrder { get => _renderOrder; set => _renderOrder = value; } + public uint RenderOrder { get; set; } private static ShaderInstance _defaultShader; @@ -268,7 +264,7 @@ namespace Robust.Client.GameObjects var layer = new Layer {State = stateId}; if (BaseRSI.TryGetState(stateId, out var state)) { - (layer.Texture, layer.AnimationTimeLeft) = state.GetFrame(CorrectLayerDir(ref layer, state), 0); + layer.AnimationTimeLeft = state.GetDelay(0); } else { @@ -314,7 +310,7 @@ namespace Robust.Client.GameObjects var layer = new Layer {State = stateId, RSI = rsi}; if (rsi.TryGetState(stateId, out var state)) { - (layer.Texture, layer.AnimationTimeLeft) = state.GetFrame(CorrectLayerDir(ref layer, state), 0); + layer.AnimationTimeLeft = state.GetDelay(0); } else { @@ -577,8 +573,7 @@ namespace Robust.Client.GameObjects { theLayer.AnimationFrame = 0; theLayer.AnimationTime = 0; - (theLayer.Texture, theLayer.AnimationTimeLeft) = - state.GetFrame(CorrectLayerDir(ref theLayer, state), 0); + theLayer.AnimationTimeLeft = state.GetDelay(0); } else { @@ -625,8 +620,7 @@ namespace Robust.Client.GameObjects { theLayer.AnimationFrame = 0; theLayer.AnimationTime = 0; - (theLayer.Texture, theLayer.AnimationTimeLeft) = - state.GetFrame(CorrectLayerDir(ref theLayer, state), 0); + theLayer.AnimationTimeLeft = state.GetDelay(0); } else { @@ -708,8 +702,7 @@ namespace Robust.Client.GameObjects { if (rsi.TryGetState(theLayer.State, out var state)) { - (theLayer.Texture, theLayer.AnimationTimeLeft) = - state.GetFrame(CorrectLayerDir(ref theLayer, state), 0); + theLayer.AnimationTimeLeft = state.GetDelay(0); } else { @@ -876,7 +869,6 @@ namespace Robust.Client.GameObjects var theLayer = Layers[layer]; theLayer.DirOffset = offset; - _recalcDirections = true; } public void LayerSetDirOffset(object layerKey, DirectionOffset offset) @@ -918,15 +910,13 @@ namespace Robust.Client.GameObjects { // Going backwards we re-calculate from zero. // Definitely possible to optimize this for going backwards but I'm too lazy to figure that out. - theLayer.AnimationTimeLeft = -animationTime + state.GetFrame(correctDir, 0).delay; + theLayer.AnimationTimeLeft = -animationTime + state.GetDelay(0); theLayer.AnimationFrame = 0; } theLayer.AnimationTime = animationTime; // After setting timing data correctly, run advance to get to the correct frame. - _advanceFrameAnimation(ref theLayer, state, correctDir); - // And set to said frame. - theLayer.Texture = state.GetFrame(correctDir, theLayer.AnimationFrame).icon; + _advanceFrameAnimation(ref theLayer, state); } public void LayerSetAnimationTime(object layerKey, float animationTime) @@ -1001,7 +991,7 @@ namespace Robust.Client.GameObjects return LayerGetActualRSI(layer); } - internal void OpenGLRender(DrawingHandleWorld drawingHandle, bool useWorldTransform=true) + internal void OpenGLRender(DrawingHandleWorld drawingHandle, bool useWorldTransform=true, Direction? overrideDirection=null) { Matrix3 transform; if (useWorldTransform) @@ -1027,7 +1017,9 @@ namespace Robust.Client.GameObjects { transform = Matrix3.Identity; } + drawingHandle.SetTransform(transform); + foreach (var layer in Layers) { if (!layer.Visible) @@ -1036,13 +1028,50 @@ namespace Robust.Client.GameObjects } // TODO: Implement layer-specific rotation and scale. - var texture = layer.Texture ?? resourceCache.GetFallback(); + + var texture = layer.Texture; + + if (layer.State.IsValid) + { + // Pull texture from RSI state instead. + var rsi = layer.RSI ?? BaseRSI; + if (rsi != null) + { + var state = rsi[layer.State]; + + RSI.State.Direction layerSpecificDir; + if (state.Directions == RSI.State.DirectionType.Dir1) + { + layerSpecificDir = RSI.State.Direction.South; + } + else + { + RSI.State.Direction dir; + if (overrideDirection != null) + { + dir = overrideDirection.Value.Convert(state.Directions); + } + else + { + dir = GetDir(state.Directions); + } + layerSpecificDir = OffsetRsiDir(dir, layer.DirOffset); + } + + texture = state.GetFrame(layerSpecificDir, layer.AnimationFrame); + } + } + + texture ??= resourceCache.GetFallback(); + if (layer.Shader != null) { drawingHandle.UseShader(layer.Shader); } + drawingHandle.DrawTexture(texture, -(Vector2)texture.Size/(2f*EyeManager.PIXELSPERMETER), color * layer.Color); + if (layer.Shader != null) { drawingHandle.UseShader(null); @@ -1093,8 +1122,6 @@ namespace Robust.Client.GameObjects { Layers.Add(new Layer(clone)); } - // Do this because the directions in the cache may not be correct for us. - _recalcDirections = true; return; } @@ -1156,7 +1183,7 @@ namespace Robust.Client.GameObjects if (theRsi.TryGetState(stateid, out var state)) { // Always use south because this layer will be cached in the serializer. - (layer.Texture, layer.AnimationTimeLeft) = state.GetFrame(RSI.State.Direction.South, 0); + layer.AnimationTimeLeft = state.GetDelay(0); } else { @@ -1236,31 +1263,14 @@ namespace Robust.Client.GameObjects _layerMapShared = true; serializer.SetCacheData(LayerSerializationCache, Layers.ShallowClone()); serializer.SetCacheData(LayerMapSerializationCache, layerMap); - // Do this because the directions in the cache may not be correct. - _recalcDirections = true; } public void FrameUpdate(float delta) { - // TODO: This entire method is a hotspot of redundant code. - // This is definitely gonna deserve some optimizations later down the line. - - var dirWeAreFacing = GetDir(RSI.State.DirectionType.Dir8); - var diagonalDirChanged = false; - var cardinalDirChanged = false; - - if (LastDir != dirWeAreFacing || _recalcDirections) + foreach (var t in Layers) { - diagonalDirChanged = true; - cardinalDirChanged = LastDir.RoundToCardinal() != dirWeAreFacing.RoundToCardinal() || _recalcDirections; - LastDir = dirWeAreFacing; - _recalcDirections = false; - } - - for (var i = 0; i < Layers.Count; i++) - { - var layer = Layers[i]; - // Since State is a struct, we can't null-check it directly. + var layer = t; + // Since StateId is a struct, we can't null-check it directly. if (!layer.State.IsValid || !layer.Visible) { continue; @@ -1271,61 +1281,34 @@ namespace Robust.Client.GameObjects { continue; } - var state = rsi[layer.State]; - RSI.State.Direction layerSpecificDir; - if (state.Directions == RSI.State.DirectionType.Dir1) - { - layerSpecificDir = RSI.State.Direction.South; - } - else - { - layerSpecificDir = OffsetRsiDir(GetDir(state.Directions), layer.DirOffset); - } - // Is this layer's direction changed? - // This depends on the direction type of the layer. - var dirChanged = state.Directions == RSI.State.DirectionType.Dir8 - ? diagonalDirChanged - : cardinalDirChanged; + var state = rsi[layer.State]; + + if (!state.IsAnimated) + { + continue; + } layer.AnimationTime += delta; - if (!dirChanged) - { - var delayCount = state.DelayCount(layerSpecificDir); - if (delayCount < 2 || !layer.AutoAnimated) - { - // Don't bother animating this. - // There's no animation frames! - continue; - } - - layer.AnimationTimeLeft -= delta; - } - else - { - // Mess with animation data so _advanceFrameAnimation fixes the position to where it should be. - // So cross-direction animations are synced so long as they have the same total length. - layer.AnimationFrame = 0; - layer.AnimationTimeLeft = -layer.AnimationTime; - } - - _advanceFrameAnimation(ref layer, state, layerSpecificDir); - layer.Texture = state.GetFrame(layerSpecificDir, layer.AnimationFrame).icon; + layer.AnimationTimeLeft -= delta; + _advanceFrameAnimation(ref layer, state); } } - private static void _advanceFrameAnimation(ref Layer layer, RSI.State state, RSI.State.Direction layerSpecificDir) + private static void _advanceFrameAnimation(ref Layer layer, RSI.State state) { - var delayCount = state.DelayCount(layerSpecificDir); + var delayCount = state.DelayCount; while (layer.AnimationTimeLeft < 0) { - if (++layer.AnimationFrame >= delayCount) + layer.AnimationFrame += 1; + + if (layer.AnimationFrame >= delayCount) { layer.AnimationFrame = 0; layer.AnimationTime = -layer.AnimationTimeLeft; } - layer.AnimationTimeLeft += state.GetFrame(layerSpecificDir, layer.AnimationFrame).delay; + layer.AnimationTimeLeft += state.GetDelay(layer.AnimationFrame); } } diff --git a/Robust.Client/GameObjects/EntitySystems/EffectSystem.cs b/Robust.Client/GameObjects/EntitySystems/EffectSystem.cs index d5072a1fa2..d1937f94c9 100644 --- a/Robust.Client/GameObjects/EntitySystems/EffectSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/EffectSystem.cs @@ -297,13 +297,13 @@ namespace Robust.Client.GameObjects } // Calculate RSI animations. - var delayCount = RsiState.DelayCount(RSI.State.Direction.South); + var delayCount = RsiState.DelayCount; if (delayCount > 0 && (AnimationLoops || AnimationIndex < delayCount - 1)) { AnimationTime += frameTime; - while (RsiState.GetFrame(RSI.State.Direction.South, AnimationIndex).delay < AnimationTime) + while (RsiState.GetDelay(AnimationIndex) < AnimationTime) { - var (_, delay) = RsiState.GetFrame(RSI.State.Direction.South, AnimationIndex); + var delay = RsiState.GetDelay(AnimationIndex); AnimationIndex += 1; AnimationTime -= delay; if (AnimationIndex == delayCount) @@ -318,7 +318,7 @@ namespace Robust.Client.GameObjects } } - EffectSprite = RsiState.GetFrame(RSI.State.Direction.South, AnimationIndex).icon; + EffectSprite = RsiState.GetFrame(RSI.State.Direction.South, AnimationIndex); } } } diff --git a/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs b/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs index ffab78ce6f..1db4e64875 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs @@ -75,7 +75,7 @@ namespace Robust.Client.Graphics.Clyde _clyde.DrawSwitchSpace(space); } - public void DrawEntity(IEntity entity, Vector2 position, Vector2 scale) + public void DrawEntity(IEntity entity, Vector2 position, Vector2 scale, Direction? overrideDirection) { if (entity.Deleted) { @@ -101,7 +101,7 @@ namespace Robust.Client.Graphics.Clyde } // Draw the entity. - sprite.OpenGLRender(DrawingHandleWorld, false); + sprite.OpenGLRender(DrawingHandleWorld, false, overrideDirection); // Reset to screen space SetSpace(CurrentSpace.ScreenSpace); diff --git a/Robust.Client/Graphics/RSI/RSI.State.cs b/Robust.Client/Graphics/RSI/RSI.State.cs index b6130ab677..12bd453fdc 100644 --- a/Robust.Client/Graphics/RSI/RSI.State.cs +++ b/Robust.Client/Graphics/RSI/RSI.State.cs @@ -1,67 +1,142 @@ using System; using Robust.Shared.Maths; using Robust.Client.Utility; -using System.Collections.Generic; +using Robust.Shared.Utility; namespace Robust.Client.Graphics { public sealed partial class RSI { + // See https://github.com/space-wizards/RobustToolbox/issues/905 for the simplification thing of playback. /// /// Represents a single icon state inside an RSI. /// + /// + /// While the RSI spec allows different animation timing for directions in the same frame, + /// RSIs are folded into a single set of animation timings when loaded. + /// This is to simplify animation playback code in-engine. + /// public sealed class State : IDirectionalTextureProvider { - public Vector2u Size { get; } - public StateId StateId { get; } - public DirectionType Directions { get; } - public Texture Frame0 => Icons[0][0].icon; - public float? AnimationLength { get; } - private readonly (Texture icon, float delay)[][] Icons; + // List of delays for the frame to reach the next frame. + private readonly float[] Delays; - internal State(Vector2u size, StateId stateId, DirectionType direction, (Texture icon, float delay)[][] icons) + // 2D array for the texture to use for each animation frame at each direction. + private readonly Texture[][] Icons; + + internal State(Vector2i size, StateId stateId, DirectionType direction, float[] delays, Texture[][] icons) { + DebugTools.Assert(size.X > 0); + DebugTools.Assert(size.Y > 0); + DebugTools.Assert(stateId.IsValid); + Size = size; StateId = stateId; Directions = direction; + Delays = delays; Icons = icons; - float? animLength = null; - foreach (var dirFrames in icons) + foreach (var delay in delays) { - if (dirFrames.Length <= 1) - { - continue; - } - - var length = 0f; - foreach (var (_, delay) in dirFrames) - { - length += delay; - } - - if (animLength == null) - { - animLength = length; - } - else - { - animLength = Math.Max(animLength.Value, length); - } + AnimationLength += delay; } - - AnimationLength = animLength; } + /// + /// The size of each individual frame in this state. + /// + public Vector2i Size { get; } + + /// + /// The identifier for this state inside an RSI. + /// + public StateId StateId { get; } + + /// + /// How many directions this state has. + /// + public DirectionType Directions { get; } + + /// + /// The first frame of the "south" direction. + /// + /// + /// Always available and better than nothing for previews etc. + /// + public Texture Frame0 => Icons[0][0]; + + /// + /// The total play length of this state's animation, in seconds. + /// + public float AnimationLength { get; } + + /// + /// The amount of frames in the animation of this state. + /// + public int DelayCount => Delays.Length; + + /// + /// If true, this state has an animation to play. + /// + public bool IsAnimated => DelayCount > 1; + + public Texture GetFrame(Direction direction, int frame) + { + return Icons[(int) direction][frame]; + } + + /// + /// Gets the delay between the specified frame and the next frame. + /// + /// The index of the frame. + /// The delay, in seconds. + /// + /// Thrown if the frame index does not exist. + /// + public float GetDelay(int frame) + { + return Delays[frame]; + } + + Texture IDirectionalTextureProvider.Default => Frame0; + + Texture IDirectionalTextureProvider.TextureFor(Shared.Maths.Direction dir) + { + if (Directions == DirectionType.Dir1) + { + return Frame0; + } + + return GetFrame(dir.Convert(Directions), 0); + } + + /// + /// Specifies which types of directions an RSI state has. + /// public enum DirectionType : byte { + /// + /// A single direction, namely South. + /// Dir1, + + /// + /// 4 cardinal directions. + /// Dir4, + + /// + /// 4 cardinal + 4 diagonal directions. + /// Dir8, } + /// + /// Specifies a direction in an RSI state. + /// public enum Direction : byte { + // Value of the enum here matches the index used to store it in the icons array. South = 0, North = 1, East = 2, @@ -71,32 +146,6 @@ namespace Robust.Client.Graphics NorthEast = 6, NorthWest = 7, } - - public (Texture icon, float delay) GetFrame(Direction direction, int frame) - { - return Icons[(int)direction][frame]; - } - - public IReadOnlyCollection<(Texture icon, float delay)> GetDirectionFrames(Direction direction) - { - return Icons[(int)direction]; - } - - public int DelayCount(Direction direction) - { - return Icons[(int)direction].Length; - } - - Texture IDirectionalTextureProvider.Default => GetFrame(Direction.South, 0).icon; - - Texture IDirectionalTextureProvider.TextureFor(Shared.Maths.Direction dir) - { - if (Directions == DirectionType.Dir1) - { - return GetFrame(Direction.South, 0).icon; - } - return GetFrame(dir.Convert(Directions), 0).icon; - } } } } diff --git a/Robust.Client/Graphics/RSI/RSI.cs b/Robust.Client/Graphics/RSI/RSI.cs index b787252b0b..703b171c4b 100644 --- a/Robust.Client/Graphics/RSI/RSI.cs +++ b/Robust.Client/Graphics/RSI/RSI.cs @@ -15,7 +15,7 @@ namespace Robust.Client.Graphics /// The size of this RSI, width x height. /// [ViewVariables] - public Vector2u Size { get; private set; } + public Vector2i Size { get; private set; } private Dictionary States = new Dictionary(); public State this[StateId key] => States[key]; @@ -35,7 +35,7 @@ namespace Robust.Client.Graphics return States.TryGetValue(stateId, out state); } - public RSI(Vector2u size) + public RSI(Vector2i size) { Size = size; } diff --git a/Robust.Client/Interfaces/Graphics/IClyde.cs b/Robust.Client/Interfaces/Graphics/IClyde.cs index cd6e4c8c48..4077d40eb3 100644 --- a/Robust.Client/Interfaces/Graphics/IClyde.cs +++ b/Robust.Client/Interfaces/Graphics/IClyde.cs @@ -123,7 +123,7 @@ namespace Robust.Client.Interfaces.Graphics DrawingHandleWorld DrawingHandleWorld { get; } void SetScissor(UIBox2i? scissorBox); - void DrawEntity(IEntity entity, Vector2 position, Vector2 scale); + void DrawEntity(IEntity entity, Vector2 position, Vector2 scale, Direction? overrideDirection); } /// diff --git a/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs b/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs index 721fd44939..63be9ac908 100644 --- a/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs +++ b/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs @@ -65,34 +65,277 @@ namespace Robust.Client.ResourceManagement // Ok schema validated just fine. var manifestJson = JObject.Parse(manifestContents); - var size = manifestJson["size"].ToObject(); - var rsi = new RSI(size); + var toAtlas = new List<(Image src, Texture[][] output, int[][] indices, int totalFrameCount)>(); - var images = new List<(Image src, Vector2i offset)>(); - var directionFramesList = new List<(Texture, float)[]>(); + var metaData = ParseMetaData(manifestJson); + var frameSize = metaData.Size; + var rsi = new RSI(frameSize); // Do every state. + foreach (var stateObject in metaData.States) + { + // Load image from disk. + var texPath = path / (stateObject.StateId + ".png"); + var image = Image.Load(cache.ContentFileRead(texPath)); + var sheetSize = new Vector2i(image.Width, image.Height); + + if (sheetSize.X % frameSize.X != 0 || sheetSize.Y % frameSize.Y != 0) + { + throw new RSILoadException("State image size is not a multiple of the icon size."); + } + + // Load all frames into a list so we can operate on it more sanely. + var frameCount = stateObject.Delays.Sum(delayList => delayList.Length); + + var (foldedDelays, foldedIndices) = FoldDelays(stateObject.Delays); + + var textures = new Texture[foldedIndices.Length][]; + + for (var i = 0; i < textures.Length; i++) + { + textures[i] = new Texture[foldedIndices[0].Length]; + } + + var state = new RSI.State(frameSize, stateObject.StateId, stateObject.DirType, foldedDelays, textures); + rsi.AddState(state); + + toAtlas.Add((image, textures, foldedIndices, frameCount)); + } + + // Poorly hacked in texture atlas support here. + { + var totalFrameCount = toAtlas.Sum(p => p.totalFrameCount); + + // Generate atlas. + var dimensionX = (int) MathF.Ceiling(MathF.Sqrt(totalFrameCount)); + var dimensionY = (int) MathF.Ceiling((float) totalFrameCount / dimensionX); + + using var sheet = new Image(dimensionX * frameSize.X, dimensionY * frameSize.Y); + + var sheetIndex = 0; + foreach (var (src, _, _, frameCount) in toAtlas) + { + // Blit all the frames over. + for (var i = 0; i < frameCount; i++) + { + var srcWidth = (src.Width / frameSize.X); + var srcColumn = i % srcWidth; + var srcRow = i / srcWidth; + var srcPos = (srcColumn * frameSize.X, srcRow * frameSize.Y); + + var sheetColumn = (sheetIndex + i) % dimensionX; + var sheetRow = (sheetIndex + i) / dimensionX; + var sheetPos = (sheetColumn * frameSize.X, sheetRow * frameSize.Y); + + var srcBox = UIBox2i.FromDimensions(srcPos, frameSize); + + src.Blit(srcBox, sheet, sheetPos); + } + + sheetIndex += frameCount; + } + + // Load atlas. + var texture = Texture.LoadFromImage(sheet, path.ToString()); + + var sheetOffset = 0; + foreach (var (src, output, indices, frameCount) in toAtlas) + { + for (var i = 0; i < indices.Length; i++) + { + var dirIndices = indices[i]; + var dirOutput = output[i]; + + for (var j = 0; j < dirIndices.Length; j++) + { + var index = sheetOffset + dirIndices[j]; + + var sheetColumn = index % dimensionX; + var sheetRow = index / dimensionX; + var sheetPos = (sheetColumn * frameSize.X, sheetRow * frameSize.Y); + + dirOutput[j] = new AtlasTexture(texture, UIBox2.FromDimensions(sheetPos, frameSize)); + } + } + + sheetOffset += frameCount; + } + } + + foreach (var (image, _, _, _) in toAtlas) + { + image.Dispose(); + } + + RSI = rsi; + } + + /// + /// Folds a per-directional sets of animation delays + /// into an equivalent set of animation delays and indices that works for every direction. + /// + internal static (float[] delays, int[][] indices) FoldDelays(float[][] delays) + { + if (delays.Length == 1) + { + // Short circuit handle single directional sprites. + var delayList = delays[0]; + var output = new float[delayList.Length]; + var indices = new int[delayList.Length]; + + for (var i = 0; i < delayList.Length; i++) + { + output[i] = delayList[i]; + indices[i] = i; + } + + return (output, new[] {indices}); + } + + // Multiply by 1000 so we have millisecond precision in our fixed point. + const float fixedPointResolution = 1000; + + var dirCount = delays.Length; + + // Convert to int[][] to use our fixed point and avoid floating point pains. + // Also we mutate these arrays to make the calculations easier. + // We also calculate the lengths for later. + var iDelays = new int[dirCount][]; + Span dirLengths = stackalloc int[dirCount]; + var maxLength = 0; + + for (var d = 0; d < dirCount; d++) + { + var length = 0; + var fDelayList = delays[d]; + var delayList = new int[fDelayList.Length]; + iDelays[d] = delayList; + + for (var i = 0; i < delayList.Length; i++) + { + var delay = (int) (fDelayList[i] * fixedPointResolution); + delayList[i] = delay; + length += delay; + } + + maxLength = Math.Max(length, maxLength); + dirLengths[d] = length; + } + + // Extend final delay so that all sets have the same total length. + // Strictly speaking this shouldn't be necessary, since the RSI spec mandates equal lengths. + // But better safe than sorry, especially if there's some funky floating point conversions. + for (var d = 0; d < dirCount; d++) + { + var length = dirLengths[d]; + var diff = maxLength - length; + + iDelays[d][^1] += diff; + } + + // Calculate base texture indices for the directions. + Span dirIndexOffsets = stackalloc int[dirCount]; + dirIndexOffsets.Fill(0); + + for (var i = 0; i < dirCount - 1; i++) + { + dirIndexOffsets[i + 1] = dirIndexOffsets[i] + delays[i].Length; + } + + // Offsets in each directions array, since we don't go through each with the same index. + Span dirDelayOffsets = stackalloc int[dirCount]; + dirDelayOffsets.Fill(0); + + // Output delays list. + var newDelays = new List(); + + // Output indices list. + var newIndices = new List[dirCount]; + for (var d = 0; d < dirCount; d++) + { + newIndices[d] = new List(); + } + + // Actually get churning through these delays. + while (true) + { + var minDelay = int.MaxValue; + + // Calculate the minimum delay for each direction we're currently at. + for (var d = 0; d < dirCount; d++) + { + var o = dirDelayOffsets[d]; + var delay = iDelays[d][o]; + + minDelay = Math.Min(delay, minDelay); + + // This will obviously be a frame, so write the index for the texture into output indices. + newIndices[d].Add(dirIndexOffsets[d] + o); + } + + // Add said minimum delay to the output delays. + newDelays.Add(minDelay); + + for (var d = 0; d < dirCount; d++) + { + ref var o = ref dirDelayOffsets[d]; + ref var delay = ref iDelays[d][o]; + + // Subtract working delays. + delay -= minDelay; + + if (delay == 0) + { + // Increment offset in array for the direction(s) that fully completed a frame this iteration. + o += 1; + } + + // We've reached the end of one direction. + // Since every direction has the same length, this *must* mean we're done. + if (o == iDelays[d].Length) + { + goto done; + } + } + } + + done: + + // Turn output data into a format suitable for returning and we're done. + var floatDelays = new float[newDelays.Count]; + + for (var i = 0; i < newDelays.Count; i++) + { + floatDelays[i] = newDelays[i] / fixedPointResolution; + } + + var arrayIndices = new int[dirCount][]; + for (var d = 0; d < dirCount; d++) + { + arrayIndices[d] = newIndices[d].ToArray(); + } + + return (floatDelays, arrayIndices); + } + + internal static RsiMetadata ParseMetaData(JObject manifestJson) + { + var size = manifestJson["size"].ToObject(); + var states = new List(); + foreach (var stateObject in manifestJson["states"].Cast()) { var stateName = stateObject["name"].ToObject(); var dirValue = stateObject["directions"].ToObject(); - RSI.State.DirectionType directions; - switch (dirValue) + var directions = dirValue switch { - case 1: - directions = RSI.State.DirectionType.Dir1; - break; - case 4: - directions = RSI.State.DirectionType.Dir4; - break; - case 8: - directions = RSI.State.DirectionType.Dir8; - break; - default: - throw new RSILoadException($"Invalid direction: {dirValue}"); - } + 1 => RSI.State.DirectionType.Dir1, + 4 => RSI.State.DirectionType.Dir4, + 8 => RSI.State.DirectionType.Dir8, + _ => throw new RSILoadException($"Invalid direction: {dirValue}") + }; // We can ignore selectors and flags for now, // because they're not used yet! @@ -128,99 +371,13 @@ namespace Robust.Client.ResourceManagement } } - var texPath = path / (stateName + ".png"); - var image = Image.Load(cache.ContentFileRead(texPath)); - var sheetSize = new Vector2i(image.Width, image.Height); - - if (sheetSize.X % size.X != 0 || sheetSize.Y % size.Y != 0) - { - throw new RSILoadException("State image size is not a multiple of the icon size."); - } - - // Amount of icons per row of the sprite sheet. - var sheetWidth = (sheetSize.X / size.X); - - var iconFrames = new (Texture, float)[dirValue][]; - var counter = 0; - for (var j = 0; j < iconFrames.Length; j++) - { - var delayList = delays[j]; - var directionFrames = new (Texture, float)[delayList.Length]; - directionFramesList.Add(directionFrames); - for (var i = 0; i < delayList.Length; i++) - { - var posX = (int) ((counter % sheetWidth) * size.X); - var posY = (int) ((counter / sheetWidth) * size.Y); - - images.Add((image, (posX, posY))); - - directionFrames[i] = (null, delayList[i]); - counter++; - } - - iconFrames[j] = directionFrames; - } - - var state = new RSI.State(size, stateName, directions, iconFrames); - rsi.AddState(state); + states.Add(new StateMetadata(new RSI.StateId(stateName), directions, delays)); } - // Poorly hacked in texture atlas support here. - { - // Generate atlas. - var dimensionX = (int) Math.Ceiling(Math.Sqrt(images.Count)); - var dimensionY = (int) Math.Ceiling((float) images.Count / dimensionX); - - int i; - Texture texture; - using (var sheet = new Image((int) (dimensionX * size.X), (int) (dimensionY * size.Y))) - { - i = 0; - foreach (var list in directionFramesList) - { - for (var j = 0; j < list.Length; j++, i++) - { - var column = i % dimensionX; - var row = i / dimensionX; - - var (image, offset) = images[i]; - - var srcBox = UIBox2i.FromDimensions(offset, (Vector2i) size); - var dstOffset = ((int)(column * size.X), (int)(row * size.Y)); - image.Blit(srcBox, sheet, dstOffset); - } - } - - // Load atlas. - texture = Texture.LoadFromImage(sheet, path.ToString()); - } - - // Assign AtlasTexture instances. - i = 0; - foreach (var list in directionFramesList) - { - for (var j = 0; j < list.Length; j++, i++) - { - ref var tuple = ref list[j]; - var column = i % dimensionX; - var row = i / dimensionX; - - var pX = (int) (column * size.X); - var pY = (int) (row * size.Y); - - tuple.Item1 = new AtlasTexture(texture, UIBox2.FromDimensions(pX, pY, size.X, size.Y)); - } - } - } - - foreach (var (image, _) in images) - { - image.Dispose(); - } - - RSI = rsi; + return new RsiMetadata(size, states); } +#if DEBUG private static readonly JsonSchema RSISchema = GetSchema(); private static JsonSchema GetSchema() @@ -243,6 +400,45 @@ namespace Robust.Client.ResourceManagement return null; } } +#endif + + internal sealed class RsiMetadata + { + public RsiMetadata(Vector2i size, List states) + { + Size = size; + States = states; + } + + public Vector2i Size { get; } + public List States { get; } + } + + internal sealed class StateMetadata + { + public StateMetadata(RSI.StateId stateId, RSI.State.DirectionType dirType, float[][] delays) + { + StateId = stateId; + DirType = dirType; + Delays = delays; + + DebugTools.Assert(delays.Length == DirCount); + DebugTools.Assert(StateId.IsValid); + } + + public RSI.StateId StateId { get; } + public RSI.State.DirectionType DirType { get; } + + public int DirCount => DirType switch + { + RSI.State.DirectionType.Dir1 => 1, + RSI.State.DirectionType.Dir4 => 4, + RSI.State.DirectionType.Dir8 => 8, + _ => 1 + }; + + public float[][] Delays { get; } + } } [Serializable] diff --git a/Robust.Client/UserInterface/Controls/SpriteView.cs b/Robust.Client/UserInterface/Controls/SpriteView.cs index 7f3eb87a2b..a47b68c683 100644 --- a/Robust.Client/UserInterface/Controls/SpriteView.cs +++ b/Robust.Client/UserInterface/Controls/SpriteView.cs @@ -20,6 +20,15 @@ namespace Robust.Client.UserInterface.Controls public ISpriteComponent Sprite { get; set; } + /// + /// Overrides the direction used to render the sprite. + /// + /// + /// If null, the world space orientation of the entity will be used. + /// Otherwise the specified direction will be used. + /// + public Direction? OverrideDirection { get; set; } + public SpriteView() { RectClipContent = true; @@ -39,7 +48,7 @@ namespace Robust.Client.UserInterface.Controls return; } - renderHandle.DrawEntity(Sprite.Owner, GlobalPixelPosition + PixelSize / 2, Scale); + renderHandle.DrawEntity(Sprite.Owner, GlobalPixelPosition + PixelSize / 2, Scale, OverrideDirection); } } } diff --git a/Robust.Shared.Maths/Vector2i.cs b/Robust.Shared.Maths/Vector2i.cs index a2a52ceb32..efe99134c6 100644 --- a/Robust.Shared.Maths/Vector2i.cs +++ b/Robust.Shared.Maths/Vector2i.cs @@ -1,8 +1,10 @@ using System; using System.Runtime.InteropServices; +using Newtonsoft.Json; namespace Robust.Shared.Maths { + [JsonObject(MemberSerialization.Fields)] [Serializable] [StructLayout(LayoutKind.Sequential)] public readonly struct Vector2i : IEquatable diff --git a/Robust.UnitTesting/Client/ResourceManagement/RsiResourceTest.cs b/Robust.UnitTesting/Client/ResourceManagement/RsiResourceTest.cs new file mode 100644 index 0000000000..7b55c50568 --- /dev/null +++ b/Robust.UnitTesting/Client/ResourceManagement/RsiResourceTest.cs @@ -0,0 +1,108 @@ +using NUnit.Framework; +using Robust.Client.ResourceManagement; + +namespace Robust.UnitTesting.Client.ResourceManagement +{ + [TestOf(typeof(RSIResource))] + [Parallelizable(ParallelScope.All)] + [TestFixture] + public class RsiResourceTest + { + /// + /// Simple test in which the delays are already identical so nothing should change much. + /// + [Test] + public void TestFoldDelaysIdentical() + { + var delays = new[] + { + new float[] {5, 5, 5, 5}, + new float[] {5, 5, 5, 5}, + new float[] {5, 5, 5, 5}, + new float[] {5, 5, 5, 5}, + }; + + var (newDelays, indices) = RSIResource.FoldDelays(delays); + + Assert.That(newDelays, Is.EqualTo(new[] {5f, 5f, 5f, 5f})); + + for (var i = 0; i < 4; i++) + { + var o = i * 4; + Assert.That(indices[i], Is.EqualTo(new[] {o, o + 1, o + 2, o + 3})); + } + } + + /// + /// Test in which the delays are different but equal amount of frames. + /// + [Test] + public void TestFoldDelaysDifferent() + { + var delays = new[] + { + new float[] {5, 5, 5, 5}, + new float[] {5, 7, 3, 5}, + new float[] {5, 5, 5, 5}, + new float[] {5, 5, 5, 5}, + }; + + var (newDelays, indices) = RSIResource.FoldDelays(delays); + + Assert.That(newDelays, Is.EqualTo(new[] {5f, 5f, 2f, 3f, 5f})); + + Assert.That(indices[0], Is.EqualTo(new[] {0, 1, 2, 2, 3})); + Assert.That(indices[1], Is.EqualTo(new[] {4, 5, 5, 6, 7})); + Assert.That(indices[2], Is.EqualTo(new[] {8, 9, 10, 10, 11})); + Assert.That(indices[3], Is.EqualTo(new[] {12, 13, 14, 14, 15})); + } + + /// + /// Test in which the delays are different but equal amount of frames. + /// + [Test] + public void TestFoldFramesDifferent() + { + var delays = new[] + { + new float[] {5, 5, 5, 5}, + new float[] {5, 5, 5, 5}, + new float[] {5, 5, 10}, + new float[] {5, 5, 5, 5}, + }; + + var (newDelays, indices) = RSIResource.FoldDelays(delays); + + Assert.That(newDelays, Is.EqualTo(new[] {5f, 5f, 5f, 5f})); + + Assert.That(indices[0], Is.EqualTo(new[] {0, 1, 2, 3})); + Assert.That(indices[1], Is.EqualTo(new[] {4, 5, 6, 7})); + Assert.That(indices[2], Is.EqualTo(new[] {8, 9, 10, 10})); + Assert.That(indices[3], Is.EqualTo(new[] {11, 12, 13, 14})); + } + + /// + /// The very complicated test where a ton of stuff goes down. + /// + [Test] + public void TestFoldWild() + { + var delays = new[] + { + new float[] {1, 9}, + new float[] {3, 3, 3, 1}, + new float[] {7, 1, 2}, + new float[] {5, 2, 2, 1}, + }; + + var (newDelays, indices) = RSIResource.FoldDelays(delays); + + Assert.That(newDelays, Is.EqualTo(new[] {1f, 2f, 2f, 1f, 1f, 1f, 1f, 1f})); + + Assert.That(indices[0], Is.EqualTo(new[] {0, 1, 1, 1, 1, 1, 1, 1})); + Assert.That(indices[1], Is.EqualTo(new[] {2, 2, 3, 3, 4, 4, 4, 5})); + Assert.That(indices[2], Is.EqualTo(new[] {6, 6, 6, 6, 6, 7, 8, 8})); + Assert.That(indices[3], Is.EqualTo(new[] {9, 9, 9, 10, 10, 11, 11, 12})); + } + } +}