From 9023b5a47358d973c584987f6c0aaf7b3089c571 Mon Sep 17 00:00:00 2001 From: Brett Date: Fri, 7 Jul 2023 15:28:23 -0300 Subject: [PATCH] Nuget Package CI/CD Overhaul (#7) --- .github/workflows/ci.yml | 40 ++++++ .github/workflows/dotnet.yml | 30 ----- .github/workflows/release.yml | 22 ++++ DMISharp.Tests/DMICreationTests.cs | 4 + DMISharp.Tests/DMIModifyTests.cs | 7 +- DMISharp.Tests/DMISharp.Tests.csproj | 12 +- DMISharp.Tests/DMIWriteTests.cs | 143 ++++++++++----------- DMISharp/DMIFile.cs | 82 +++++++----- DMISharp/DMISharp.csproj | 31 +++-- DMISharp/DMIState.cs | 131 ++++++++++++++++--- DMISharp/Interfaces/IExportable.cs | 12 ++ DMISharp/Metadata/DMIMetadata.cs | 47 ++++--- DMISharp/Metadata/StateMetadata.cs | 16 ++- DMISharp/Quantization/NoFrillsQuantizer.cs | 4 +- Directory.Build.props | 17 +++ 15 files changed, 394 insertions(+), 204 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/dotnet.yml create mode 100644 .github/workflows/release.yml create mode 100644 Directory.Build.props diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..513076d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: ci +on: + push: + branches: [master, release-*] + pull_request: +env: + DOTNET_NOLOGO: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true +jobs: + ci: + strategy: + fail-fast: false + matrix: + job: + - name: ubuntu + os: ubuntu-22.04 + - name: windows + os: windows-2022 + name: ${{ matrix.job.name }} + runs-on: ${{ matrix.job.os }} + steps: + - uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 6.0.411 + 7.0.305 + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal DMISharp.Tests/DMISharp.Tests.csproj --logger GitHubActions + - if: matrix.job.name == 'ubuntu' + uses: actions/upload-artifact@v3 + with: + name: NuGet packages + path: ./**/*.nupkg \ No newline at end of file diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml deleted file mode 100644 index 34dced9..0000000 --- a/.github/workflows/dotnet.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Run .NET Tests -on: - push: - branches: - - master - pull_request: - branches: - - master -jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ ubuntu-latest, windows-latest ] - steps: - - uses: actions/checkout@v2 - - name: Setup .NET 6.0 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 6.0.x - - name: Setup .NET 7.0 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 7.0.x - - name: Restore dependencies - run: dotnet restore - - name: Build - run: dotnet build --no-restore - - name: Test - run: dotnet test --no-build --verbosity normal DMISharp.Tests/DMISharp.Tests.csproj diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e29526d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,22 @@ +name: release +on: + push: + tags: ["*.*.*"] +env: + DOTNET_NOLOGO: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true +jobs: + release: + runs-on: ubuntu-22.04 + steps: + - uses: actions/setup-dotnet@v3 + with: + dotnet-version: 7.0.305 + - uses: actions/checkout@v3 + - run: dotnet build --configuration Release --nologo + - name: push + env: + SOURCE: ${{ secrets.NUGET_PUSH_SOURCE }} + API_KEY: ${{ secrets.NUGET_PUSH_API_KEY }} + if: env.SOURCE != '' || env.API_KEY != '' + run: dotnet nuget push ./**/*.nupkg --source ${{ env.SOURCE }} --api-key ${{ env.API_KEY }} \ No newline at end of file diff --git a/DMISharp.Tests/DMICreationTests.cs b/DMISharp.Tests/DMICreationTests.cs index 9a67c7e..a7c9cde 100644 --- a/DMISharp.Tests/DMICreationTests.cs +++ b/DMISharp.Tests/DMICreationTests.cs @@ -31,7 +31,9 @@ public void CanCreateDMIFromImages() foreach (var source in sourceData) { var img = Image.Load($@"Data/Input/SourceImages/{source}.png"); +#pragma warning disable CA2000 var newState = new DMIState(source, DirectionDepth.One, 1, 32, 32); +#pragma warning restore CA2000 newState.SetFrame(img, 0); newDMI.AddState(newState); } @@ -46,7 +48,9 @@ public void CanChangeDMIDepths() // Create state var img = Image.Load($@"Data/Input/SourceImages/steve32.png"); +#pragma warning disable CA2000 var newState = new DMIState("steve32", DirectionDepth.One, 1, 32, 32); +#pragma warning restore CA2000 newState.SetFrame(img, 0); newDMI.AddState(newState); diff --git a/DMISharp.Tests/DMIModifyTests.cs b/DMISharp.Tests/DMIModifyTests.cs index b13f039..0ce88ef 100644 --- a/DMISharp.Tests/DMIModifyTests.cs +++ b/DMISharp.Tests/DMIModifyTests.cs @@ -3,8 +3,7 @@ namespace DMISharp.Tests; -// ReSharper disable once ClassNeverInstantiated.Global -public class DMIModifyTests +public static class DMIModifyTests { [Fact] public static void ShouldRemoveStateMetadata() @@ -37,7 +36,7 @@ public static void ShouldDetectValidUnmodifiedDMIState() } [Fact] - public static void ShouldDetectInvalidDMIState__MissingFrame() + public static void ShouldDetectInvalidDMIStateMissingFrame() { // Arrange using var file = new DMIFile(@"Data/Input/turf_analysis.dmi"); @@ -52,7 +51,7 @@ public static void ShouldDetectInvalidDMIState__MissingFrame() } [Fact] - public static void ShouldDetectInvalidDMIState__MissingDirection() + public static void ShouldDetectInvalidDMIStateMissingDirection() { // Arrange using var file = new DMIFile(@"Data/Input/turf_analysis.dmi"); diff --git a/DMISharp.Tests/DMISharp.Tests.csproj b/DMISharp.Tests/DMISharp.Tests.csproj index 88ce678..15c5d7a 100644 --- a/DMISharp.Tests/DMISharp.Tests.csproj +++ b/DMISharp.Tests/DMISharp.Tests.csproj @@ -6,13 +6,17 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -20,7 +24,7 @@ - + diff --git a/DMISharp.Tests/DMIWriteTests.cs b/DMISharp.Tests/DMIWriteTests.cs index 8bf5c10..c2f0f50 100644 --- a/DMISharp.Tests/DMIWriteTests.cs +++ b/DMISharp.Tests/DMIWriteTests.cs @@ -84,136 +84,133 @@ public void ResavingFileMatchesOriginalMetadata(string inputPath, string outputP if (File.Exists(outputPath)) File.Delete(outputPath); - using (var fs = File.OpenWrite(outputPath!)) + using (var fs = File.OpenWrite(outputPath)) using (var originalFile = new DMIFile(inputPath)) originalFile.Save(fs); // Check metadata is equal - using (var oldFile = File.OpenRead(inputPath)) - using (var newFile = File.OpenRead(outputPath)) - Assert.Equal(DMIMetadata.GetDMIMetadata(oldFile).ToString(), DMIMetadata.GetDMIMetadata(newFile).ToString()); + using var oldFile = File.OpenRead(inputPath); + using var newFile = File.OpenRead(outputPath); + Assert.Equal(DMIMetadata.GetDMIMetadata(oldFile).ToString(), DMIMetadata.GetDMIMetadata(newFile).ToString()); } [Theory] [InlineData(@"Data/Input/broadMobs.dmi", @"Data/Output/broadMobs.dmi")] - public void ResavingFileMatchesOriginalImage_RectSprites(string inputPath, string outputPath) + public void ResavingFileMatchesOriginalImageRectSprites(string inputPath, string outputPath) { if (File.Exists(outputPath)) File.Delete(outputPath); - using (var fs = File.OpenWrite(outputPath!)) + using (var fs = File.OpenWrite(outputPath)) using (var originalFile = new DMIFile(inputPath)) originalFile.Save(fs); // Check image is equal var pixelDiffs = 0; - using (var oldFile = Image.Load(inputPath)) - using (var newFile = Image.Load(outputPath)) - { - // Check overall dimensions - Assert.Equal(oldFile.Width, newFile.Width); - Assert.Equal(oldFile.Height, newFile.Height); + using var oldFile = Image.Load(inputPath); + using var newFile = Image.Load(outputPath); + + // Check overall dimensions + Assert.Equal(oldFile.Width, newFile.Width); + Assert.Equal(oldFile.Height, newFile.Height); - // Check pixel content - var height = oldFile.Height; - var width = oldFile.Width; - oldFile.ProcessPixelRows(newFile, (oldAccessor, newAccessor) => + // Check pixel content + var height = oldFile.Height; + var width = oldFile.Width; + oldFile.ProcessPixelRows(newFile, (oldAccessor, newAccessor) => + { + for (var ypx = 0; ypx < height; ypx++) { - for (var ypx = 0; ypx < height; ypx++) + var oldSpan = oldAccessor.GetRowSpan(ypx); + var newSpan = newAccessor.GetRowSpan(ypx); + for (var xpx = 0; xpx < width; xpx++) { - var oldSpan = oldAccessor.GetRowSpan(ypx); - var newSpan = newAccessor.GetRowSpan(ypx); - for (var xpx = 0; xpx < width; xpx++) - { - if (!(oldSpan[xpx].A == 0 && newSpan[xpx].A == 0) && oldSpan[xpx] != newSpan[xpx]) - pixelDiffs++; - } + if (!(oldSpan[xpx].A == 0 && newSpan[xpx].A == 0) && oldSpan[xpx] != newSpan[xpx]) + pixelDiffs++; } - }); - } - + } + }); + Assert.Equal(0, pixelDiffs); } [Theory] [InlineData(@"Data/Input/animal.dmi", @"Data/Output/animal.dmi")] - public void ResavingFileMatchesOriginalImage_SquareSprites(string inputPath, string outputPath) + public void ResavingFileMatchesOriginalImageSquareSprites(string inputPath, string outputPath) { if (File.Exists(outputPath)) File.Delete(outputPath); - using (var fs = File.OpenWrite(outputPath!)) + using (var fs = File.OpenWrite(outputPath)) using (var originalFile = new DMIFile(inputPath)) originalFile.Save(fs); // Check image is equal var pixelDiffs = 0; - using (var oldFile = Image.Load(inputPath)) - using (var newFile = Image.Load(outputPath)) - { - // Check overall dimensions - Assert.Equal(oldFile.Width, newFile.Width); - Assert.Equal(oldFile.Height, newFile.Height); + using var oldFile = Image.Load(inputPath); + using var newFile = Image.Load(outputPath); + + // Check overall dimensions + Assert.Equal(oldFile.Width, newFile.Width); + Assert.Equal(oldFile.Height, newFile.Height); - // Check pixel content - var height = oldFile.Height; - var width = oldFile.Width; - oldFile.ProcessPixelRows(newFile, (oldAccessor, newAccessor) => + // Check pixel content + var height = oldFile.Height; + var width = oldFile.Width; + oldFile.ProcessPixelRows(newFile, (oldAccessor, newAccessor) => + { + for (var ypx = 0; ypx < height; ypx++) { - for (var ypx = 0; ypx < height; ypx++) + var oldSpan = oldAccessor.GetRowSpan(ypx); + var newSpan = newAccessor.GetRowSpan(ypx); + for (var xpx = 0; xpx < width; xpx++) { - var oldSpan = oldAccessor.GetRowSpan(ypx); - var newSpan = newAccessor.GetRowSpan(ypx); - for (var xpx = 0; xpx < width; xpx++) - { - if (!(oldSpan[xpx].A == 0 && newSpan[xpx].A == 0) && oldSpan[xpx] != newSpan[xpx]) - pixelDiffs++; - } + if (!(oldSpan[xpx].A == 0 && newSpan[xpx].A == 0) && oldSpan[xpx] != newSpan[xpx]) + pixelDiffs++; } - }); - } - + } + }); + Assert.Equal(0, pixelDiffs); } [Theory] [InlineData(@"Data/Input/light_64.dmi", @"Data/Output/light_64.dmi")] - public void ResavingFileMatchesOriginalImage_SingleSprite(string inputPath, string outputPath) + public void ResavingFileMatchesOriginalImageSingleSprite(string inputPath, string outputPath) { if (File.Exists(outputPath)) File.Delete(outputPath); - using (var fs = File.OpenWrite(outputPath!)) + using (var fs = File.OpenWrite(outputPath)) using (var originalFile = new DMIFile(inputPath)) originalFile.Save(fs); // Check image is equal var pixelDiffs = 0; - using (var oldFile = Image.Load(inputPath)) - using (var newFile = Image.Load(outputPath)) - { - // Check overall dimensions - Assert.Equal(oldFile.Width, newFile.Width); - Assert.Equal(oldFile.Height, newFile.Height); + using var oldFile = Image.Load(inputPath); + using var newFile = Image.Load(outputPath); + + // Check overall dimensions + Assert.Equal(oldFile.Width, newFile.Width); + Assert.Equal(oldFile.Height, newFile.Height); - // Check pixel content - var height = oldFile.Height; - var width = oldFile.Width; - oldFile.ProcessPixelRows(newFile, (oldAccessor, newAccessor) => + // Check pixel content + var height = oldFile.Height; + var width = oldFile.Width; + oldFile.ProcessPixelRows(newFile, (oldAccessor, newAccessor) => + { + for (var ypx = 0; ypx < height; ypx++) { - for (var ypx = 0; ypx < height; ypx++) + var oldSpan = oldAccessor.GetRowSpan(ypx); + var newSpan = newAccessor.GetRowSpan(ypx); + for (var xpx = 0; xpx < width; xpx++) { - var oldSpan = oldAccessor.GetRowSpan(ypx); - var newSpan = newAccessor.GetRowSpan(ypx); - for (var xpx = 0; xpx < width; xpx++) - { - if (!(oldSpan[xpx].A == 0 && newSpan[xpx].A == 0) && oldSpan[xpx] != newSpan[xpx]) - pixelDiffs++; - } + if (!(oldSpan[xpx].A == 0 && newSpan[xpx].A == 0) && oldSpan[xpx] != newSpan[xpx]) + pixelDiffs++; } - }); - } - + } + }); + Assert.Equal(0, pixelDiffs); } } \ No newline at end of file diff --git a/DMISharp/DMIFile.cs b/DMISharp/DMIFile.cs index fe0bb8d..37bb111 100644 --- a/DMISharp/DMIFile.cs +++ b/DMISharp/DMIFile.cs @@ -23,6 +23,11 @@ public sealed class DMIFile : IDisposable, IExportable private bool _disposedValue; private List _states; + /// + /// Constructs a new for a provided pair of state dimensions. + /// + /// The width of frames in each state in pixels + /// The height of frames in each state in pixels public DMIFile(int frameWidth, int frameHeight) { Metadata = new DMIMetadata(4.0, frameWidth, frameHeight); @@ -30,7 +35,7 @@ public DMIFile(int frameWidth, int frameHeight) } /// - /// Initializes a new instance of a DMI File. + /// Constructs a new from a provided stream. /// /// The Stream containing the DMI file data. [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] @@ -60,17 +65,24 @@ public DMIFile(string file) { } + /// + /// The metadata for this DMI File. + /// public DMIMetadata Metadata { get; } + + /// + /// All of the entries for this DMI file. + /// public IReadOnlyCollection States => _states.AsReadOnly(); /// /// Saves a DMI File to a stream. The resulting file is .dmi-ready /// - /// The stream to save the DMI File to. + /// The stream to save the DMI File to. /// True if the file was saved, false otherwise - public void Save(Stream stream) + public void Save(Stream dataStream) { - if (stream == null) throw new ArgumentNullException(nameof(stream), "Target stream cannot be null!"); + if (dataStream == null) throw new ArgumentNullException(nameof(dataStream), "Target stream cannot be null!"); // prepare frames var frames = new List>(); @@ -80,7 +92,12 @@ public void Save(Stream stream) { for (var dir = 0; dir < state.Dirs; dir++) { - frames.Add(state.GetFrame((StateDirection)dir, frame)); + var foundFrame = state.GetFrame((StateDirection)dir, frame); + if (foundFrame != null) + frames.Add(foundFrame); + else + throw new InvalidOperationException( + $"Failed to get frame for state: {dir} frame {frame} is null"); } } } @@ -197,7 +214,7 @@ public void Save(Stream stream) // If there is no possibility of a color palette we can return at this point if (colors.Count > 256) { - img.SaveAsPng(stream, pngEncoder); + img.SaveAsPng(dataStream, pngEncoder); return; } @@ -226,7 +243,7 @@ public void Save(Stream stream) var smallest = paletteMs.Length < normalMs.Length ? paletteMs : normalMs; smallest.Seek(0, SeekOrigin.Begin); - smallest.CopyTo(stream); + smallest.CopyTo(dataStream); } /// @@ -263,15 +280,19 @@ private string GetTextChunk() { var builder = new StringBuilder(); builder.Append( - $"# BEGIN DMI\nversion = {Metadata.Version:0.0}\n\twidth = {Metadata.FrameWidth}\n\theight = {Metadata.FrameHeight}\n"); + FormattableString.Invariant( + $"# BEGIN DMI\nversion = {Metadata.Version:0.0}\n\twidth = {Metadata.FrameWidth}\n\theight = {Metadata.FrameHeight}\n")); foreach (var state in States) { - builder.Append($"state = \"{state.Name}\"\n\tdirs = {state.Dirs}\n\tframes = {state.Frames}\n"); - if (state.Data.Delay != null) builder.Append($"\tdelay = {string.Join(",", state.Data.Delay)}\n"); - if (state.Data.Loop > 0) builder.Append($"\tloop = {state.Data.Loop}\n"); + builder.Append( + FormattableString.Invariant( + $"state = \"{state.Name}\"\n\tdirs = {state.Dirs}\n\tframes = {state.Frames}\n")); + if (state.Data.Delay != null) + builder.Append(FormattableString.Invariant($"\tdelay = {string.Join(",", state.Data.Delay)}\n")); + if (state.Data.Loop > 0) builder.Append(FormattableString.Invariant($"\tloop = {state.Data.Loop}\n")); if (state.Data.Hotspots != null) - builder.Append($"\thotspots = {string.Join(",", state.Data.Hotspots)}\n"); + builder.Append(FormattableString.Invariant($"\thotspots = {string.Join(",", state.Data.Hotspots)}\n")); if (state.Data.Movement) builder.Append("\tmovement = 1\n"); if (state.Data.Rewind) builder.Append("\trewind = 1\n"); } @@ -357,10 +378,9 @@ public void SortStates(IComparer comparer) /// /// The DMI file to import states from /// The number of states imported - public int ImportStates(DMIFile other) + public int ImportStates(DMIFile? other) { - if (other is { States: not null, Metadata: not null } - && Metadata != null + if (other != null && other.Metadata.FrameHeight == Metadata.FrameHeight && other.Metadata.FrameWidth == Metadata.FrameWidth) { @@ -375,10 +395,8 @@ public int ImportStates(DMIFile other) return added; } - else - { - return 0; - } + + return 0; } /// @@ -390,7 +408,7 @@ public void ClearStates() } /// - /// Removes a state from a DMI File + /// Removes a state from a DMI File. /// /// The DMIState to remove /// True if the state was removed, otherwise false @@ -400,20 +418,20 @@ public bool RemoveState(DMIState toRemove) { return Metadata.States.Remove(toRemove.Data); } - else - { - return false; - } + + return false; } /// - /// Adds a state to a DMI File + /// Adds a state to a DMI File. /// /// The DMIState to add /// True if the state was added, otherwise false public bool AddState(DMIState toAdd) { - if (!StateValidForFile(toAdd) || toAdd?.Data == null) + if (toAdd == null) + throw new ArgumentNullException(nameof(toAdd)); + if (!StateValidForFile(toAdd)) return false; _states.Add(toAdd); @@ -422,13 +440,12 @@ public bool AddState(DMIState toAdd) } /// - /// Ensures that a state is valid for a DMI File's existing dimensions + /// Ensures that a state is valid for a DMI File's existing dimensions. /// /// The DMIState to check against the file /// True if the state is compatible with the file, false otherwise private bool StateValidForFile(DMIState toCheck) => - toCheck != null - && toCheck.Height == Metadata.FrameHeight + toCheck.Height == Metadata.FrameHeight && toCheck.Width == Metadata.FrameWidth; private static PngBitDepth GetBitDepth(ReadOnlySpan colors) @@ -442,6 +459,9 @@ private static PngBitDepth GetBitDepth(ReadOnlySpan colors) }; } + /// + /// Dispose of the DMI file. + /// public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method @@ -458,13 +478,13 @@ private void Dispose(bool disposing) if (disposing) { - foreach (var state in States) + foreach (var state in _states) { state.Dispose(); } } - _states = null; + _states.Clear(); _disposedValue = true; } } \ No newline at end of file diff --git a/DMISharp/DMISharp.csproj b/DMISharp/DMISharp.csproj index 322aedb..7f34748 100644 --- a/DMISharp/DMISharp.csproj +++ b/DMISharp/DMISharp.csproj @@ -2,39 +2,48 @@ net6.0;net7.0 + true + true + DMISharp Bobbahbrown MelonMesa + Copyright © $([System.DateTime]::Now.Year) MelonMesa Library for handling BYOND DMI files. - Copyright (c) 2023 MelonMesa https://github.com/bobbahbrown/DMISharp https://github.com/bobbahbrown/DMISharp Git - 2.0.0.0 - 2.0.0 - 2.0.0.0 dmisharp.png - true - GPL-3.0-only - DMI BYOND SpaceStation13 - - - Updated to .NET 7 - - Updated ImageSharp to 3 - + LICENSE + DMI BYOND SpaceStation13 Space Station 13 tgstation tg + https://github.com/bobbahbrown/DMISharp/blob/master/CHANGELOG.md README.md + + 2.0 + + all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/DMISharp/DMIState.cs b/DMISharp/DMIState.cs index 6e624eb..2fbb815 100644 --- a/DMISharp/DMIState.cs +++ b/DMISharp/DMIState.cs @@ -12,41 +12,82 @@ namespace DMISharp; /// -/// Representative of the BYOND state directions +/// Representative of the BYOND state directions. /// public enum StateDirection { + /// + /// Southern (typically downward) direction + /// South, + + /// + /// Northern (typically upwards) direction + /// North, + + /// + /// Eastern (typically right) direction + /// East, + + /// + /// Western (typically left) direction + /// West, + + /// + /// Southeastern (typically down-right) direction + /// SouthEast, + + /// + /// Southwestern (typically down-left) direction + /// SouthWest, + + /// + /// Northeastern (typically up-right) direction + /// NorthEast, + + /// + /// Northwestern (typically up-left) direction + /// NorthWest } /// /// Representative of the depth of directions available to a state. -/// One = S -/// Four = S, N, E, W -/// Eight = S, N, E, W, SE, SW, NE, NW /// +#pragma warning disable CA1008, CA1027 public enum DirectionDepth +#pragma warning restore CA1008, CA1027 { + /// + /// One direction depth: S + /// One = 1, + + /// + /// Four direction depth: S, N, E, W + /// Four = 4, + + /// + /// Eight direction depth: S, N, E, W, SE, SW, NE, NW + /// Eight = 8 } /// -/// Represents an icon state in a BYOND DMI file. -/// Allows for interacting with each frame of the state. +/// Represents an icon state in a BYOND DMI file. Allows for interacting with each frame of the state. /// public sealed class DMIState : IDisposable { - private Image[][] _images; // Stores each frame image following the [direction][frame] pattern. - + private Image?[][] _images; // Stores each frame image following the [direction][frame] pattern. + private bool _disposed; + /// /// Initializes a blank DMI state. /// @@ -100,19 +141,53 @@ public DMIState(StateMetadata state, Image source, int currWIndex, int w DirectionDepth = (DirectionDepth)_images.Length; } + /// + /// The name of the state, accessible from DM. + /// public string Name { get => Data.State; set => Data.State = value; } + /// + /// The directions for this state. + /// public int Dirs => Data.Dirs; + + /// + /// The frames for this state. + /// public int Frames => Data.Frames; + + /// + /// The height of this state in pixels. + /// public int Height { get; } + + /// + /// The width of this state in pixels. + /// public int Width { get; } + + /// + /// The total number of frames in this state. + /// public int TotalFrames => _images.Sum(x => x.Count(y => y != null)); + + /// + /// The total possible number of frames in this state. + /// public int FrameCapacity => _images.Sum(x => x.Length); + + /// + /// The direction depth of this state. + /// public DirectionDepth DirectionDepth { get; private set; } + + /// + /// The metadata store for this state. + /// public StateMetadata Data { get; } // Stores key, value pairs from DMI file metadata. /// @@ -120,10 +195,18 @@ public string Name /// public void Dispose() { + if (_disposed) + return; + foreach (var image in _images.SelectMany(x => x).Where(x => x != null)) { - image.Dispose(); + image!.Dispose(); } + + // Empty the array of images to remove all references + _images = Array.Empty[]>(); + + _disposed = true; } /// @@ -248,10 +331,16 @@ public Image GetAnimated(StateDirection direction) for (var frame = 0; frame < Frames; frame++) { var cursor = _images[(int)direction][frame]; - var metadata = cursor.Frames.RootFrame.Metadata.GetFormatMetadata(GifFormat.Instance); - metadata.FrameDelay = (int)(Data.Delay[frame] * 10.0); // GIF frames are 10ms compared to 100ms tick - metadata.DisposalMethod = GifDisposalMethod.RestoreToBackground; // Ensures transparent pixels - toReturn.Frames.InsertFrame(frame, cursor.Frames.RootFrame); + if (cursor != null) + { + var metadata = cursor.Frames.RootFrame.Metadata.GetFormatMetadata(GifFormat.Instance); + metadata.FrameDelay = (int)(Data.Delay![frame] * 10.0); // GIF frames are 10ms compared to 100ms tick + metadata.DisposalMethod = GifDisposalMethod.RestoreToBackground; // Ensures transparent pixels + toReturn.Frames.InsertFrame(frame, cursor.Frames.RootFrame); + } + else + throw new InvalidOperationException( + $"Image data missing frame for direction {direction}, cannot animate"); } // Final process @@ -273,7 +362,7 @@ public Image GetAnimated(StateDirection direction) /// The direction of the state to retrieve the gif for. /// The GifEncoder to use, if null a default is generated. Only override if required. [SuppressMessage("ReSharper", "InconsistentNaming")] - public void SaveAnimatedGIF(Stream stream, StateDirection direction, GifEncoder encoder = null) + public void SaveAnimatedGIF(Stream stream, StateDirection direction, GifEncoder? encoder = null) { if (!IsAnimated()) { @@ -296,14 +385,14 @@ public void SaveAnimatedGIF(Stream stream, StateDirection direction, GifEncoder /// The direction of the frame /// The frame index /// An ImageSharp Image representing the frame - public Image GetFrame(StateDirection direction, int frame) => _images[(int)direction][frame]; + public Image? GetFrame(StateDirection direction, int frame) => _images[(int)direction][frame]; /// /// Helper for frame retrieval, defaults to the North (1) direction /// /// The frame index /// An ImageSharp Image representing the frame - public Image GetFrame(int frame) => GetFrame(StateDirection.South, frame); + public Image? GetFrame(int frame) => GetFrame(StateDirection.South, frame); /// /// Sets the content of a frame in a state. @@ -368,7 +457,7 @@ public void SetDirectionDepth(DirectionDepth depth) return; var minDepth = Math.Min((int)depth, (int)DirectionDepth); - var temp = new Image[(int)depth][]; + var temp = new Image?[(int)depth][]; for (var i = 0; i < minDepth; i++) { temp[i] = _images[i]; @@ -382,7 +471,7 @@ public void SetDirectionDepth(DirectionDepth depth) for (var j = 0; j < Frames; j++) { var cursor = _images[i][j]; - cursor.Dispose(); + cursor?.Dispose(); } } } @@ -410,12 +499,12 @@ public void SetFrameCount(int frames) if (Frames == frames) return; - var temp = new Image[Dirs][]; + var temp = new Image?[Dirs][]; var minFrames = Math.Min(Frames, frames); for (var dir = 0; dir < Dirs; dir++) { - temp[dir] = new Image[frames]; + temp[dir] = new Image?[frames]; for (var i = 0; i < minFrames; i++) { temp[dir][i] = _images[dir][i]; @@ -427,7 +516,7 @@ public void SetFrameCount(int frames) for (var i = minFrames; i < Frames; i++) { - _images[dir][i].Dispose(); + _images[dir][i]?.Dispose(); } } diff --git a/DMISharp/Interfaces/IExportable.cs b/DMISharp/Interfaces/IExportable.cs index d34aa29..7cb8555 100644 --- a/DMISharp/Interfaces/IExportable.cs +++ b/DMISharp/Interfaces/IExportable.cs @@ -2,8 +2,20 @@ namespace DMISharp.Interfaces; +/// +/// Provides a mechanism for saving an object to a stream +/// public interface IExportable { + /// + /// Saves the to the provided stream. + /// + /// The stream to write the data to void Save(Stream dataStream); + + /// + /// Determines if the can be saved. + /// + /// True if savable, false otherwise bool CanSave(); } \ No newline at end of file diff --git a/DMISharp/Metadata/DMIMetadata.cs b/DMISharp/Metadata/DMIMetadata.cs index 05324d0..e9920cd 100644 --- a/DMISharp/Metadata/DMIMetadata.cs +++ b/DMISharp/Metadata/DMIMetadata.cs @@ -7,12 +7,18 @@ namespace DMISharp.Metadata; /// -/// Represents the header data of a DMI file +/// Represents the header data of a DMI file. /// public class DMIMetadata { private static readonly Regex DMIStart = new(@"#\s{0,1}BEGIN DMI", RegexOptions.Compiled); - + + /// + /// Constructs a new for a provided BYOND version and pair of state dimensions. + /// + /// The version of BYOND this metadata is for + /// The width of each state frame in pixels + /// The height of each state frame in pixels public DMIMetadata(double byondVersion, int frameWidth, int frameHeight) { Version = byondVersion; @@ -22,7 +28,7 @@ public DMIMetadata(double byondVersion, int frameWidth, int frameHeight) } /// - /// Instantiates a DMIMetadata object from a file stream of that DMI file + /// Instantiates a DMIMetadata object from a file stream of that DMI file. /// /// The data stream to read from public DMIMetadata(Stream stream) @@ -37,7 +43,7 @@ public DMIMetadata(Stream stream) } /// - /// The version of the DMI metadata, dictated by BYOND + /// The version of the DMI metadata, dictated by BYOND. /// public double Version { get; private set; } @@ -51,17 +57,20 @@ public DMIMetadata(Stream stream) /// public int FrameHeight { get; internal set; } + /// + /// The set of metadata for each of the states in the DMI file. + /// public List States { get; } /// - /// Gets a collection of DMI metadata directories and breaks it into individual lines of DMI metadata + /// Gets a collection of DMI metadata directories and breaks it into individual lines of DMI metadata. /// /// The file to get the metadata of /// A ReadOnlySpan of the DMI's metadata if found public static ReadOnlySpan GetDMIMetadata(Stream stream) { var directories = PngMetadataReader.ReadMetadata(stream); - string metaDesc = null; + string? metaDesc = null; foreach (var t in directories) { foreach (var tag in t.Tags) @@ -79,25 +88,25 @@ public static ReadOnlySpan GetDMIMetadata(Stream stream) if (metaDesc == null) { - throw new Exception("Failed to find BYOND DMI metadata in PNG text data!"); + throw new InvalidOperationException("Failed to find BYOND DMI metadata in PNG text data!"); } - return metaDesc.AsSpan()[metaDesc.IndexOf('#')..]; + return metaDesc.AsSpan()[metaDesc.IndexOf('#', StringComparison.InvariantCultureIgnoreCase)..]; } /// - /// Attempts to apply all data from the provided metadata to this DMIMetadata object + /// Attempts to apply all data from the provided metadata to this DMIMetadata object. /// /// The metadata string to parse private void ParseMetadata(ReadOnlySpan data) { - StateMetadata currentState = null; + StateMetadata? currentState = null; var tokenizer = new DMITokenizer(data); // Parse header var hasBody = ParseHeader(ref tokenizer); if (Version == 0d) - throw new Exception("Failed to parse required header data of DMI file, this file may be corrupt."); + throw new InvalidOperationException("Failed to parse required header data of DMI file, this file may be corrupt."); // Check for any additional data after the header if (!hasBody) @@ -111,17 +120,13 @@ private void ParseMetadata(ReadOnlySpan data) if (currentState != null) States.Add(currentState); - currentState = new StateMetadata() - { - State = tokenizer.CurrentValue.ToString() - }; - + currentState = new StateMetadata(tokenizer.CurrentValue.ToString()); continue; } // At this point if no state is present, then we have invalid data if (currentState == null) - throw new Exception("Started to read state data without a state, this file may be corrupt"); + throw new InvalidOperationException("Started to read state data without a state, this file may be corrupt"); // Handle value if (tokenizer.KeyEquals("dirs")) @@ -148,7 +153,7 @@ private void ParseMetadata(ReadOnlySpan data) } /// - /// Attempts to parse the header data (version, frame size) to this DMIMetadata object + /// Attempts to parse the header data (version, frame size) to this DMIMetadata object. /// /// The tokenizer containing the data /// True if more data (states) follows, false if no data is found after the header @@ -159,21 +164,21 @@ private bool ParseHeader(ref DMITokenizer tokenizer) if (tokenizer.KeyEquals("version")) { if (Version != 0d) - throw new Exception("Found more than one version line, this file may be corrupt!"); + throw new InvalidOperationException("Found more than one version line, this file may be corrupt!"); Version = tokenizer.ValueAsDouble(); } else if (tokenizer.KeyEquals("width")) { if (FrameWidth != -1) - throw new Exception("Found more than one frame width line, this file may be corrupt!"); + throw new InvalidOperationException("Found more than one frame width line, this file may be corrupt!"); FrameWidth = tokenizer.ValueAsInt(); } else if (tokenizer.KeyEquals("height")) { if (FrameHeight != -1) - throw new Exception("Found more than one frame height line, this file may be corrupt!"); + throw new InvalidOperationException("Found more than one frame height line, this file may be corrupt!"); FrameHeight = tokenizer.ValueAsInt(); } diff --git a/DMISharp/Metadata/StateMetadata.cs b/DMISharp/Metadata/StateMetadata.cs index e58d96c..0730ede 100644 --- a/DMISharp/Metadata/StateMetadata.cs +++ b/DMISharp/Metadata/StateMetadata.cs @@ -7,11 +7,13 @@ namespace DMISharp.Metadata; /// public class StateMetadata { - internal StateMetadata() - { - } - - public StateMetadata(string name, DirectionDepth directionDepth, int frames) + /// + /// Constructs a new for the provided name, depth, and frame count. + /// + /// The name of the state + /// The depth of directions for this state + /// The number of frames per direction for this state + public StateMetadata(string name, DirectionDepth directionDepth = DirectionDepth.One, int frames = 1) { State = name; Dirs = (int)directionDepth; @@ -36,7 +38,7 @@ public StateMetadata(string name, DirectionDepth directionDepth, int frames) /// /// The delays used for animating each frame in each direction, each unit is 100ms. /// - public double[] Delay { get; internal set; } + public double[]? Delay { get; set; } /// /// Controls if the state has rewind, which will run the animation to end frame and then back @@ -56,5 +58,5 @@ public StateMetadata(string name, DirectionDepth directionDepth, int frames) /// /// Controls the hotspots, used for defining custom cursors /// - public List Hotspots { get; set; } + public List? Hotspots { get; set; } } \ No newline at end of file diff --git a/DMISharp/Quantization/NoFrillsQuantizer.cs b/DMISharp/Quantization/NoFrillsQuantizer.cs index 1d82058..d6acee7 100644 --- a/DMISharp/Quantization/NoFrillsQuantizer.cs +++ b/DMISharp/Quantization/NoFrillsQuantizer.cs @@ -8,7 +8,7 @@ namespace DMISharp.Quantization; -public class NoFrillsQuantizer : IQuantizer +internal class NoFrillsQuantizer : IQuantizer { private readonly ReadOnlyMemory _colorPalette; @@ -35,7 +35,7 @@ public IQuantizer CreatePixelSpecificQuantizer(Configuration con } } -internal struct NoFrillsQuantizer : IQuantizer +internal readonly struct NoFrillsQuantizer : IQuantizer where TPixel : unmanaged, IPixel { private readonly Dictionary _paletteLookup; diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..dda0462 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,17 @@ + + + + All + 6.0 + embedded + true + true + true + enable + + + + true + + + \ No newline at end of file