Compare commits

4 Commits

Author SHA1 Message Date
e76952cd0d Asset compiler first iteration: 2026-06-14 21:18:50 +02:00
2207116d0a spart to data 2026-06-14 21:00:14 +02:00
201b13f80f Scaffolded resource manager 2026-06-14 20:39:39 +02:00
e3f9c1cc52 Fixes extractor not working post merge 2026-06-14 19:12:05 +02:00
24 changed files with 944 additions and 34 deletions

View File

@@ -7,8 +7,10 @@ using System.IO;
namespace _2DGAMELIB
{
[Serializable]
public class BodyTemplate
public class BodyTemplate
{
public string Tag = ""; // I didn't remove this, but it has been removed on merging my changes? What? - REDCODE
public OrderedDictionary<string, VariantGrid> Difss = new OrderedDictionary<string, VariantGrid>();
private VariantGrid r;

View File

@@ -78,6 +78,7 @@ namespace _2DGAMELIB
public bool Dra = true;
public bool Hit = true;
private bool closed;
public bool IsClosed => closed;
public PartGroup GetParent()
{

View File

@@ -0,0 +1,187 @@
using System.Text.RegularExpressions;
using SlaveEngine.AssetBuilder.Models;
using SlaveEngine.Assets;
using SlaveEngine.Assets.Models;
using SlaveEngine.Assets.Primitives;
using Spectre.Console;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace SlaveEngine.AssetBuilder;
public class Compiler {
private readonly string _inputDir;
private readonly string _outputDir;
private readonly IDeserializer _yaml;
public Compiler(string inputDir, string outputDir) {
_inputDir = inputDir;
_outputDir = outputDir;
_yaml = new DeserializerBuilder()
.WithNamingConvention(UnderscoredNamingConvention.Instance)
.IgnoreUnmatchedProperties()
.Build();
}
private T LoadYaml<T>(string path) {
var text = File.ReadAllText(path);
text = NormalizeFlowArrays(text);
return _yaml.Deserialize<T>(text);
}
private static string NormalizeFlowArrays(string yaml) {
return Regex.Replace(yaml,
@"(position:\s*)\[([\s\S]*?)\]",
m => {
var prefix = m.Groups[1].Value.TrimEnd();
var values = m.Groups[2].Value
.Replace('\n', ' ')
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(v => v.Trim())
.Where(v => v.Length > 0)
.ToList();
if (values.Count == 0) return prefix + " []";
return prefix + "\n" +
string.Join("\n", values.Select(v => $" - {v}"));
});
}
public int Run() {
AnsiConsole.Write(new Rule("[yellow]SlaveEngine Asset Builder[/]") { Justification = Justify.Left });
AnsiConsole.MarkupLine(" [bold]Input:[/] {0}", _inputDir);
AnsiConsole.MarkupLine(" [bold]Output:[/] {0}", _outputDir);
AnsiConsole.WriteLine();
Directory.CreateDirectory(_outputDir);
var parts = DiscoverParts();
if (parts.Count == 0) {
AnsiConsole.MarkupLine("[red]No parts found.[/]");
return 1;
}
var success = 0;
AnsiConsole.Progress()
.Columns(new TaskDescriptionColumn(), new ProgressBarColumn(), new PercentageColumn(), new ElapsedTimeColumn())
.Start(ctx => {
var task = ctx.AddTask("Compiling parts");
task.MaxValue = parts.Count;
foreach (var (id, partDir) in parts) {
var (ok, msg) = CompilePart(partDir);
if (ok)
AnsiConsole.MarkupLine("[green]{0}[/]", msg);
else
AnsiConsole.MarkupLine("[red]{0}[/]", msg);
if (ok) success++;
task.Increment(1);
}
});
if (success == parts.Count)
AnsiConsole.MarkupLine("[green]Done. {0}/{1} compiled.[/]", success, parts.Count);
else if (success > 0)
AnsiConsole.MarkupLine("[yellow]Done. {0}/{1} compiled.[/]", success, parts.Count);
else
AnsiConsole.MarkupLine("[red]Done. {0}/{1} compiled.[/]", success, parts.Count);
return success == parts.Count ? 0 : 1;
}
private List<(string id, string dir)> DiscoverParts() {
var parts = new List<(string id, string dir)>();
var catalogPath = Path.Combine(_inputDir, "Catalog.yaml");
if (File.Exists(catalogPath)) {
AnsiConsole.MarkupLine("[dim]Using catalog:[/] {0}", catalogPath);
var catalog = LoadYaml<CatalogYaml>(catalogPath);
if (catalog?.Parts != null) {
foreach (var entry in catalog.Parts) {
var partDir = Path.GetFullPath(Path.Combine(_inputDir, entry.Path));
if (Directory.Exists(partDir))
parts.Add((entry.Id, partDir));
else
AnsiConsole.MarkupLine("[yellow] Warning:[/] directory not found: {0}", partDir);
}
return parts;
}
}
var scanDir = Path.Combine(_inputDir, "Parts");
if (!Directory.Exists(scanDir)) return parts;
AnsiConsole.MarkupLine("[dim]Scanning:[/] {0}", scanDir);
foreach (var dir in Directory.GetDirectories(scanDir)) {
var yamlPath = Path.Combine(dir, "part.yaml");
if (!File.Exists(yamlPath)) continue;
try {
var partYaml = LoadYaml<PartYaml>(yamlPath);
if (partYaml != null && !string.IsNullOrEmpty(partYaml.Id))
parts.Add((partYaml.Id, dir));
} catch (Exception ex) {
AnsiConsole.MarkupLine("[yellow] Warning:[/] failed to parse {0}: {1}", yamlPath, ex.Message);
}
}
return parts;
}
private (bool success, string message) CompilePart(string partDir) {
var yamlPath = Path.Combine(partDir, "part.yaml");
if (!File.Exists(yamlPath))
return (false, $"Skipping (no part.yaml): {partDir}");
PartYaml partYaml;
try {
partYaml = LoadYaml<PartYaml>(yamlPath);
} catch (Exception ex) {
return (false, $"Failed to parse {yamlPath}: {ex.Message}");
}
if (partYaml == null)
return (false, $"Empty YAML: {yamlPath}");
try {
var variantList = partYaml.Variants ?? new List<VariantYaml>();
var variants = new VariantAsset[variantList.Count];
for (var i = 0; i < variantList.Count; i++) {
var v = variantList[i];
var svgPath = Path.Combine(partDir, v.File);
if (!File.Exists(svgPath)) {
variants[i] = new VariantAsset { X = v.X, Y = v.Y, Groups = Array.Empty<PathGroup>() };
continue;
}
var groups = SvgParser.Parse(svgPath);
variants[i] = new VariantAsset { X = v.X, Y = v.Y, Groups = groups };
}
var fieldList = partYaml.Fields ?? new List<FieldYaml>();
var jointList = partYaml.Joints ?? new List<JointYaml>();
var asset = new PartAsset {
Guid = Guid.NewGuid(),
FileInfo = new AssetFileInfo { Filename = partYaml.Id, FilePath = "" },
Id = partYaml.Id,
OriginalKey = partYaml.OriginalKey ?? "",
Resource = partYaml.Resource ?? "",
MorphX = partYaml.MorphX,
MorphY = partYaml.MorphY,
Fields = fieldList.Select(f => f.Name ?? "").ToArray(),
Joints = jointList.Select(j => new Joint {
X = j.Position != null && j.Position.Count > 0 ? j.Position[0] : 0,
Y = j.Position != null && j.Position.Count > 1 ? j.Position[1] : 0
}).ToArray(),
Variants = variants
};
var outputPath = Path.Combine(_outputDir, $"{partYaml.Id}.spart");
SparWriter.Write(asset, outputPath);
return (true, $" {partYaml.Id}... OK ({variants.Length} variants)");
} catch (Exception ex) {
return (false, $" {partYaml.Id}... FAILED: {ex.Message}");
}
}
}

View File

@@ -0,0 +1,35 @@
namespace SlaveEngine.AssetBuilder.Models;
public class PartYaml {
public string Id { get; set; } = "";
public string OriginalKey { get; set; } = "";
public string Resource { get; set; } = "";
public int MorphX { get; set; }
public int MorphY { get; set; }
public List<VariantYaml>? Variants { get; set; }
public List<FieldYaml>? Fields { get; set; }
public List<JointYaml>? Joints { get; set; }
}
public class VariantYaml {
public int X { get; set; }
public int Y { get; set; }
public string File { get; set; } = "";
}
public class FieldYaml {
public string Name { get; set; } = "";
}
public class JointYaml {
public List<float>? Position { get; set; }
}
public class CatalogYaml {
public List<CatalogEntry>? Parts { get; set; }
}
public class CatalogEntry {
public string Id { get; set; } = "";
public string Path { get; set; } = "";
}

View File

@@ -0,0 +1,32 @@
using System.CommandLine;
using Spectre.Console;
namespace SlaveEngine.AssetBuilder;
class Program {
static int Main(string[] args) {
var inputOption = new Option<DirectoryInfo>("--input", "Path to the input SlaveMatrix/Assets folder");
inputOption.SetDefaultValueFactory(() => new DirectoryInfo("./SlaveMatrix/Assets"));
var outputOption = new Option<DirectoryInfo>("--output", "Path to the output directory for compiled .spart files");
outputOption.SetDefaultValueFactory(() => new DirectoryInfo("./Assets"));
var rootCommand = new RootCommand(
"SlaveEngine Asset Builder — compiles YAML + SVG assets into .spart binaries");
rootCommand.AddOption(inputOption);
rootCommand.AddOption(outputOption);
rootCommand.SetHandler((DirectoryInfo input, DirectoryInfo output) => {
if (!input.Exists) {
AnsiConsole.MarkupLine("[red]Error:[/] input directory not found: {0}", input.FullName);
Environment.Exit(1);
}
var compiler = new Compiler(input.FullName, output.FullName);
var result = compiler.Run();
Environment.Exit(result);
}, inputOption, outputOption);
return rootCommand.Invoke(args);
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\SlaveEngine.Assets\SlaveEngine.Assets.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Spectre.Console" Version="0.57.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="YamlDotNet" Version="18.0.0" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,73 @@
using System.Text;
using SlaveEngine.Assets;
using SlaveEngine.Assets.Models;
using SlaveEngine.Assets.Primitives;
namespace SlaveEngine.AssetBuilder;
public static class SparWriter {
public static void Write(PartAsset asset, string outputPath) {
using var stream = File.Create(outputPath);
using var writer = new BinaryWriter(stream);
writer.Write(Encoding.UTF8.GetBytes("SPRT"));
writer.Write(1); // version
WriteString(writer, asset.Id);
WriteString(writer, asset.OriginalKey);
WriteString(writer, asset.Resource);
writer.Write(asset.MorphX);
writer.Write(asset.MorphY);
writer.Write(asset.Fields.Length);
foreach (var f in asset.Fields) WriteString(writer, f);
writer.Write(asset.Joints.Length);
foreach (var j in asset.Joints) {
writer.Write(j.X);
writer.Write(j.Y);
}
writer.Write(asset.Variants.Length);
foreach (var variant in asset.Variants) {
writer.Write(variant.X);
writer.Write(variant.Y);
writer.Write(variant.Groups.Length);
foreach (var group in variant.Groups) {
WriteString(writer, group.Name);
writer.Write(group.HasTransform);
if (group.HasTransform) {
writer.Write(group.Tx);
writer.Write(group.Ty);
writer.Write(group.Angle);
writer.Write(group.Sx);
writer.Write(group.Sy);
writer.Write(group.Bx);
writer.Write(group.By);
}
writer.Write(group.Paths.Length);
foreach (var path in group.Paths) {
WriteString(writer, path.Fill);
WriteString(writer, path.Stroke);
writer.Write(path.StrokeWidth);
writer.Write(path.IsClosed);
writer.Write(path.Commands.Length);
foreach (var cmd in path.Commands) {
writer.Write((byte)cmd.Type);
writer.Write(cmd.Args.Length);
foreach (var arg in cmd.Args) writer.Write(arg);
}
}
}
}
}
private static void WriteString(BinaryWriter writer, string s) {
var bytes = Encoding.UTF8.GetBytes(s);
writer.Write(bytes.Length);
writer.Write(bytes);
}
}

View File

@@ -0,0 +1,192 @@
using System.Globalization;
using System.Xml.Linq;
using SlaveEngine.Assets.Models;
using SlaveEngine.Assets.Primitives;
namespace SlaveEngine.AssetBuilder;
public static class SvgParser {
private static readonly CultureInfo Inv = CultureInfo.InvariantCulture;
public static PathGroup[] Parse(string svgPath) {
var doc = XDocument.Load(svgPath);
var svg = doc.Root;
if (svg == null) return Array.Empty<PathGroup>();
var groups = new List<PathGroup>();
CollectGroups(svg, groups, null);
return groups.ToArray();
}
private static void CollectGroups(XElement parent, List<PathGroup> groups, string? inheritedName) {
foreach (var child in parent.Elements()) {
var name = child.Name.LocalName;
if (name != "g" && name != "path") continue;
var id = child.Attribute("id")?.Value ?? inheritedName ?? "";
if (name == "g") {
var transformAttr = child.Attribute("transform")?.Value;
if (transformAttr != null) {
ParseTransform(transformAttr,
out var tx, out var ty, out var angle,
out var sx, out var sy, out var bx, out var by);
var paths = CollectPaths(child);
groups.Add(new PathGroup {
Name = id,
HasTransform = true,
Tx = tx, Ty = ty, Angle = angle,
Sx = sx, Sy = sy, Bx = bx, By = by,
Paths = paths
});
} else {
CollectGroups(child, groups, id);
}
}
}
}
private static PathData[] CollectPaths(XElement parent) {
var paths = new List<PathData>();
foreach (var elem in parent.Elements()) {
var name = elem.Name.LocalName;
if (name == "path") {
var d = elem.Attribute("d")?.Value ?? "";
var (commands, isClosed) = ParsePathData(d);
paths.Add(new PathData {
Fill = elem.Attribute("fill")?.Value ?? "none",
Stroke = elem.Attribute("stroke")?.Value ?? "none",
StrokeWidth = float.Parse(
elem.Attribute("stroke-width")?.Value ?? "0", Inv),
IsClosed = isClosed,
Commands = commands
});
} else if (name == "g") {
paths.AddRange(CollectPaths(elem));
}
}
return paths.ToArray();
}
private static void ParseTransform(string t,
out float tx, out float ty, out float angle,
out float sx, out float sy, out float bx, out float by) {
tx = ty = angle = 0;
sx = sy = 1;
bx = by = 0;
if (string.IsNullOrWhiteSpace(t)) return;
var parts = t.Split(')', StringSplitOptions.RemoveEmptyEntries);
foreach (var part in parts) {
var trimmed = part.TrimStart();
if (trimmed.StartsWith("translate(")) {
var args = trimmed["translate(".Length..].Split(',');
if (args.Length >= 2) {
tx = float.Parse(args[0].Trim(), Inv);
ty = float.Parse(args[1].Trim(), Inv);
}
} else if (trimmed.StartsWith("rotate(")) {
var arg = trimmed["rotate(".Length..].Trim();
angle = float.Parse(arg, Inv);
} else if (trimmed.StartsWith("scale(")) {
var args = trimmed["scale(".Length..].Split(',');
if (args.Length >= 2) {
sx = float.Parse(args[0].Trim(), Inv);
sy = float.Parse(args[1].Trim(), Inv);
}
}
}
}
internal static (BezierCommand[] commands, bool isClosed) ParsePathData(string d) {
var commands = new List<BezierCommand>();
var tokens = Tokenize(d);
var i = 0;
float cx = 0, cy = 0;
while (i < tokens.Count) {
var cmd = tokens[i][0];
i++;
switch (cmd) {
case 'M': case 'm': {
var x = float.Parse(tokens[i++], Inv);
var y = float.Parse(tokens[i++], Inv);
if (cmd == 'm') { x += cx; y += cy; }
commands.Add(new BezierCommand { Type = CommandType.MoveTo, Args = new[] { x, y } });
cx = x; cy = y;
break;
}
case 'L': case 'l': {
var x = float.Parse(tokens[i++], Inv);
var y = float.Parse(tokens[i++], Inv);
if (cmd == 'l') { x += cx; y += cy; }
commands.Add(new BezierCommand { Type = CommandType.LineTo, Args = new[] { x, y } });
cx = x; cy = y;
break;
}
case 'C': case 'c': {
var c1x = float.Parse(tokens[i++], Inv);
var c1y = float.Parse(tokens[i++], Inv);
var c2x = float.Parse(tokens[i++], Inv);
var c2y = float.Parse(tokens[i++], Inv);
var ex = float.Parse(tokens[i++], Inv);
var ey = float.Parse(tokens[i++], Inv);
if (cmd == 'c') {
c1x += cx; c1y += cy;
c2x += cx; c2y += cy;
ex += cx; ey += cy;
}
commands.Add(new BezierCommand {
Type = CommandType.CubicBezierTo,
Args = new[] { c1x, c1y, c2x, c2y, ex, ey }
});
cx = ex; cy = ey;
break;
}
case 'Z': case 'z': {
commands.Add(new BezierCommand { Type = CommandType.Close, Args = Array.Empty<float>() });
break;
}
}
}
var closed = commands.Count > 0 && commands[^1].Type == CommandType.Close;
return (commands.ToArray(), closed);
}
private static List<string> Tokenize(string d) {
var tokens = new List<string>();
var i = 0;
while (i < d.Length) {
var c = d[i];
if (char.IsWhiteSpace(c) || c == ',') { i++; continue; }
if (char.IsLetter(c)) {
var upper = char.ToUpperInvariant(c);
if (upper is 'M' or 'L' or 'C' or 'Z') {
tokens.Add(c.ToString());
i++;
continue;
}
i++;
continue;
}
if (c is '-' or '+' or '.' || char.IsDigit(c)) {
var start = i;
if (c is '-' or '+') i++;
while (i < d.Length && (char.IsDigit(d[i]) || d[i] == '.')) i++;
if (i < d.Length && (d[i] is 'e' or 'E')) {
i++;
if (i < d.Length && (d[i] is '+' or '-')) i++;
while (i < d.Length && char.IsDigit(d[i])) i++;
}
tokens.Add(d[start..i]);
continue;
}
i++;
}
return tokens;
}
}

View File

@@ -0,0 +1,6 @@
namespace SlaveEngine.Assets;
public abstract class Asset {
public Guid Guid { get; init; }
public AssetFileInfo FileInfo { get; init; }
}

View File

@@ -0,0 +1,6 @@
namespace SlaveEngine.Assets;
public struct AssetFileInfo {
public string Filename;
public string FilePath;
}

View File

@@ -0,0 +1,5 @@
namespace SlaveEngine.Assets;
public abstract class AssetProcessor {
public abstract Asset Process(AssetFileInfo fileInfo , byte[] data);
}

View File

@@ -0,0 +1,46 @@
namespace SlaveEngine.Assets;
public class AssetProcessorAttribute : Attribute {
public Type ManagedType { get; init; }
public string[] ManagedExtensions { get; init; }
public AssetProcessorAttribute(Type managedType, string[] managedExtensions)
{
ManagedType = managedType ?? throw new ArgumentNullException(nameof(managedType));
// Validate that the provided type is Asset or a subclass of Asset
if (!typeof(Asset).IsSubclassOf(managedType))
throw new ArgumentException($"The type '{managedType.FullName}' must derive from '{typeof(Asset).FullName}'.", nameof(managedType));
// Validate extensions: non-null, non-empty, normalized, start with '.', no whitespace, unique
if (managedExtensions == null)
throw new ArgumentNullException(nameof(managedExtensions));
if (managedExtensions.Length == 0)
throw new ArgumentException("At least one file extension must be provided.", nameof(managedExtensions));
var normalized = new List<string>(managedExtensions.Length);
foreach (var ext in managedExtensions)
{
if (string.IsNullOrWhiteSpace(ext))
throw new ArgumentException("Extensions must be non-empty strings.", nameof(managedExtensions));
var e = ext.Trim();
// allow either "png" or ".png" as input, normalize to lowercase with leading dot
if (!e.StartsWith('.'))
e = "." + e;
if (e.Length < 2)
throw new ArgumentException($"Invalid extension '{ext}'.", nameof(managedExtensions));
if (e.Any(char.IsWhiteSpace))
throw new ArgumentException($"Extension '{ext}' contains whitespace.", nameof(managedExtensions));
normalized.Add(e.ToLowerInvariant());
}
// ensure uniqueness
ManagedExtensions = normalized.Distinct().ToArray();
}
}

View File

@@ -0,0 +1,14 @@
using SlaveEngine.Assets.Primitives;
namespace SlaveEngine.Assets.Models;
public sealed class PartAsset : Asset {
public required string Id { get; init; }
public required string OriginalKey { get; init; }
public required string Resource { get; init; }
public int MorphX { get; init; }
public int MorphY { get; init; }
public required string[] Fields { get; init; }
public required Joint[] Joints { get; init; }
public required VariantAsset[] Variants { get; init; }
}

View File

@@ -0,0 +1,11 @@
using SlaveEngine.Assets.Primitives;
namespace SlaveEngine.Assets.Models;
public sealed class PathData {
public required string Fill { get; init; }
public required string Stroke { get; init; }
public float StrokeWidth { get; init; }
public bool IsClosed { get; init; }
public required BezierCommand[] Commands { get; init; }
}

View File

@@ -0,0 +1,14 @@
namespace SlaveEngine.Assets.Models;
public sealed class PathGroup {
public required string Name { get; init; }
public bool HasTransform { get; init; }
public float Tx { get; init; }
public float Ty { get; init; }
public float Angle { get; init; }
public float Sx { get; init; }
public float Sy { get; init; }
public float Bx { get; init; }
public float By { get; init; }
public required PathData[] Paths { get; init; }
}

View File

@@ -0,0 +1,7 @@
namespace SlaveEngine.Assets.Models;
public sealed class VariantAsset {
public int X { get; init; }
public int Y { get; init; }
public required PathGroup[] Groups { get; init; }
}

View File

@@ -0,0 +1,6 @@
namespace SlaveEngine.Assets.Primitives;
public readonly struct BezierCommand {
public CommandType Type { get; init; }
public float[] Args { get; init; }
}

View File

@@ -0,0 +1,8 @@
namespace SlaveEngine.Assets.Primitives;
public enum CommandType : byte {
MoveTo = 0,
LineTo = 1,
CubicBezierTo = 2,
Close = 3
}

View File

@@ -0,0 +1,6 @@
namespace SlaveEngine.Assets.Primitives;
public readonly struct Joint {
public float X { get; init; }
public float Y { get; init; }
}

View File

@@ -0,0 +1,120 @@
using System.Text;
using SlaveEngine.Assets.Models;
using SlaveEngine.Assets.Primitives;
namespace SlaveEngine.Assets.Processors;
[AssetProcessor(typeof(PartAsset), managedExtensions: [".spart"])]
public sealed class PartProcessor : AssetProcessor {
public override Asset Process(AssetFileInfo fileInfo, byte[] data) {
using var stream = new MemoryStream(data);
using var reader = new BinaryReader(stream);
var magic = reader.ReadBytes(4);
if (magic[0] != 'S' || magic[1] != 'P' || magic[2] != 'R' || magic[3] != 'T')
throw new InvalidDataException("Invalid .spart file magic");
var version = reader.ReadInt32();
if (version != 1)
throw new InvalidDataException($"Unsupported .spart version: {version}");
var id = ReadString(reader);
var originalKey = ReadString(reader);
var resource = ReadString(reader);
var morphX = reader.ReadInt32();
var morphY = reader.ReadInt32();
var fieldCount = reader.ReadInt32();
var fields = new string[fieldCount];
for (var i = 0; i < fieldCount; i++)
fields[i] = ReadString(reader);
var jointCount = reader.ReadInt32();
var joints = new Joint[jointCount];
for (var i = 0; i < jointCount; i++)
joints[i] = new Joint { X = reader.ReadSingle(), Y = reader.ReadSingle() };
var variantCount = reader.ReadInt32();
var variants = new VariantAsset[variantCount];
for (var v = 0; v < variantCount; v++) {
var vx = reader.ReadInt32();
var vy = reader.ReadInt32();
var groupCount = reader.ReadInt32();
var groups = new PathGroup[groupCount];
for (var g = 0; g < groupCount; g++) {
var groupName = ReadString(reader);
var hasTransform = reader.ReadBoolean();
float tx = 0, ty = 0, angle = 0, sx = 1, sy = 1, bx = 0, by = 0;
if (hasTransform) {
tx = reader.ReadSingle();
ty = reader.ReadSingle();
angle = reader.ReadSingle();
sx = reader.ReadSingle();
sy = reader.ReadSingle();
bx = reader.ReadSingle();
by = reader.ReadSingle();
}
var pathCount = reader.ReadInt32();
var paths = new PathData[pathCount];
for (var p = 0; p < pathCount; p++) {
var fill = ReadString(reader);
var stroke = ReadString(reader);
var strokeWidth = reader.ReadSingle();
var isClosed = reader.ReadBoolean();
var cmdCount = reader.ReadInt32();
var cmds = new BezierCommand[cmdCount];
for (var c = 0; c < cmdCount; c++) {
var cmdType = (CommandType)reader.ReadByte();
var argCount = reader.ReadInt32();
var args = new float[argCount];
for (var a = 0; a < argCount; a++)
args[a] = reader.ReadSingle();
cmds[c] = new BezierCommand { Type = cmdType, Args = args };
}
paths[p] = new PathData {
Fill = fill,
Stroke = stroke,
StrokeWidth = strokeWidth,
IsClosed = isClosed,
Commands = cmds
};
}
groups[g] = new PathGroup {
Name = groupName,
HasTransform = hasTransform,
Tx = tx, Ty = ty, Angle = angle,
Sx = sx, Sy = sy, Bx = bx, By = by,
Paths = paths
};
}
variants[v] = new VariantAsset {
X = vx, Y = vy, Groups = groups
};
}
return new PartAsset {
Guid = Guid.NewGuid(),
FileInfo = fileInfo,
Id = id,
OriginalKey = originalKey,
Resource = resource,
MorphX = morphX,
MorphY = morphY,
Fields = fields,
Joints = joints,
Variants = variants
};
}
private static string ReadString(BinaryReader reader) {
var len = reader.ReadInt32();
var bytes = reader.ReadBytes(len);
return Encoding.UTF8.GetString(bytes);
}
}

View File

@@ -0,0 +1,78 @@
using System.Reflection;
using System.Text;
namespace SlaveEngine.Assets;
public sealed class ResourceManager {
public ResourceManager(string basePath) {
if (!Path.Exists(basePath))
throw new DirectoryNotFoundException($"The base path '{basePath}' does not exist.");
BasePath = basePath;
Instance = this;
}
public static ResourceManager? Instance { get; private set; }
public string BasePath { get; private set; }
private readonly List<AssetProcessor> _assetProcessors = new();
private readonly Dictionary<string, string> _assetIndex = new();
private readonly Dictionary<string, Asset> _assetCache = new();
public void Initialize() {
ScanDirectory(BasePath);
}
private void ScanDirectory(string directory) {
foreach (var file in Directory.GetFiles(directory, "*.spart")) {
try {
var id = ReadAssetIdFromSpart(file);
_assetIndex[id] = file;
} catch {
// skip corrupt files
}
}
}
public T? GetAsset<T>(string id) where T : Asset {
if (_assetCache.TryGetValue(id, out var cached))
return (T)cached;
if (!_assetIndex.TryGetValue(id, out var path))
return null;
var ext = Path.GetExtension(path);
var processor = GetProcessorForExtension(ext);
if (processor == null)
return null;
var fileInfo = new AssetFileInfo {
Filename = Path.GetFileNameWithoutExtension(path),
FilePath = path
};
var asset = processor.Process(fileInfo, File.ReadAllBytes(path));
_assetCache[id] = asset;
return (T)asset;
}
public IReadOnlyCollection<string> GetAssetIds() => _assetIndex.Keys;
public void RegisterProcessor(AssetProcessor processor) {
if (_assetProcessors.All(p => p.GetType() != processor.GetType()))
_assetProcessors.Add(processor);
}
private AssetProcessor? GetProcessorForExtension(string extension) =>
_assetProcessors.FirstOrDefault(p => p.GetType()
.GetCustomAttribute<AssetProcessorAttribute>()?.ManagedExtensions
.Contains(extension) == true);
private static string ReadAssetIdFromSpart(string path) {
using var stream = File.OpenRead(path);
using var reader = new BinaryReader(stream);
reader.ReadBytes(8);
var len = reader.ReadInt32();
return Encoding.UTF8.GetString(reader.ReadBytes(len));
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="YamlDotNet" Version="18.0.0" />
</ItemGroup>
</Project>

View File

@@ -277,15 +277,15 @@ class Program
["id"] = engKey,
["original_key"] = originalKey,
["resource"] = name,
["morph_x"] = difs.CountX,
["morph_y"] = difs.CountY,
["morph_x"] = difs.GetCountX(),
["morph_y"] = difs.GetCountY(),
["variants"] = new JArray(),
["fields"] = new JArray()
};
var variants = (JArray)partEntry["variants"];
for (int x = 0; x < difs.CountX; x++)
for (int x = 0; x < difs.GetCountX(); x++)
{
var dif = difs[x];
for (int y = 0; y < dif.Count; y++)
@@ -305,7 +305,7 @@ class Program
}
}
foreach (string childKey in difs[0][0].Keys)
foreach (string childKey in difs[0][0].pars.Keys)
{
((JArray)partEntry["fields"]).Add(new JObject { ["name"] = childKey });
}
@@ -432,7 +432,7 @@ class Program
static void ExportParsToSvgInner(PartGroup PartGroup, StringBuilder sb, List<string> foundJoints)
{
foreach (string key in PartGroup.Keys)
foreach (string key in PartGroup.pars.Keys)
{
var val = PartGroup[key];
if (val is PartGroup childPars)
@@ -452,13 +452,13 @@ class Program
{
if (!ShapePart.Dra) return;
var bx = ShapePart.BasePoint.X;
var by = ShapePart.BasePoint.Y;
var px = ShapePart.Position.X;
var py = ShapePart.Position.Y;
var angle = ShapePart.Angle;
var sx = ShapePart.Size * ShapePart.SizeX;
var sy = ShapePart.Size * ShapePart.SizeY;
var bx = ShapePart.GetBasePoint().X;
var by = ShapePart.GetBasePoint().Y;
var px = ShapePart.GetPosition().X;
var py = ShapePart.GetPosition().Y;
var angle = ShapePart.GetAngle();
var sx = ShapePart.GetSize() * ShapePart.GetSizeX();
var sy = ShapePart.GetSize() * ShapePart.GetSizeY();
var hasTransform = System.Math.Abs(bx) > 0.001 || System.Math.Abs(by) > 0.001
|| System.Math.Abs(px) > 0.001 || System.Math.Abs(py) > 0.001
@@ -467,22 +467,22 @@ class Program
if (hasTransform)
sb.Append($"<g transform=\"translate({F(px)},{F(py)}) rotate({F(angle)}) scale({F(sx)},{F(sy)}) translate({F(-bx)},{F(-by)})\">");
foreach (var outObj in ShapePart.OP)
foreach (var outObj in ShapePart.GetOP())
{
var points = outObj.ps;
if (points.Count < 2) continue;
var d = BuildSvgPath(points, outObj.Tension, ShapePart.Closed);
var fill = ShapePart.Closed ? "#cccccc" : "none";
var d = BuildSvgPath(points, outObj.Tension, ShapePart.IsClosed);
var fill = ShapePart.IsClosed ? "#cccccc" : "none";
var stroke = outObj.Outline ? "#000000" : "none";
var sw_val = outObj.Outline ? System.Math.Max(ShapePart.PenWidth, 0.001) : 0.0;
var sw_val = outObj.Outline ? System.Math.Max(ShapePart.GetPenWidth(), 0.001) : 0.0;
sb.AppendLine($"<path d=\"{d}\" fill=\"{fill}\" stroke=\"{stroke}\" stroke-width=\"{F(sw_val)}\"/>");
}
if (hasTransform)
sb.Append("</g>");
foreach (var joi in ShapePart.JP)
foreach (var joi in ShapePart.GetJP())
{
foundJoints.Add($"{F(joi.Joint.X)},{F(joi.Joint.Y)}");
}
@@ -579,15 +579,15 @@ class Program
static JObject ExportDifs(VariantGrid VariantGrid)
{
int xCount = VariantGrid.CountX;
int yCount = VariantGrid.CountY;
int xCount = VariantGrid.GetCountX();
int yCount = VariantGrid.GetCountY();
var result = new JObject
{
["Tag"] = VariantGrid.Tag ?? "",
["ValueX"] = VariantGrid.ValueX,
["ValueY"] = VariantGrid.ValueY,
["CountX"] = xCount,
["GetCountX()"] = xCount,
["CountY"] = yCount,
["VariantGrid"] = new JArray()
};
@@ -616,7 +616,7 @@ class Program
["Children"] = new JObject()
};
foreach (string key in PartGroup.Keys)
foreach (string key in PartGroup.pars.Keys)
{
var val = PartGroup[key];
if (val is PartGroup childPars)
@@ -638,18 +638,18 @@ class Program
{
["Tag"] = ShapePart.Tag ?? "",
["Dra"] = ShapePart.Dra,
["PenWidth"] = ShapePart.PenWidth,
["Closed"] = ShapePart.Closed,
["BasePoint"] = ExportVec(ShapePart.BasePoint),
["Position"] = ExportVec(ShapePart.Position),
["Angle"] = ShapePart.Angle,
["Size"] = ShapePart.Size,
["SizeX"] = ShapePart.SizeX,
["SizeY"] = ShapePart.SizeY,
["PenWidth"] = ShapePart.GetPenWidth(),
["Closed"] = ShapePart.IsClosed,
["BasePoint"] = ExportVec(ShapePart.GetBasePoint()),
["Position"] = ExportVec(ShapePart.GetPosition()),
["Angle"] = ShapePart.GetAngle(),
["Size"] = ShapePart.GetSize(),
["SizeX"] = ShapePart.GetSizeX(),
["SizeY"] = ShapePart.GetSizeY(),
["CurveOutline"] = new JArray()
};
foreach (var outObj in ShapePart.OP)
foreach (var outObj in ShapePart.GetOP())
{
var outJ = new JObject
{
@@ -662,10 +662,10 @@ class Program
((JArray)result["CurveOutline"]).Add(outJ);
}
if (ShapePart.JP.Count > 0)
if (ShapePart.GetJP().Count > 0)
{
result["Joints"] = new JArray();
foreach (var joi in ShapePart.JP)
foreach (var joi in ShapePart.GetJP())
((JArray)result["Joints"]).Add(ExportVec(joi.Joint));
}
@@ -676,7 +676,7 @@ class Program
{
var result = ExportPar(ShapePartT);
result["Text"] = ShapePartT.Text ?? "";
result["FontSize"] = ShapePartT.FontSize;
result["FontSize"] = ShapePartT.GetFontSize();
return result;
}

View File

@@ -9,6 +9,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "2DGAMELIB", "2DGAMELIB\2DGA
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlaveMatrix.Extract", "SlaveMatrix.Extract\SlaveMatrix.Extract.csproj", "{DAEAB23A-E099-432E-9C85-39D0853BD78A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlaveEngine.Assets", "SlaveEngine.Assets\SlaveEngine.Assets.csproj", "{11B3B1C3-B393-4A4D-AEC5-B26C86F6C0C6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlaveEngine.AssetBuilder", "SlaveEngine.AssetBuilder\SlaveEngine.AssetBuilder.csproj", "{DE1800EA-9AB1-4558-BB60-40BAA505F475}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
@@ -55,6 +59,30 @@ Global
{DAEAB23A-E099-432E-9C85-39D0853BD78A}.Release|x86.Build.0 = Release|Any CPU
{DAEAB23A-E099-432E-9C85-39D0853BD78A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DAEAB23A-E099-432E-9C85-39D0853BD78A}.Release|Any CPU.Build.0 = Release|Any CPU
{11B3B1C3-B393-4A4D-AEC5-B26C86F6C0C6}.Debug|x64.ActiveCfg = Debug|Any CPU
{11B3B1C3-B393-4A4D-AEC5-B26C86F6C0C6}.Debug|x64.Build.0 = Debug|Any CPU
{11B3B1C3-B393-4A4D-AEC5-B26C86F6C0C6}.Debug|x86.ActiveCfg = Debug|Any CPU
{11B3B1C3-B393-4A4D-AEC5-B26C86F6C0C6}.Debug|x86.Build.0 = Debug|Any CPU
{11B3B1C3-B393-4A4D-AEC5-B26C86F6C0C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{11B3B1C3-B393-4A4D-AEC5-B26C86F6C0C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11B3B1C3-B393-4A4D-AEC5-B26C86F6C0C6}.Release|x64.ActiveCfg = Release|Any CPU
{11B3B1C3-B393-4A4D-AEC5-B26C86F6C0C6}.Release|x64.Build.0 = Release|Any CPU
{11B3B1C3-B393-4A4D-AEC5-B26C86F6C0C6}.Release|x86.ActiveCfg = Release|Any CPU
{11B3B1C3-B393-4A4D-AEC5-B26C86F6C0C6}.Release|x86.Build.0 = Release|Any CPU
{11B3B1C3-B393-4A4D-AEC5-B26C86F6C0C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11B3B1C3-B393-4A4D-AEC5-B26C86F6C0C6}.Release|Any CPU.Build.0 = Release|Any CPU
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Debug|x64.ActiveCfg = Debug|Any CPU
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Debug|x64.Build.0 = Debug|Any CPU
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Debug|x86.ActiveCfg = Debug|Any CPU
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Debug|x86.Build.0 = Debug|Any CPU
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Release|x64.ActiveCfg = Release|Any CPU
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Release|x64.Build.0 = Release|Any CPU
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Release|x86.ActiveCfg = Release|Any CPU
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Release|x86.Build.0 = Release|Any CPU
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE