diff --git a/Directory.Build.props b/Directory.Build.props
index 6b76cede..399e843a 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -40,7 +40,7 @@
5.8.0
0.1.0
5.8.0
- 5.8.0
+ 5.9.0
0.1.0
5.8.0
5.8.1
diff --git a/NuGet.config b/NuGet.config
index b9dffb44..63c99eab 100644
--- a/NuGet.config
+++ b/NuGet.config
@@ -26,6 +26,7 @@
+
diff --git a/README.md b/README.md
index f1e17f4c..7495a41b 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ War3Net is a collection of libraries for Warcraft III modding.
| [War3Net.CodeAnalysis.Jass] | War3Net.CodeAnalysis.Jass is a library for parsing and rendering JASS source files. | [![VCodeJass]][PCodeJass] |
| [War3Net.CodeAnalysis.Transpilers]| Transpiles JASS source code to C# or lua. | [![VCodeTrans]][PCodeTrans] |
| [War3Net.Common] | Contains some methods used by several other War3Net projects. | [![VCommon]][PCommon] |
-| [War3Net.Drawing.Blp] | War3Net.Drawing.Blp is a library for reading files with the ".blp" extension. | [![VBlp]][PBlp] |
+| [War3Net.Drawing.Blp] | War3Net.Drawing.Blp is a library for reading and writing files with the ".blp" extension. | [![VBlp]][PBlp] |
| [War3Net.IO.Casc] | Class library for opening CASC archives. | *Coming soon* |
| [War3Net.IO.Compression] | Decompression and compression algorithms for compression methods commonly used in MPQ archives. | [![VCompress]][PCompress] |
| [War3Net.IO.Mpq] | Class library for opening and creating MPQ files. | [![VMpq]][PMpq] |
diff --git a/src/War3Net.Drawing.Blp/Blp1EncodingOptions.cs b/src/War3Net.Drawing.Blp/Blp1EncodingOptions.cs
new file mode 100644
index 00000000..02c46a8f
--- /dev/null
+++ b/src/War3Net.Drawing.Blp/Blp1EncodingOptions.cs
@@ -0,0 +1,47 @@
+// ------------------------------------------------------------------------------
+//
+// Licensed under the MIT license.
+// See the LICENSE file in the project root for more information.
+//
+// ------------------------------------------------------------------------------
+
+namespace War3Net.Drawing.Blp
+{
+ ///
+ /// Options for encoding a BLP1 file with JPEG compression.
+ ///
+ public class Blp1EncodingOptions
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Blp1EncodingOptions()
+ {
+ GenerateMipmaps = true;
+ MipmapLevels = 0;
+ JpegQuality = 85;
+ }
+
+ ///
+ /// Gets or sets a value indicating whether to generate mipmaps automatically.
+ ///
+ public bool GenerateMipmaps { get; set; }
+
+ ///
+ /// Gets or sets the number of mipmap levels to generate (1-16).
+ /// Only used if is .
+ /// Set to 0 to generate all mipmaps.
+ ///
+ public int MipmapLevels { get; set; }
+
+ ///
+ /// Gets or sets the JPEG quality (1-100).
+ ///
+ public int JpegQuality { get; set; }
+
+ ///
+ /// Gets or sets the extra flags field (team colors/alpha info).
+ ///
+ public uint ExtraFlags { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/War3Net.Drawing.Blp/BlpEncoder.cs b/src/War3Net.Drawing.Blp/BlpEncoder.cs
new file mode 100644
index 00000000..350f2a66
--- /dev/null
+++ b/src/War3Net.Drawing.Blp/BlpEncoder.cs
@@ -0,0 +1,298 @@
+// ------------------------------------------------------------------------------
+//
+// Licensed under the MIT license.
+// See the LICENSE file in the project root for more information.
+//
+// ------------------------------------------------------------------------------
+
+using System;
+using System.IO;
+using System.Text;
+
+using JpegLibrary;
+
+namespace War3Net.Drawing.Blp
+{
+ ///
+ /// Encoder for creating BLP1 files with JPEG compression and BGRA channels.
+ /// BLP1 uses a non-standard 4-channel JPEG format (BGRA) instead of the typical 3-channel RGB.
+ /// The encoder stores normal (non-inverted) pixel values in the JPEG stream.
+ /// Decoding behavior is platform-specific: Windows WPF inverts after decoding, JpegLibrary returns raw values.
+ ///
+ public sealed class BlpEncoder
+ {
+ private readonly Blp1EncodingOptions _options;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Encoding options.
+ public BlpEncoder(Blp1EncodingOptions? options = null)
+ {
+ _options = options ?? new Blp1EncodingOptions();
+ }
+
+ ///
+ /// Encodes BGRA pixel data to a BLP1 JPEG file.
+ /// The encoder stores normal (non-inverted) pixel values using non-standard 4-channel JPEG (BGRA).
+ ///
+ /// The stream to write the BLP1 data to.
+ /// Width of the image.
+ /// Height of the image.
+ /// BGRA pixel data (4 bytes per pixel).
+ public void Encode(Stream stream, int width, int height, byte[] bgra)
+ {
+ if (stream is null)
+ {
+ throw new ArgumentNullException(nameof(stream));
+ }
+
+ if (bgra is null)
+ {
+ throw new ArgumentNullException(nameof(bgra));
+ }
+
+ if (bgra.Length != width * height * 4)
+ {
+ throw new ArgumentException("BGRA data length does not match width * height * 4", nameof(bgra));
+ }
+
+ if (width <= 0 || height <= 0)
+ {
+ throw new ArgumentException("Width and height must be positive");
+ }
+
+ // Generate mipmaps
+ var mipmaps = GenerateMipmaps(width, height, bgra);
+
+ // Encode each mipmap to JPEG
+ var jpegMipmaps = new byte[mipmaps.Length][];
+ for (var i = 0; i < mipmaps.Length; i++)
+ {
+ var mipWidth = Math.Max(1, width >> i);
+ var mipHeight = Math.Max(1, height >> i);
+ jpegMipmaps[i] = EncodeJpeg(mipmaps[i], mipWidth, mipHeight);
+ }
+
+ using (var writer = new BinaryWriter(stream, Encoding.ASCII, true))
+ {
+ // Write header
+ WriteHeader(writer, width, height, jpegMipmaps.Length);
+
+ // Calculate and write mipmap offsets and sizes
+ var (offsets, sizes) = CalculateMipmapOffsetsAndSizes(jpegMipmaps);
+ WriteMipmapInfo(writer, offsets, sizes);
+
+ // Write JPEG header size (0 = not using shared header)
+ writer.Write(0u);
+
+ // Write JPEG data for each mipmap
+ foreach (var jpegData in jpegMipmaps)
+ {
+ writer.Write(jpegData);
+ }
+ }
+ }
+
+ private byte[][] GenerateMipmaps(int width, int height, byte[] bgra)
+ {
+ if (!_options.GenerateMipmaps)
+ {
+ return new[] { bgra };
+ }
+
+ // Calculate number of mipmap levels
+ var maxLevels = CalculateMaxMipmapLevels(width, height);
+ var levels = _options.MipmapLevels > 0
+ ? Math.Min(_options.MipmapLevels, maxLevels)
+ : maxLevels;
+
+ var mipmaps = new byte[levels][];
+ mipmaps[0] = bgra;
+
+ // Generate each mipmap level
+ for (var i = 1; i < levels; i++)
+ {
+ var prevWidth = Math.Max(1, width >> (i - 1));
+ var prevHeight = Math.Max(1, height >> (i - 1));
+ var currWidth = Math.Max(1, width >> i);
+ var currHeight = Math.Max(1, height >> i);
+
+ mipmaps[i] = GenerateMipmap(mipmaps[i - 1], prevWidth, prevHeight, currWidth, currHeight);
+ }
+
+ return mipmaps;
+ }
+
+ private byte[] GenerateMipmap(byte[] source, int srcWidth, int srcHeight, int dstWidth, int dstHeight)
+ {
+ var dest = new byte[dstWidth * dstHeight * 4];
+
+ // Simple box filter downsampling
+ for (var y = 0; y < dstHeight; y++)
+ {
+ for (var x = 0; x < dstWidth; x++)
+ {
+ var srcX = x * 2;
+ var srcY = y * 2;
+
+ // Sample 2x2 pixels and average them
+ int b = 0, g = 0, r = 0, a = 0;
+ var count = 0;
+
+ for (var dy = 0; dy < 2 && srcY + dy < srcHeight; dy++)
+ {
+ for (var dx = 0; dx < 2 && srcX + dx < srcWidth; dx++)
+ {
+ var srcIdx = ((srcY + dy) * srcWidth + (srcX + dx)) * 4;
+ b += source[srcIdx + 0];
+ g += source[srcIdx + 1];
+ r += source[srcIdx + 2];
+ a += source[srcIdx + 3];
+ count++;
+ }
+ }
+
+ var dstIdx = (y * dstWidth + x) * 4;
+ dest[dstIdx + 0] = (byte)(b / count);
+ dest[dstIdx + 1] = (byte)(g / count);
+ dest[dstIdx + 2] = (byte)(r / count);
+ dest[dstIdx + 3] = (byte)(a / count);
+ }
+ }
+
+ return dest;
+ }
+
+ private int CalculateMaxMipmapLevels(int width, int height)
+ {
+ var levels = 1;
+ var minDim = Math.Min(width, height);
+
+ while (minDim > 1 && levels < 16)
+ {
+ minDim >>= 1;
+ levels++;
+ }
+
+ return levels;
+ }
+
+ private byte[] EncodeJpeg(byte[] bgra, int width, int height)
+ {
+ // BLP1 uses non-standard 4-channel JPEG (BGRA format instead of RGB/CMYK)
+ // Pixel values are stored normally (NOT inverted) in the JPEG stream
+ // The decoder handles platform-specific interpretation:
+ // - Windows WPF: interprets as RGB/RGBA, inverts after decoding
+ // - JpegLibrary: reads raw 4-channel data directly
+ var encoder = new JpegEncoder();
+
+ // Set quantization tables for all 4 components
+ // Each component needs its own quantization table with unique identifier
+ var qTable0 = JpegStandardQuantizationTable.ScaleByQuality(
+ JpegStandardQuantizationTable.GetLuminanceTable(JpegElementPrecision.Precision8Bit, 0),
+ _options.JpegQuality);
+ var qTable1 = JpegStandardQuantizationTable.ScaleByQuality(
+ JpegStandardQuantizationTable.GetChrominanceTable(JpegElementPrecision.Precision8Bit, 1),
+ _options.JpegQuality);
+ var qTable2 = JpegStandardQuantizationTable.ScaleByQuality(
+ JpegStandardQuantizationTable.GetChrominanceTable(JpegElementPrecision.Precision8Bit, 2),
+ _options.JpegQuality);
+ var qTable3 = JpegStandardQuantizationTable.ScaleByQuality(
+ JpegStandardQuantizationTable.GetChrominanceTable(JpegElementPrecision.Precision8Bit, 3),
+ _options.JpegQuality);
+
+ encoder.SetQuantizationTable(qTable0);
+ encoder.SetQuantizationTable(qTable1);
+ encoder.SetQuantizationTable(qTable2);
+ encoder.SetQuantizationTable(qTable3);
+
+ // Set Huffman tables (we'll reuse the same tables for all components)
+ encoder.SetHuffmanTable(true, 0, JpegStandardHuffmanEncodingTable.GetLuminanceDCTable());
+ encoder.SetHuffmanTable(false, 0, JpegStandardHuffmanEncodingTable.GetLuminanceACTable());
+ encoder.SetHuffmanTable(true, 1, JpegStandardHuffmanEncodingTable.GetChrominanceDCTable());
+ encoder.SetHuffmanTable(false, 1, JpegStandardHuffmanEncodingTable.GetChrominanceACTable());
+ encoder.SetHuffmanTable(true, 2, JpegStandardHuffmanEncodingTable.GetChrominanceDCTable());
+ encoder.SetHuffmanTable(false, 2, JpegStandardHuffmanEncodingTable.GetChrominanceACTable());
+ encoder.SetHuffmanTable(true, 3, JpegStandardHuffmanEncodingTable.GetChrominanceDCTable());
+ encoder.SetHuffmanTable(false, 3, JpegStandardHuffmanEncodingTable.GetChrominanceACTable());
+
+ // Add 4 components (BGRA - non-standard for JPEG!)
+ // Parameters: componentId, quantTableId, dcTableId, acTableId, hSampling, vSampling
+ encoder.AddComponent(1, 0, 0, 0, 1, 1); // Blue component
+ encoder.AddComponent(2, 1, 1, 1, 1, 1); // Green component
+ encoder.AddComponent(3, 2, 2, 2, 1, 1); // Red component
+ encoder.AddComponent(4, 3, 3, 3, 1, 1); // Alpha component
+
+ // Set input reader with original (non-inverted) pixel data
+ encoder.SetInputReader(new JpegBufferInputReader(width, height, 4, bgra));
+
+ // Encode to buffer
+ var writer = new System.Buffers.ArrayBufferWriter();
+ encoder.SetOutput(writer);
+ encoder.Encode();
+
+ return writer.WrittenSpan.ToArray();
+ }
+
+ private void WriteHeader(BinaryWriter writer, int width, int height, int mipmapCount)
+ {
+ // Magic number - BLP1
+ writer.Write((int)FileFormatVersion.BLP1);
+
+ // Content type (0=JPEG)
+ writer.Write((int)FileContent.JPG);
+
+ // Alpha depth (0 for JPEG format, even though we encode BGRA)
+ writer.Write(0u);
+
+ // Width and height
+ writer.Write(width);
+ writer.Write(height);
+
+ // Extra flags
+ writer.Write(_options.ExtraFlags);
+
+ // Has mipmaps
+ writer.Write(mipmapCount > 1 ? 1u : 0u);
+ }
+
+ private static (uint[] offsets, uint[] sizes) CalculateMipmapOffsetsAndSizes(byte[][] jpegMipmaps)
+ {
+ var offsets = new uint[16];
+ var sizes = new uint[16];
+
+ // Header structure:
+ // 0x00-0x1B: BLP1 header (28 bytes)
+ // 0x1C-0x5B: Mipmap offsets (64 bytes)
+ // 0x5C-0x9B: Mipmap sizes (64 bytes)
+ // 0x9C-0x9F: JPEG header size field (4 bytes)
+ // 0xA0+: JPEG data starts here
+ uint currentOffset = 0x9C + 4; // After all header data and JPEG header size field
+
+ for (var i = 0; i < jpegMipmaps.Length; i++)
+ {
+ offsets[i] = currentOffset;
+ sizes[i] = (uint)jpegMipmaps[i].Length;
+ currentOffset += sizes[i];
+ }
+
+ return (offsets, sizes);
+ }
+
+ private void WriteMipmapInfo(BinaryWriter writer, uint[] offsets, uint[] sizes)
+ {
+ // Write 16 mipmap offsets
+ for (var i = 0; i < 16; i++)
+ {
+ writer.Write(offsets[i]);
+ }
+
+ // Write 16 mipmap sizes
+ for (var i = 0; i < 16; i++)
+ {
+ writer.Write(sizes[i]);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/War3Net.Drawing.Blp/JpegBufferInputReader.cs b/src/War3Net.Drawing.Blp/JpegBufferInputReader.cs
new file mode 100644
index 00000000..914bb952
--- /dev/null
+++ b/src/War3Net.Drawing.Blp/JpegBufferInputReader.cs
@@ -0,0 +1,64 @@
+// ------------------------------------------------------------------------------
+//
+// Licensed under the MIT license.
+// See the LICENSE file in the project root for more information.
+//
+// ------------------------------------------------------------------------------
+
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+using JpegLibrary;
+
+namespace War3Net.Drawing.Blp
+{
+ ///
+ /// Input reader for JPEG encoding from a byte buffer.
+ ///
+ internal sealed class JpegBufferInputReader : JpegBlockInputReader
+ {
+ private readonly int _width;
+ private readonly int _height;
+ private readonly int _componentCount;
+ private readonly Memory _buffer;
+
+ public JpegBufferInputReader(int width, int height, int componentCount, Memory buffer)
+ {
+ _width = width;
+ _height = height;
+ _componentCount = componentCount;
+ _buffer = buffer;
+ }
+
+ public override int Width => _width;
+
+ public override int Height => _height;
+
+ public override void ReadBlock(ref short blockRef, int componentIndex, int x, int y)
+ {
+ var width = _width;
+ var componentCount = _componentCount;
+
+ ref var sourceRef = ref MemoryMarshal.GetReference(MemoryMarshal.AsBytes(_buffer.Span));
+
+ var blockWidth = Math.Min(width - x, 8);
+ var blockHeight = Math.Min(_height - y, 8);
+
+ if (blockWidth != 8 || blockHeight != 8)
+ {
+ Unsafe.As(ref blockRef) = default;
+ }
+
+ for (var offsetY = 0; offsetY < blockHeight; offsetY++)
+ {
+ var sourceRowOffset = ((y + offsetY) * width) + x;
+ ref var destinationRowRef = ref Unsafe.Add(ref blockRef, offsetY * 8);
+ for (var offsetX = 0; offsetX < blockWidth; offsetX++)
+ {
+ Unsafe.Add(ref destinationRowRef, offsetX) = Unsafe.Add(ref sourceRef, ((sourceRowOffset + offsetX) * componentCount) + componentIndex);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/War3Net.Drawing.Blp/War3Net.Drawing.Blp.csproj b/src/War3Net.Drawing.Blp/War3Net.Drawing.Blp.csproj
index 8bf46a1f..7d00c5a8 100644
--- a/src/War3Net.Drawing.Blp/War3Net.Drawing.Blp.csproj
+++ b/src/War3Net.Drawing.Blp/War3Net.Drawing.Blp.csproj
@@ -7,7 +7,7 @@
- War3Net.Drawing.Blp is a library for reading files with the ".blp" extension.
+ War3Net.Drawing.Blp is a library for reading and writing files with the ".blp" extension.
blp;warcraft3
@@ -15,7 +15,7 @@
true
-
+
diff --git a/tests/War3Net.Drawing.Blp.Tests/BlpEncoderRoundtripTest.cs b/tests/War3Net.Drawing.Blp.Tests/BlpEncoderRoundtripTest.cs
new file mode 100644
index 00000000..c862b9ba
--- /dev/null
+++ b/tests/War3Net.Drawing.Blp.Tests/BlpEncoderRoundtripTest.cs
@@ -0,0 +1,156 @@
+// ------------------------------------------------------------------------------
+//
+// Licensed under the MIT license.
+// See the LICENSE file in the project root for more information.
+//
+// ------------------------------------------------------------------------------
+
+using System;
+using System.IO;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.PixelFormats;
+
+using War3Net.TestTools.UnitTesting;
+
+namespace War3Net.Drawing.Blp.Tests
+{
+ [TestClass]
+ public class BlpEncoderRoundtripTest
+ {
+ ///
+ /// Performs a roundtrip test: PNG -> BLP -> PNG for all mipmaps.
+ /// Uses VillageFallStonePath.png as input, encodes it to BLP1 JPEG,
+ /// then decodes all mipmap levels and saves them as separate PNG files.
+ ///
+ [TestMethod]
+ public void TestRoundtrip_VillageFallStonePath()
+ {
+ // Load the test image
+ var inputPngPath = TestDataProvider.GetPath("Blp/VillageFallStonePath.png");
+
+ if (!File.Exists(inputPngPath))
+ {
+ Assert.Inconclusive($"Test image not found: {inputPngPath}");
+ return;
+ }
+
+ // Load PNG
+ var bgra = LoadPngAsBgra(inputPngPath, out var width, out var height);
+ Console.WriteLine($"Loaded PNG: {width}x{height} from {inputPngPath}");
+
+ // Create output directory
+ var outputDir = Path.Combine(Path.GetTempPath(), "BlpRoundtripTest_VillageFallStonePath");
+ Directory.CreateDirectory(outputDir);
+
+ try
+ {
+ // Save copy of original for comparison
+ var originalCopyPath = Path.Combine(outputDir, "original.png");
+ SaveAsPng(bgra, width, height, originalCopyPath);
+ Console.WriteLine($"Saved original copy: {originalCopyPath}");
+
+ // Encode to BLP
+ var blpPath = Path.Combine(outputDir, "encoded.blp");
+
+ // Use default options
+ var options = new Blp1EncodingOptions();
+
+ var encoder = new BlpEncoder(options);
+ using (var blpStream = File.Create(blpPath))
+ {
+ encoder.Encode(blpStream, width, height, bgra);
+ }
+
+ Console.WriteLine($"Encoded to BLP: {blpPath}");
+
+ // Decode BLP and save all mipmaps as PNG
+ using (var blpStream = File.OpenRead(blpPath))
+ {
+ using var blpFile = new BlpFile(blpStream);
+
+ Console.WriteLine($"BLP contains {blpFile.MipMapCount} mipmap levels");
+ Console.WriteLine($"Base dimensions: {blpFile.Width}x{blpFile.Height}");
+
+ // Decode and save each mipmap
+ for (var level = 0; level < blpFile.MipMapCount; level++)
+ {
+ var pixels = blpFile.GetPixels(level, out var mipWidth, out var mipHeight, bgra: true);
+ var mipPngPath = Path.Combine(outputDir, $"mipmap_{level}_{mipWidth}x{mipHeight}.png");
+
+ SaveAsPng(pixels, mipWidth, mipHeight, mipPngPath);
+ Console.WriteLine($" Saved mipmap {level}: {mipPngPath} ({mipWidth}x{mipHeight})");
+
+ // Verify dimensions
+ var expectedWidth = Math.Max(1, width >> level);
+ var expectedHeight = Math.Max(1, height >> level);
+ Assert.AreEqual(expectedWidth, mipWidth, $"Mipmap {level} width mismatch");
+ Assert.AreEqual(expectedHeight, mipHeight, $"Mipmap {level} height mismatch");
+
+ // For level 0, compare with original (allowing for JPEG compression artifacts)
+ if (level == 0)
+ {
+ var differences = 0;
+ var maxDiff = 0;
+ long totalDiff = 0;
+
+ for (var i = 0; i < bgra.Length; i++)
+ {
+ var diff = Math.Abs(bgra[i] - pixels[i]);
+ if (diff > 0)
+ {
+ differences++;
+ totalDiff += diff;
+ maxDiff = Math.Max(maxDiff, diff);
+ }
+ }
+
+ var avgDiff = differences > 0 ? (double)totalDiff / differences : 0;
+ Console.WriteLine($" Comparison with original:");
+ Console.WriteLine($" Pixels different: {differences} / {bgra.Length} ({100.0 * differences / bgra.Length:F2}%)");
+ Console.WriteLine($" Max difference: {maxDiff}");
+ Console.WriteLine($" Avg difference: {avgDiff:F2}");
+
+ // Assert that images are reasonably similar (JPEG is lossy)
+ Assert.IsTrue(maxDiff < 50, $"Max pixel difference too high: {maxDiff}");
+ }
+ }
+ }
+
+ Console.WriteLine();
+ Console.WriteLine($"Roundtrip test completed successfully!");
+ Console.WriteLine($"Output directory: {outputDir}");
+ }
+ finally
+ {
+ // Note: Not deleting output directory so results can be inspected
+ Console.WriteLine();
+ Console.WriteLine($"Test files kept in: {outputDir}");
+ }
+ }
+
+ private static void SaveAsPng(byte[] bgra, int width, int height, string path)
+ {
+ using (var image = Image.LoadPixelData(bgra, width, height))
+ {
+ image.SaveAsPng(path);
+ }
+ }
+
+ private static byte[] LoadPngAsBgra(string path, out int width, out int height)
+ {
+ using (var image = Image.Load(path))
+ {
+ width = image.Width;
+ height = image.Height;
+
+ var bgra = new byte[width * height * 4];
+ image.CopyPixelDataTo(bgra);
+
+ return bgra;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/War3Net.Drawing.Blp.Tests/War3Net.Drawing.Blp.Tests.csproj b/tests/War3Net.Drawing.Blp.Tests/War3Net.Drawing.Blp.Tests.csproj
index 8739e6a3..9bdfd75f 100644
--- a/tests/War3Net.Drawing.Blp.Tests/War3Net.Drawing.Blp.Tests.csproj
+++ b/tests/War3Net.Drawing.Blp.Tests/War3Net.Drawing.Blp.Tests.csproj
@@ -5,6 +5,10 @@
net$(NETCoreAppMaximumVersion);net$(NETCoreAppMaximumVersion)-windows
+
+
+
+