mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
* Move to Central Package Management. Allows us to store NuGet package versions all in one place. Yay! * Update NuGet packages and fix code for changes. Notable: Changes to ILVerify. Npgsql doesn't need hacks for inet anymore, now we need hacks to make the old code work with this new reality. NUnit's analyzers are already complaining and I didn't even update it to 4.x yet. TerraFX changed to GetLastSystemError so error handling had to be changed. Buncha APIs have more NRT annotations. * Remove dotnet-eng NuGet package source. I genuinely don't know what this was for, and Central Package Management starts throwing warnings about it, so YEET. * Fix double loading of assemblies due to ALC shenanigans. Due to how the "sideloading" code for the ModLoader was set up, it would first try to load Microsoft.Extensions.Primitives from next to the content dll. But we already have that library in Robust! Chaos ensues. We now try to forcibly prioritize loading from the default ALC first to avoid this. * Remove Robust.Physics project. Never used. * Remove erroneous NVorbis reference. Should be VorbisPizza and otherwise wasn't used. * Sandbox fixes * Remove unused unit test package references. Castle.Core and NUnit.ConsoleRunner. * Update NUnit to 4.0.1 This requires replacing all the old assertion methods because they removed them 🥲 * Mute CA1416 (platform check) errors TerraFX started annotating APIs with this and I can't be arsed to entertain this analyzer so out it goes. * Fine ya cranky, no more CPM for Robust.Client.Injectors * Changelog * Oh so that's what dotnet-eng was used for. Yeah ok that makes sense. * Central package management for remaining 2 robust projects * Ok that was a bad idea let's just use NUnit 3 on the analyzer test project * Oh right forgot to remove this one * Update to a newer version of RemoteExecutor * Disable RemoteExecutor test https://github.com/dotnet/arcade/issues/8483 Yeah this package is not well maintained and clearly we can't rely on it. * Fix immutable list serialization
192 lines
6.2 KiB
C#
192 lines
6.2 KiB
C#
using System.Text.RegularExpressions;
|
|
using Robust.Shared.Maths;
|
|
using Robust.Shared.Resources;
|
|
using Robust.Shared.Utility;
|
|
using SixLabors.ImageSharp;
|
|
using SixLabors.ImageSharp.Formats.Png;
|
|
using SixLabors.ImageSharp.Formats.Png.Chunks;
|
|
using SixLabors.ImageSharp.PixelFormats;
|
|
|
|
namespace Robust.Packaging.AssetProcessing.Passes;
|
|
|
|
// This is a proof of concept/example. The client is not currently able to load these.
|
|
|
|
/// <summary>
|
|
/// Packs .rsi bundles into .rsic files,
|
|
/// that are single pre-atlassed PNG files with JSON metadata embedded in the PNG header.
|
|
/// </summary>
|
|
internal sealed class AssetPassPackRsis : AssetPass
|
|
{
|
|
private readonly Dictionary<string, RsiDat> _foundRsis = new();
|
|
|
|
private static readonly Regex RegexMetaJson = new(@"^(.+)\.rsi/meta\.json$");
|
|
private static readonly Regex RegexPng = new(@"^(.+)\.rsi/(.+)\.png$");
|
|
|
|
protected override AssetFileAcceptResult AcceptFile(AssetFile file)
|
|
{
|
|
if (!file.Path.Contains(".rsi/"))
|
|
return AssetFileAcceptResult.Pass;
|
|
|
|
// .rsi/meta.json
|
|
var matchMetaJson = RegexMetaJson.Match(file.Path);
|
|
if (matchMetaJson.Success)
|
|
{
|
|
lock (_foundRsis)
|
|
{
|
|
var dat = _foundRsis.GetOrNew(matchMetaJson.Groups[1].Value);
|
|
dat.MetaJson = file;
|
|
}
|
|
|
|
return AssetFileAcceptResult.Consumed;
|
|
}
|
|
|
|
// .rsi/*.png
|
|
var matchPng = RegexPng.Match(file.Path);
|
|
if (matchPng.Success)
|
|
{
|
|
lock (_foundRsis)
|
|
{
|
|
var dat = _foundRsis.GetOrNew(matchPng.Groups[1].Value);
|
|
dat.StatesFound.Add(matchPng.Groups[2].Value, file);
|
|
|
|
return AssetFileAcceptResult.Consumed;
|
|
}
|
|
}
|
|
|
|
return AssetFileAcceptResult.Pass;
|
|
}
|
|
|
|
protected override void AcceptFinished()
|
|
{
|
|
// ReSharper disable once InconsistentlySynchronizedField
|
|
foreach (var (key, dat) in _foundRsis)
|
|
{
|
|
if (dat.MetaJson == null)
|
|
continue;
|
|
|
|
RunJob(() =>
|
|
{
|
|
// Console.WriteLine($"Packing RSI: {key}");
|
|
|
|
var result = PackRsi($"{key}.rsi", dat);
|
|
SendFile(result);
|
|
});
|
|
}
|
|
}
|
|
|
|
private static AssetFile PackRsi(string rsiPath, RsiDat dat)
|
|
{
|
|
RsiLoading.RsiMetadata metadata;
|
|
string metaJson;
|
|
using (var manifestFile = dat.MetaJson!.Open())
|
|
{
|
|
metadata = RsiLoading.LoadRsiMetadata(manifestFile);
|
|
|
|
manifestFile.Position = 0;
|
|
using var sr = new StreamReader(manifestFile);
|
|
metaJson = sr.ReadToEnd();
|
|
}
|
|
|
|
// Check for duplicate states
|
|
for (var i = 0; i < metadata.States.Length; i++)
|
|
{
|
|
var stateId = metadata.States[i].StateId;
|
|
|
|
for (int j = i + 1; j < metadata.States.Length; j++)
|
|
{
|
|
if (stateId == metadata.States[j].StateId)
|
|
throw new RSILoadException($"RSI '{rsiPath}' has a duplicate stateId '{stateId}'.");
|
|
}
|
|
}
|
|
|
|
var stateCount = metadata.States.Length;
|
|
var toAtlas = new StateReg[stateCount];
|
|
|
|
var frameSize = metadata.Size;
|
|
|
|
// Do every state.
|
|
for (var index = 0; index < metadata.States.Length; index++)
|
|
{
|
|
ref var reg = ref toAtlas[index];
|
|
|
|
var stateObject = metadata.States[index];
|
|
// Load image from disk.
|
|
var texFile = dat.StatesFound[stateObject.StateId];
|
|
using (var stream = texFile.Open())
|
|
{
|
|
reg.Src = Image.Load<Rgba32>(stream);
|
|
}
|
|
|
|
if (reg.Src.Width % frameSize.X != 0 || reg.Src.Height % frameSize.Y != 0)
|
|
{
|
|
var regDims = $"{reg.Src.Width}x{reg.Src.Height}";
|
|
var iconDims = $"{frameSize.X}x{frameSize.Y}";
|
|
throw new RSILoadException(
|
|
$"State '{stateObject.StateId}' image size ({regDims}) is not a multiple of the icon size ({iconDims}).");
|
|
}
|
|
|
|
// Load all frames into a list so we can operate on it more sanely.
|
|
reg.TotalFrameCount = stateObject.Delays.Sum(delayList => delayList.Length);
|
|
}
|
|
|
|
// 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);
|
|
|
|
var sheet = new Image<Rgba32>(dimensionX * frameSize.X, dimensionY * frameSize.Y);
|
|
|
|
var sheetIndex = 0;
|
|
for (var index = 0; index < toAtlas.Length; index++)
|
|
{
|
|
ref var reg = ref toAtlas[index];
|
|
// Blit all the frames over.
|
|
for (var i = 0; i < reg.TotalFrameCount; i++)
|
|
{
|
|
var srcWidth = (reg.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);
|
|
|
|
ImageOps.Blit(reg.Src, srcBox, sheet, sheetPos);
|
|
}
|
|
|
|
sheetIndex += reg.TotalFrameCount;
|
|
}
|
|
|
|
for (var i = 0; i < toAtlas.Length; i++)
|
|
{
|
|
ref var reg = ref toAtlas[i];
|
|
reg.Src.Dispose();
|
|
}
|
|
|
|
var ms = new MemoryStream();
|
|
sheet.Metadata.GetPngMetadata().TextData.Add(new PngTextData("Description", metaJson, "", ""));
|
|
sheet.SaveAsPng(ms);
|
|
|
|
sheet.Dispose();
|
|
|
|
return new AssetFileMemory($"{rsiPath}c", ms.ToArray());
|
|
}
|
|
|
|
internal struct StateReg
|
|
{
|
|
public Image<Rgba32> Src;
|
|
public int TotalFrameCount;
|
|
}
|
|
|
|
private sealed class RsiDat
|
|
{
|
|
public AssetFile? MetaJson;
|
|
public readonly Dictionary<string, AssetFile> StatesFound = new();
|
|
}
|
|
}
|