mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
RSI loader improvements:
1. Stop using NJsonSchema, it didn't do anything useful. 2. Use System.Text.Json instead of Newtonsoft.Json. 3. General cleanup of the code, using arrays instead of lists, etc...
This commit is contained in:
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "RSI Image Format Validation Schema V1",
|
||||
"description": "Robust Station Image",
|
||||
"type": "object",
|
||||
"definitions": {
|
||||
"size": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {"type": "integer", "minimum": 1},
|
||||
"y": {"type": "integer", "minimum": 1}
|
||||
},
|
||||
"required": ["x","y"]
|
||||
},
|
||||
"directions": {
|
||||
"type": "integer",
|
||||
"enum": [1,4,8]
|
||||
},
|
||||
"state": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"flags": {"type": "object"}, //To be de-serialized as a Dictionary
|
||||
"directions": {"$ref": "#/definitions/directions"},
|
||||
"delays": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {"type": "number", "minimum": 0, "exclusiveMinimum": true} //number == float
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name"] //'delays' is marked as optional in the spec
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"version": {"type": "integer", "minimum": 1, "maximum": 1},
|
||||
"size": {"$ref": "#/definitions/size"},
|
||||
"states": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/definitions/state"},
|
||||
"minItems": 1
|
||||
}
|
||||
},
|
||||
"required": ["version","size","states"]
|
||||
}
|
||||
@@ -2,17 +2,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
#if DEBUG
|
||||
using NJsonSchema;
|
||||
#endif
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
@@ -24,6 +19,14 @@ namespace Robust.Client.ResourceManagement
|
||||
/// </summary>
|
||||
public sealed class RSIResource : BaseResource
|
||||
{
|
||||
private static readonly float[] OneArray = {1};
|
||||
|
||||
private static readonly JsonSerializerOptions SerializerOptions =
|
||||
new JsonSerializerOptions(JsonSerializerDefaults.Web)
|
||||
{
|
||||
AllowTrailingCommas = true
|
||||
};
|
||||
|
||||
public RSI RSI { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
@@ -38,64 +41,36 @@ namespace Robust.Client.ResourceManagement
|
||||
|
||||
public override void Load(IResourceCache cache, ResourcePath path)
|
||||
{
|
||||
var manifestPath = path / "meta.json";
|
||||
string manifestContents;
|
||||
var metadata = LoadRsiMetadata(cache, path);
|
||||
|
||||
using (var manifestFile = cache.ContentFileRead(manifestPath))
|
||||
using (var reader = new StreamReader(manifestFile))
|
||||
{
|
||||
manifestContents = reader.ReadToEnd();
|
||||
}
|
||||
var stateCount = metadata.States.Length;
|
||||
var toAtlas = new StateReg[stateCount];
|
||||
|
||||
#if DEBUG
|
||||
if (RSISchema != null)
|
||||
{
|
||||
var errors = RSISchema.Validate(manifestContents);
|
||||
if (errors.Count != 0)
|
||||
{
|
||||
Logger.Error($"Unable to load RSI from '{path}', {errors.Count} errors:");
|
||||
|
||||
foreach (var error in errors)
|
||||
{
|
||||
Logger.Error("{0}", error.ToString());
|
||||
}
|
||||
|
||||
throw new RSILoadException($"{errors.Count} errors while loading RSI. See console.");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Ok schema validated just fine.
|
||||
var manifestJson = JObject.Parse(manifestContents);
|
||||
|
||||
var toAtlas = new List<(Image<Rgba32> src, Texture[][] output, int[][] indices, Vector2i[][] offsets, int totalFrameCount)>();
|
||||
|
||||
var metaData = ParseMetaData(manifestJson);
|
||||
var frameSize = metaData.Size;
|
||||
var frameSize = metadata.Size;
|
||||
var rsi = new RSI(frameSize, path);
|
||||
|
||||
var callbackOffsets = new Dictionary<RSI.StateId, Vector2i[][]>();
|
||||
var callbackOffsets = new Dictionary<RSI.StateId, Vector2i[][]>(stateCount);
|
||||
|
||||
// Do every state.
|
||||
foreach (var stateObject in metaData.States)
|
||||
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 texPath = path / (stateObject.StateId + ".png");
|
||||
var stream = cache.ContentFileRead(texPath);
|
||||
Image<Rgba32> image;
|
||||
using (stream)
|
||||
using (var stream = cache.ContentFileRead(texPath))
|
||||
{
|
||||
image = Image.Load<Rgba32>(stream);
|
||||
reg.Src = Image.Load<Rgba32>(stream);
|
||||
}
|
||||
var sheetSize = new Vector2i(image.Width, image.Height);
|
||||
|
||||
if (sheetSize.X % frameSize.X != 0 || sheetSize.Y % frameSize.Y != 0)
|
||||
if (reg.Src.Width % frameSize.X != 0 || reg.Src.Height % 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);
|
||||
reg.TotalFrameCount = stateObject.Delays.Sum(delayList => delayList.Length);
|
||||
|
||||
var (foldedDelays, foldedIndices) = FoldDelays(stateObject.Delays);
|
||||
|
||||
@@ -108,15 +83,18 @@ namespace Robust.Client.ResourceManagement
|
||||
callbackOffset[i] = new Vector2i[foldedIndices[0].Length];
|
||||
}
|
||||
|
||||
reg.Output = textures;
|
||||
reg.Indices = foldedIndices;
|
||||
reg.Offsets = callbackOffset;
|
||||
|
||||
var state = new RSI.State(frameSize, stateObject.StateId, stateObject.DirType, foldedDelays, textures);
|
||||
rsi.AddState(state);
|
||||
|
||||
toAtlas.Add((image, textures, foldedIndices, callbackOffset, frameCount));
|
||||
callbackOffsets[stateObject.StateId] = callbackOffset;
|
||||
}
|
||||
|
||||
// Poorly hacked in texture atlas support here.
|
||||
var totalFrameCount = toAtlas.Sum(p => p.totalFrameCount);
|
||||
var totalFrameCount = toAtlas.Sum(p => p.TotalFrameCount);
|
||||
|
||||
// Generate atlas.
|
||||
var dimensionX = (int) MathF.Ceiling(MathF.Sqrt(totalFrameCount));
|
||||
@@ -125,12 +103,13 @@ namespace Robust.Client.ResourceManagement
|
||||
using var sheet = new Image<Rgba32>(dimensionX * frameSize.X, dimensionY * frameSize.Y);
|
||||
|
||||
var sheetIndex = 0;
|
||||
foreach (var (src, _, _, _, frameCount) in toAtlas)
|
||||
for (var index = 0; index < toAtlas.Length; index++)
|
||||
{
|
||||
ref var reg = ref toAtlas[index];
|
||||
// Blit all the frames over.
|
||||
for (var i = 0; i < frameCount; i++)
|
||||
for (var i = 0; i < reg.TotalFrameCount; i++)
|
||||
{
|
||||
var srcWidth = (src.Width / frameSize.X);
|
||||
var srcWidth = (reg.Src.Width / frameSize.X);
|
||||
var srcColumn = i % srcWidth;
|
||||
var srcRow = i / srcWidth;
|
||||
var srcPos = (srcColumn * frameSize.X, srcRow * frameSize.Y);
|
||||
@@ -141,23 +120,24 @@ namespace Robust.Client.ResourceManagement
|
||||
|
||||
var srcBox = UIBox2i.FromDimensions(srcPos, frameSize);
|
||||
|
||||
src.Blit(srcBox, sheet, sheetPos);
|
||||
reg.Src.Blit(srcBox, sheet, sheetPos);
|
||||
}
|
||||
|
||||
sheetIndex += frameCount;
|
||||
sheetIndex += reg.TotalFrameCount;
|
||||
}
|
||||
|
||||
// Load atlas.
|
||||
var texture = Texture.LoadFromImage(sheet, path.ToString());
|
||||
|
||||
var sheetOffset = 0;
|
||||
foreach (var (_, output, indices, offsets, frameCount) in toAtlas)
|
||||
for (var toAtlasIndex = 0; toAtlasIndex < toAtlas.Length; toAtlasIndex++)
|
||||
{
|
||||
for (var i = 0; i < indices.Length; i++)
|
||||
ref var reg = ref toAtlas[toAtlasIndex];
|
||||
for (var i = 0; i < reg.Indices.Length; i++)
|
||||
{
|
||||
var dirIndices = indices[i];
|
||||
var dirOutput = output[i];
|
||||
var dirOffsets = offsets[i];
|
||||
var dirIndices = reg.Indices[i];
|
||||
var dirOutput = reg.Output[i];
|
||||
var dirOffsets = reg.Offsets[i];
|
||||
|
||||
for (var j = 0; j < dirIndices.Length; j++)
|
||||
{
|
||||
@@ -172,12 +152,13 @@ namespace Robust.Client.ResourceManagement
|
||||
}
|
||||
}
|
||||
|
||||
sheetOffset += frameCount;
|
||||
sheetOffset += reg.TotalFrameCount;
|
||||
}
|
||||
|
||||
foreach (var (image, _, _, _, _) in toAtlas)
|
||||
for (var i = 0; i < toAtlas.Length; i++)
|
||||
{
|
||||
image.Dispose();
|
||||
ref var reg = ref toAtlas[i];
|
||||
reg.Src.Dispose();
|
||||
}
|
||||
|
||||
RSI = rsi;
|
||||
@@ -188,6 +169,90 @@ namespace Robust.Client.ResourceManagement
|
||||
}
|
||||
}
|
||||
|
||||
private static RsiMetadata LoadRsiMetadata(IResourceCache cache, ResourcePath path)
|
||||
{
|
||||
var manifestPath = path / "meta.json";
|
||||
string manifestContents;
|
||||
|
||||
using (var manifestFile = cache.ContentFileRead(manifestPath))
|
||||
using (var reader = new StreamReader(manifestFile))
|
||||
{
|
||||
manifestContents = reader.ReadToEnd();
|
||||
}
|
||||
|
||||
// Ok schema validated just fine.
|
||||
var manifestJson = JsonSerializer.Deserialize<RsiJsonMetadata>(manifestContents, SerializerOptions);
|
||||
|
||||
if (manifestJson == null)
|
||||
throw new RSILoadException("Manifest JSON was null!");
|
||||
|
||||
var size = manifestJson.Size;
|
||||
var states = new StateMetadata[manifestJson.States.Length];
|
||||
|
||||
for (var stateI = 0; stateI < manifestJson.States.Length; stateI++)
|
||||
{
|
||||
var stateObject = manifestJson.States[stateI];
|
||||
var stateName = stateObject.Name;
|
||||
RSI.State.DirectionType directions;
|
||||
int dirValue;
|
||||
|
||||
if (stateObject.Directions is { } dirVal)
|
||||
{
|
||||
dirValue = dirVal;
|
||||
directions = dirVal switch
|
||||
{
|
||||
1 => RSI.State.DirectionType.Dir1,
|
||||
4 => RSI.State.DirectionType.Dir4,
|
||||
8 => RSI.State.DirectionType.Dir8,
|
||||
_ => throw new RSILoadException($"Invalid direction: {dirValue} expected 1, 4 or 8")
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
dirValue = 1;
|
||||
directions = RSI.State.DirectionType.Dir1;
|
||||
}
|
||||
|
||||
// We can ignore selectors and flags for now,
|
||||
// because they're not used yet!
|
||||
|
||||
// Get the lists of delays.
|
||||
float[][] delays;
|
||||
if (stateObject.Delays != null)
|
||||
{
|
||||
delays = stateObject.Delays;
|
||||
|
||||
if (delays.Length != dirValue)
|
||||
{
|
||||
throw new RSILoadException(
|
||||
"DirectionsdirectionFramesList count does not match amount of delays specified.");
|
||||
}
|
||||
|
||||
for (var i = 0; i < delays.Length; i++)
|
||||
{
|
||||
var delayList = delays[i];
|
||||
if (delayList.Length == 0)
|
||||
{
|
||||
delays[i] = OneArray;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
delays = new float[dirValue][];
|
||||
// No delays specified, default to 1 frame per dir.
|
||||
for (var i = 0; i < dirValue; i++)
|
||||
{
|
||||
delays[i] = OneArray;
|
||||
}
|
||||
}
|
||||
|
||||
states[stateI] = new StateMetadata(new RSI.StateId(stateName), directions, delays);
|
||||
}
|
||||
|
||||
return new RsiMetadata(size, states);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Folds a per-directional sets of animation delays
|
||||
/// into an equivalent set of animation delays and indices that works for every direction.
|
||||
@@ -336,109 +401,25 @@ namespace Robust.Client.ResourceManagement
|
||||
return (floatDelays, arrayIndices);
|
||||
}
|
||||
|
||||
internal static RsiMetadata ParseMetaData(JObject manifestJson)
|
||||
private struct StateReg
|
||||
{
|
||||
var size = manifestJson["size"]!.ToObject<Vector2i>();
|
||||
var states = new List<StateMetadata>();
|
||||
|
||||
foreach (var stateObject in manifestJson["states"]!.Cast<JObject>())
|
||||
{
|
||||
var stateName = stateObject["name"]!.ToObject<string>()!;
|
||||
RSI.State.DirectionType directions;
|
||||
int dirValue;
|
||||
|
||||
if (stateObject.TryGetValue("directions", out var dirJToken))
|
||||
{
|
||||
dirValue= dirJToken.ToObject<int>();
|
||||
directions = dirValue switch
|
||||
{
|
||||
1 => RSI.State.DirectionType.Dir1,
|
||||
4 => RSI.State.DirectionType.Dir4,
|
||||
8 => RSI.State.DirectionType.Dir8,
|
||||
_ => throw new RSILoadException($"Invalid direction: {dirValue} expected 1, 4 or 8")
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
dirValue = 1;
|
||||
directions = RSI.State.DirectionType.Dir1;
|
||||
}
|
||||
|
||||
// We can ignore selectors and flags for now,
|
||||
// because they're not used yet!
|
||||
|
||||
// Get the lists of delays.
|
||||
float[][] delays;
|
||||
if (stateObject.TryGetValue("delays", out var delayToken))
|
||||
{
|
||||
delays = delayToken.ToObject<float[][]>()!;
|
||||
|
||||
if (delays.Length != dirValue)
|
||||
{
|
||||
throw new RSILoadException(
|
||||
"DirectionsdirectionFramesList count does not match amount of delays specified.");
|
||||
}
|
||||
|
||||
for (var i = 0; i < delays.Length; i++)
|
||||
{
|
||||
var delayList = delays[i];
|
||||
if (delayList.Length == 0)
|
||||
{
|
||||
delays[i] = new float[] {1};
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
delays = new float[dirValue][];
|
||||
// No delays specified, default to 1 frame per dir.
|
||||
for (var i = 0; i < dirValue; i++)
|
||||
{
|
||||
delays[i] = new float[] {1};
|
||||
}
|
||||
}
|
||||
|
||||
states.Add(new StateMetadata(new RSI.StateId(stateName), directions, delays));
|
||||
}
|
||||
|
||||
return new RsiMetadata(size, states);
|
||||
public Image<Rgba32> Src;
|
||||
public Texture[][] Output;
|
||||
public int[][] Indices;
|
||||
public Vector2i[][] Offsets;
|
||||
public int TotalFrameCount;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private static readonly JsonSchema? RSISchema = GetSchema();
|
||||
|
||||
private static JsonSchema? GetSchema()
|
||||
{
|
||||
try
|
||||
{
|
||||
string schema;
|
||||
using (var schemaStream = Assembly.GetExecutingAssembly()
|
||||
.GetManifestResourceStream("Robust.Client.Graphics.RSI.RSISchema.json")!)
|
||||
using (var schemaReader = new StreamReader(schemaStream))
|
||||
{
|
||||
schema = schemaReader.ReadToEnd();
|
||||
}
|
||||
|
||||
return JsonSchema.FromJsonAsync(schema).Result;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
System.Console.WriteLine("Failed to load RSI JSON Schema!\n{0}", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
internal sealed class RsiMetadata
|
||||
{
|
||||
public RsiMetadata(Vector2i size, List<StateMetadata> states)
|
||||
public RsiMetadata(Vector2i size, StateMetadata[] states)
|
||||
{
|
||||
Size = size;
|
||||
States = states;
|
||||
}
|
||||
|
||||
public Vector2i Size { get; }
|
||||
public List<StateMetadata> States { get; }
|
||||
public StateMetadata[] States { get; }
|
||||
}
|
||||
|
||||
internal sealed class StateMetadata
|
||||
@@ -466,6 +447,17 @@ namespace Robust.Client.ResourceManagement
|
||||
|
||||
public float[][] Delays { get; }
|
||||
}
|
||||
|
||||
// To be directly deserialized.
|
||||
[UsedImplicitly]
|
||||
private sealed record RsiJsonMetadata(Vector2i Size, StateJsonMetadata[] States)
|
||||
{
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
private sealed record StateJsonMetadata(string Name, int? Directions, float[][]? Delays)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.0.175" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" PrivateAssets="All" />
|
||||
<PackageReference Include="nfluidsynth" Version="0.3.1" />
|
||||
<PackageReference Include="NJsonSchema" Version="10.3.8" />
|
||||
<PackageReference Include="NVorbis" Version="0.10.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.3" />
|
||||
@@ -39,9 +38,6 @@
|
||||
<ProjectReference Include="..\Robust.Shared\Robust.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Graphics\RSI\RSISchema.json" Condition="'$(Configuration)' == 'Debug'">
|
||||
<LogicalName>Robust.Client.Graphics.RSI.RSISchema.json</LogicalName>
|
||||
</EmbeddedResource>
|
||||
|
||||
<EmbeddedResource Include="Graphics\Clyde\Shaders\*" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Robust.Shared.Maths
|
||||
@@ -16,12 +17,12 @@ namespace Robust.Shared.Maths
|
||||
/// <summary>
|
||||
/// The X component of the Vector2i.
|
||||
/// </summary>
|
||||
public int X;
|
||||
[JsonInclude] public int X;
|
||||
|
||||
/// <summary>
|
||||
/// The Y component of the Vector2i.
|
||||
/// </summary>
|
||||
public int Y;
|
||||
[JsonInclude] public int Y;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a vector from its coordinates.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Robust.Shared.Maths
|
||||
@@ -12,12 +13,12 @@ namespace Robust.Shared.Maths
|
||||
/// <summary>
|
||||
/// The X component of the Vector2i.
|
||||
/// </summary>
|
||||
public uint X;
|
||||
[JsonInclude] public uint X;
|
||||
|
||||
/// <summary>
|
||||
/// The Y component of the Vector2i.
|
||||
/// </summary>
|
||||
public uint Y;
|
||||
[JsonInclude] public uint Y;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a vector from its coordinates.
|
||||
|
||||
Reference in New Issue
Block a user