diff --git a/Content.MapRenderer/CommandLineArguments.cs b/Content.MapRenderer/CommandLineArguments.cs new file mode 100644 index 0000000000..6cfa3c71ad --- /dev/null +++ b/Content.MapRenderer/CommandLineArguments.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Content.MapRenderer.Extensions; + +namespace Content.MapRenderer; + +public sealed class CommandLineArguments +{ + public List Maps { get; set; } = new(); + public OutputFormat Format { get; set; } = OutputFormat.png; + public bool ExportViewerJson { get; set; } = false; + + public string OutputPath { get; set; } = DirectoryExtensions.MapImages().FullName; + + public static bool TryParse(IReadOnlyList args, [NotNullWhen(true)] out CommandLineArguments? parsed) + { + parsed = new CommandLineArguments(); + + if (args.Count == 0) + { + PrintHelp(); + return false; + } + + using var enumerator = args.GetEnumerator(); + + while (enumerator.MoveNext()) + { + var argument = enumerator.Current; + switch (argument) + { + case "--format": + enumerator.MoveNext(); + + if (!Enum.TryParse(enumerator.Current, out var format)) + { + Console.WriteLine("Invalid format specified for option: {0}", argument); + parsed = null; + return false; + } + + parsed.Format = format; + break; + + case "--viewer": + parsed.ExportViewerJson = true; + break; + + case "-o": + case "--output": + enumerator.MoveNext(); + parsed.OutputPath = enumerator.Current; + break; + + case "-h": + case "--help": + PrintHelp(); + return false; + + default: + parsed.Maps.Add(argument); + break; + } + } + + return true; + } + + public static void PrintHelp() + { + Console.WriteLine(@"Content.MapRenderer [map names] +Options: + --format + Specifies the format the map images will be exported as. + Defaults to: png + --viewer + Causes the map renderer to create the map.json files required for use with the map viewer. + Also puts the maps in the required directory structure. + -o / --output + Changes the path the rendered maps will get saved to. + Defaults to Resources/MapImages + -h / --help + Displays this help text"); + } +} + +public class CommandLineArgumentException : Exception +{ + public CommandLineArgumentException(string? message) : base(message) + { + } +} + +[SuppressMessage("ReSharper", "InconsistentNaming")] +public enum OutputFormat +{ + png, + webp +} diff --git a/Content.MapRenderer/MapViewerData.cs b/Content.MapRenderer/MapViewerData.cs new file mode 100644 index 0000000000..6d94b16058 --- /dev/null +++ b/Content.MapRenderer/MapViewerData.cs @@ -0,0 +1,150 @@ +using System.Collections.Generic; +using Robust.Shared.Maths; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace Content.MapRenderer; + +public sealed class MapViewerData +{ + public string Id { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public List Grids { get; set; } = new(); + public string? Attributions { get; set; } + public List ParallaxLayers { get; set; } = new(); +} + +public sealed class GridLayer +{ + public string GridId { get; set; } = string.Empty; + public Position Offset { get; set; } + public bool Tiled { get; set; } = false; + public string Url { get; set; } + public Extent Extent { get; set; } + + public GridLayer(RenderedGridImage gridImage, string url) + { + //Get the internal _uid as string + if (gridImage.GridUid.HasValue) + GridId = gridImage.GridUid.Value.GetHashCode().ToString(); + + Offset = new Position(gridImage.Offset); + Extent = new Extent(gridImage.Image.Width, gridImage.Image.Height); + Url = url; + } +} + +public sealed class LayerGroup +{ + public Position Scale { get; set; } = Position.One(); + public Position Offset { get; set; } = Position.Zero(); + public bool Static { get; set; } = false; + public float? MinScale { get; set; } + public GroupSource Source { get; set; } = new(); + public List Layers { get; set; } = new(); + + public static LayerGroup DefaultParallax() + { + return new LayerGroup() + { + Scale = new Position(0.1f, 0.1f), + Source = new GroupSource() + { + Url = "https://i.imgur.com/3YO8KRd.png", + Extent = new Extent(6000, 4000) + }, + Layers = new List() + { + new Layer() + { + Url = "https://i.imgur.com/IannmmK.png" + }, + new Layer() + { + Url = "https://i.imgur.com/T3W6JsE.png", + Composition = "lighter", + ParallaxScale = new Position(0.2f, 0.2f) + }, + new Layer() + { + Url = "https://i.imgur.com/T3W6JsE.png", + Composition = "lighter", + ParallaxScale = new Position(0.3f, 0.3f) + } + } + }; + } +} + +public sealed class GroupSource +{ + public string Url { get; set; } = string.Empty; + public Extent Extent { get; set; } = new(); +} + +public sealed class Layer +{ + public string Url { get; set; } = string.Empty; + public string Composition { get; set; } = "source-over"; + public Position ParallaxScale { get; set; } = new(0.1f, 0.1f); +} + +public readonly struct Extent +{ + public readonly float X1; + public readonly float Y1; + public readonly float X2; + public readonly float Y2; + + public Extent() + { + X1 = 0; + Y1 = 0; + X2 = 0; + Y2 = 0; + } + + public Extent(float x2, float y2) + { + X1 = 0; + Y1 = 0; + X2 = x2; + Y2 = y2; + } + + public Extent(float x1, float y1, float x2, float y2) + { + X1 = x1; + Y1 = y1; + X2 = x2; + Y2 = y2; + } +} + +public readonly struct Position +{ + public readonly float X; + public readonly float Y; + + public Position(float x, float y) + { + X = x; + Y = y; + } + + public Position(Vector2 vector2) + { + X = vector2.X; + Y = vector2.Y; + } + + public static Position Zero() + { + return new Position(0, 0); + } + + public static Position One() + { + return new Position(0, 0); + } +} diff --git a/Content.MapRenderer/Painters/MapPainter.cs b/Content.MapRenderer/Painters/MapPainter.cs index bc5a0abd16..538571e037 100644 --- a/Content.MapRenderer/Painters/MapPainter.cs +++ b/Content.MapRenderer/Painters/MapPainter.cs @@ -19,7 +19,7 @@ namespace Content.MapRenderer.Painters { public sealed class MapPainter { - public async IAsyncEnumerable Paint(string map) + public async IAsyncEnumerable> Paint(string map) { var stopwatch = new Stopwatch(); stopwatch.Start(); @@ -80,7 +80,7 @@ namespace Content.MapRenderer.Painters // Skip empty grids if (grid.LocalAABB.IsEmpty()) { - Console.WriteLine($"Warning: Grid {grid.Index} was empty. Skipping image rendering."); + Console.WriteLine($"Warning: Grid {grid.GridEntityId} was empty. Skipping image rendering."); continue; } @@ -107,7 +107,11 @@ namespace Content.MapRenderer.Painters gridCanvas.Mutate(e => e.Flip(FlipMode.Vertical)); }); - yield return gridCanvas; + var renderedImage = new RenderedGridImage(gridCanvas); + renderedImage.GridUid = grid.GridEntityId; + renderedImage.Offset = grid.WorldPosition; + + yield return renderedImage; } // We don't care if it fails as we have already saved the images. diff --git a/Content.MapRenderer/Program.cs b/Content.MapRenderer/Program.cs index 131e0e31f9..bd0be3663c 100644 --- a/Content.MapRenderer/Program.cs +++ b/Content.MapRenderer/Program.cs @@ -3,10 +3,13 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Content.MapRenderer.Extensions; using Content.MapRenderer.Painters; +using Newtonsoft.Json; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Webp; namespace Content.MapRenderer { @@ -24,82 +27,80 @@ namespace Content.MapRenderer Console.WriteLine("Didn't specify any maps to paint! Provide map names (as map prototype names)."); } - // var created = Environment.GetEnvironmentVariable(MapsAddedEnvKey); - // var modified = Environment.GetEnvironmentVariable(MapsModifiedEnvKey); - // - // var yamlStream = new YamlStream(); - // - // if (created != null) - // { - // yamlStream.Load(new StringReader(created)); - // } - // - // if (modified != null) - // { - // yamlStream.Load(new StringReader(modified)); - // } - // - // var files = new YamlSequenceNode(); - // - // foreach (var doc in yamlStream.Documents) - // { - // var filesModified = (YamlSequenceNode) doc.RootNode; - // - // foreach (var node in filesModified) - // { - // files.Add(node); - // } - // } + if (!CommandLineArguments.TryParse(args, out var arguments)) + return; - // var maps = new List(); - - // foreach (var node in files) - // { - // var fileName = node.AsString(); - // - // if (!fileName.StartsWith("Resources/Maps/") || - // !fileName.EndsWith("yml")) - // { - // continue; - // } - // - // maps.Add(fileName); - // } - - await Run(new List(args)); + await Run(arguments); } - private static async Task Run(List maps) + private static async Task Run(CommandLineArguments arguments) { - Console.WriteLine($"Creating images for {maps.Count} maps"); + + Console.WriteLine($"Creating images for {arguments.Maps.Count} maps"); var mapNames = new List(); - foreach (var map in maps) + foreach (var map in arguments.Maps) { Console.WriteLine($"Painting map {map}"); - int i = 0; - await foreach (var grid in MapPainter.Paint(map)) + var mapViewerData = new MapViewerData() { - var directory = DirectoryExtensions.MapImages().FullName; + Id = map, + Name = Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(map) + }; + + mapViewerData.ParallaxLayers.Add(LayerGroup.DefaultParallax()); + var directory = Path.Combine(arguments.OutputPath, map); + Directory.CreateDirectory(directory); + + int i = 0; + await foreach (var renderedGrid in MapPainter.Paint(map)) + { + var grid = renderedGrid.Image; Directory.CreateDirectory(directory); var fileName = Path.GetFileNameWithoutExtension(map); - var savePath = $"{directory}{Path.DirectorySeparatorChar}{fileName}-{i}.png"; + var savePath = $"{directory}{Path.DirectorySeparatorChar}{fileName}-{i}.{arguments.Format.ToString()}"; Console.WriteLine($"Writing grid of size {grid.Width}x{grid.Height} to {savePath}"); - await grid.SaveAsPngAsync(savePath); + switch (arguments.Format) + { + case OutputFormat.webp: + var encoder = new WebpEncoder + { + Method = WebpEncodingMethod.BestQuality, + FileFormat = WebpFileFormatType.Lossless, + TransparentColorMode = WebpTransparentColorMode.Preserve + }; + + await grid.SaveAsync(savePath, encoder); + break; + + default: + case OutputFormat.png: + await grid.SaveAsPngAsync(savePath); + break; + } + grid.Dispose(); + mapViewerData.Grids.Add(new GridLayer(renderedGrid, Path.Combine(map, Path.GetFileName(savePath)))); + mapNames.Add(fileName); i++; } + + if (arguments.ExportViewerJson) + { + var json = JsonConvert.SerializeObject(mapViewerData); + await File.WriteAllTextAsync(Path.Combine(arguments.OutputPath, map, "map.json"), json); + } } var mapNamesString = $"[{string.Join(',', mapNames.Select(s => $"\"{s}\""))}]"; Console.WriteLine($@"::set-output name=map_names::{mapNamesString}"); - Console.WriteLine($"Created {maps.Count} map images."); + Console.WriteLine($"Created {arguments.Maps.Count} map images."); } } } diff --git a/Content.MapRenderer/RenderedGridImage.cs b/Content.MapRenderer/RenderedGridImage.cs new file mode 100644 index 0000000000..8d08e8f3c1 --- /dev/null +++ b/Content.MapRenderer/RenderedGridImage.cs @@ -0,0 +1,18 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.Maths; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace Content.MapRenderer; + +public sealed class RenderedGridImage where T : unmanaged, IPixel +{ + public Image Image; + public Vector2 Offset { get; set; } = Vector2.Zero; + public EntityUid? GridUid { get; set; } + + public RenderedGridImage(Image image) + { + Image = image; + } +}