Asset compiler first iteration:
This commit is contained in:
187
SlaveEngine.AssetBuilder/Compiler.cs
Normal file
187
SlaveEngine.AssetBuilder/Compiler.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
35
SlaveEngine.AssetBuilder/Models/YamlModels.cs
Normal file
35
SlaveEngine.AssetBuilder/Models/YamlModels.cs
Normal 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; } = "";
|
||||
}
|
||||
32
SlaveEngine.AssetBuilder/Program.cs
Normal file
32
SlaveEngine.AssetBuilder/Program.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
20
SlaveEngine.AssetBuilder/SlaveEngine.AssetBuilder.csproj
Normal file
20
SlaveEngine.AssetBuilder/SlaveEngine.AssetBuilder.csproj
Normal 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>
|
||||
73
SlaveEngine.AssetBuilder/SparWriter.cs
Normal file
73
SlaveEngine.AssetBuilder/SparWriter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
192
SlaveEngine.AssetBuilder/SvgParser.cs
Normal file
192
SlaveEngine.AssetBuilder/SvgParser.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace SlaveEngine.Assets;
|
||||
|
||||
public class VectorAsset : Asset {
|
||||
//TODO: insert the data from an SVG here.
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace SlaveEngine.Assets.Processors;
|
||||
|
||||
[AssetProcessor(typeof(VectorAsset), managedExtensions: [".svg"])]
|
||||
public sealed class SVGProcessor : AssetProcessor{
|
||||
|
||||
public override Asset Process(AssetFileInfo fileInfo, byte[] data)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -1,61 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace SlaveEngine.Assets;
|
||||
|
||||
public sealed class ResourceManager {
|
||||
|
||||
public ResourceManager(string basePath)
|
||||
{
|
||||
public ResourceManager(string basePath) {
|
||||
if (!Path.Exists(basePath))
|
||||
throw new DirectoryNotFoundException($"The base path '{basePath}' does not exist.");
|
||||
BasePath = basePath;
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
private AssetProcessor? GetProcessorForExtension(string extension) =>
|
||||
_assetProcessors.FirstOrDefault(p => (bool)p.GetType()
|
||||
.GetCustomAttribute<AssetProcessorAttribute>()?.ManagedExtensions
|
||||
.Contains(extension)
|
||||
);
|
||||
|
||||
public static ResourceManager? Instance { get; private set; }
|
||||
|
||||
private List<AssetProcessor> _assetProcessors = new();
|
||||
|
||||
public string BasePath { get; private set; }
|
||||
|
||||
public void Initialize(string basePath)
|
||||
{
|
||||
ScanDirectory(basePath);
|
||||
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)
|
||||
{
|
||||
var files = Directory.GetFiles(directory);
|
||||
foreach (var file in files)
|
||||
{
|
||||
var ext = Path.GetExtension(file);
|
||||
// check if we can process this extension
|
||||
var processor = GetProcessorForExtension(ext);
|
||||
if (processor == null) continue;
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var name = Path.GetFileNameWithoutExtension(file);
|
||||
AssetFileInfo fileInfo = new()
|
||||
{
|
||||
Filename = name,
|
||||
FilePath = file
|
||||
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
|
||||
};
|
||||
|
||||
processor.Process(fileInfo, File.ReadAllBytes(file));
|
||||
}
|
||||
var asset = processor.Process(fileInfo, File.ReadAllBytes(path));
|
||||
_assetCache[id] = asset;
|
||||
return (T)asset;
|
||||
}
|
||||
|
||||
public void RegisterProcessor(AssetProcessor processor)
|
||||
{
|
||||
//make sure the processor for this type isn't already in:
|
||||
var ap = _assetProcessors.FirstOrDefault(p => p.GetType() == processor.GetType());
|
||||
if (ap == null) _assetProcessors.Add(processor);
|
||||
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));
|
||||
}
|
||||
}
|
||||
14
Solution.sln
14
Solution.sln
@@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlaveMatrix.Extract", "Slav
|
||||
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
|
||||
@@ -69,6 +71,18 @@ Global
|
||||
{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
|
||||
|
||||
Reference in New Issue
Block a user