diff --git a/Content.IntegrationTests/ExternalTestContext.cs b/Content.IntegrationTests/ExternalTestContext.cs new file mode 100644 index 0000000000..e23b2ee636 --- /dev/null +++ b/Content.IntegrationTests/ExternalTestContext.cs @@ -0,0 +1,12 @@ +using System.IO; + +namespace Content.IntegrationTests; + +/// +/// Generic implementation of for usage outside of actual tests. +/// +public sealed class ExternalTestContext(string name, TextWriter writer) : ITestContextLike +{ + public string FullName => name; + public TextWriter Out => writer; +} diff --git a/Content.IntegrationTests/ITestContextLike.cs b/Content.IntegrationTests/ITestContextLike.cs new file mode 100644 index 0000000000..47b6e08529 --- /dev/null +++ b/Content.IntegrationTests/ITestContextLike.cs @@ -0,0 +1,13 @@ +using System.IO; + +namespace Content.IntegrationTests; + +/// +/// Something that looks like a , for passing to integration tests. +/// +public interface ITestContextLike +{ + string FullName { get; } + TextWriter Out { get; } +} + diff --git a/Content.IntegrationTests/NUnitTestContextWrap.cs b/Content.IntegrationTests/NUnitTestContextWrap.cs new file mode 100644 index 0000000000..849c1b0910 --- /dev/null +++ b/Content.IntegrationTests/NUnitTestContextWrap.cs @@ -0,0 +1,12 @@ +using System.IO; + +namespace Content.IntegrationTests; + +/// +/// Canonical implementation of for usage in actual NUnit tests. +/// +public sealed class NUnitTestContextWrap(TestContext context, TextWriter writer) : ITestContextLike +{ + public string FullName => context.Test.FullName; + public TextWriter Out => writer; +} diff --git a/Content.IntegrationTests/Pair/TestPair.Recycle.cs b/Content.IntegrationTests/Pair/TestPair.Recycle.cs index 89a9eb6463..694d6cfa64 100644 --- a/Content.IntegrationTests/Pair/TestPair.Recycle.cs +++ b/Content.IntegrationTests/Pair/TestPair.Recycle.cs @@ -13,6 +13,7 @@ using Robust.Server.Player; using Robust.Shared.Exceptions; using Robust.Shared.GameObjects; using Robust.Shared.Network; +using Robust.Shared.Utility; namespace Content.IntegrationTests.Pair; @@ -84,6 +85,7 @@ public sealed partial class TestPair : IAsyncDisposable var returnTime = Watch.Elapsed; await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: PoolManager took {returnTime.TotalMilliseconds} ms to put pair {Id} back into the pool"); + State = PairState.Ready; } private async Task ResetModifiedPreferences() @@ -104,7 +106,7 @@ public sealed partial class TestPair : IAsyncDisposable await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Return of pair {Id} started"); State = PairState.CleanDisposed; await OnCleanDispose(); - State = PairState.Ready; + DebugTools.Assert(State is PairState.Dead or PairState.Ready); PoolManager.NoCheckReturn(this); ClearContext(); } diff --git a/Content.IntegrationTests/PoolManager.cs b/Content.IntegrationTests/PoolManager.cs index c7b8dcaee9..64aac16751 100644 --- a/Content.IntegrationTests/PoolManager.cs +++ b/Content.IntegrationTests/PoolManager.cs @@ -182,24 +182,29 @@ public static partial class PoolManager /// /// See /// - public static async Task GetServerClient(PoolSettings? poolSettings = null) + public static async Task GetServerClient( + PoolSettings? poolSettings = null, + ITestContextLike? testContext = null) { - return await GetServerClientPair(poolSettings ?? new PoolSettings()); + return await GetServerClientPair( + poolSettings ?? new PoolSettings(), + testContext ?? new NUnitTestContextWrap(TestContext.CurrentContext, TestContext.Out)); } - private static string GetDefaultTestName(TestContext testContext) + private static string GetDefaultTestName(ITestContextLike testContext) { - return testContext.Test.FullName.Replace("Content.IntegrationTests.Tests.", ""); + return testContext.FullName.Replace("Content.IntegrationTests.Tests.", ""); } - private static async Task GetServerClientPair(PoolSettings poolSettings) + private static async Task GetServerClientPair( + PoolSettings poolSettings, + ITestContextLike testContext) { if (!_initialized) throw new InvalidOperationException($"Pool manager has not been initialized"); // Trust issues with the AsyncLocal that backs this. - var testContext = TestContext.CurrentContext; - var testOut = TestContext.Out; + var testOut = testContext.Out; DieIfPoolFailure(); var currentTestName = poolSettings.TestName ?? GetDefaultTestName(testContext); diff --git a/Content.MapRenderer/CommandLineArguments.cs b/Content.MapRenderer/CommandLineArguments.cs index f75b671dcb..a4f3c83bf2 100644 --- a/Content.MapRenderer/CommandLineArguments.cs +++ b/Content.MapRenderer/CommandLineArguments.cs @@ -13,6 +13,7 @@ public sealed class CommandLineArguments public string OutputPath { get; set; } = DirectoryExtensions.MapImages().FullName; public bool ArgumentsAreFileNames { get; set; } = false; public bool ShowMarkers { get; set; } = false; + public bool OutputParallax { get; set; } = false; public static bool TryParse(IReadOnlyList args, [NotNullWhen(true)] out CommandLineArguments? parsed) { @@ -70,7 +71,17 @@ public sealed class CommandLineArguments PrintHelp(); return false; + case "--parallax": + parsed.OutputParallax = true; + break; + default: + if (argument.StartsWith('-')) + { + Console.WriteLine($"Unknown argument: {argument}"); + return false; + } + parsed.Maps.Add(argument); break; } @@ -95,7 +106,6 @@ Options: 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 @@ -104,6 +114,8 @@ Options: Example: Content.MapRenderer -f /Maps/box.yml /Maps/bagel.yml -m / --markers Show hidden markers on map render. Defaults to false. + --parallax + Output images and data used for map viewer parallax. -h / --help Displays this help text"); } diff --git a/Content.MapRenderer/MapViewerData.cs b/Content.MapRenderer/MapViewerData.cs index b7b720e004..b58d0c664b 100644 --- a/Content.MapRenderer/MapViewerData.cs +++ b/Content.MapRenderer/MapViewerData.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using System.Numerics; -using Robust.Shared.Maths; +using System.Text.Json.Serialization; +using Robust.Shared.ContentPack; +using Robust.Shared.Utility; using SixLabors.ImageSharp.PixelFormats; namespace Content.MapRenderer; @@ -43,31 +45,31 @@ public sealed class LayerGroup public GroupSource Source { get; set; } = new(); public List Layers { get; set; } = new(); - public static LayerGroup DefaultParallax() + public static LayerGroup DefaultParallax(IResourceManager resourceManager, ParallaxOutput output) { 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) + Url = output.ReferenceResourceFile(resourceManager, new ResPath("/Textures/Parallaxes/layer1.png")), + Extent = new Extent(6000, 4000), }, Layers = new List { new() { - Url = "https://i.imgur.com/IannmmK.png" + Url = output.ReferenceResourceFile(resourceManager, new ResPath("/Textures/Parallaxes/layer1.png")), }, new() { - Url = "https://i.imgur.com/T3W6JsE.png", + Url = output.ReferenceResourceFile(resourceManager, new ResPath("/Textures/Parallaxes/layer2.png")), Composition = "lighter", ParallaxScale = new Position(0.2f, 0.2f) }, new() { - Url = "https://i.imgur.com/T3W6JsE.png", + Url = output.ReferenceResourceFile(resourceManager, new ResPath("/Textures/Parallaxes/layer3.png")), Composition = "lighter", ParallaxScale = new Position(0.3f, 0.3f) } @@ -91,9 +93,13 @@ public sealed class Layer public readonly struct Extent { + [JsonInclude] public readonly float X1; + [JsonInclude] public readonly float Y1; + [JsonInclude] public readonly float X2; + [JsonInclude] public readonly float Y2; public Extent() @@ -123,7 +129,9 @@ public readonly struct Extent public readonly struct Position { + [JsonInclude] public readonly float X; + [JsonInclude] public readonly float Y; public Position(float x, float y) diff --git a/Content.MapRenderer/Painters/MapPainter.cs b/Content.MapRenderer/Painters/MapPainter.cs index e861227bcc..991fa74fe1 100644 --- a/Content.MapRenderer/Painters/MapPainter.cs +++ b/Content.MapRenderer/Painters/MapPainter.cs @@ -1,149 +1,158 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Numerics; using System.IO; using System.Threading.Tasks; +using Content.Client.Markers; using Content.IntegrationTests; +using Content.IntegrationTests.Pair; using Content.Server.GameTicking; using Robust.Client.GameObjects; using Robust.Server.GameObjects; using Robust.Server.Player; +using Robust.Shared.ContentPack; using Robust.Shared.EntitySerialization; using Robust.Shared.EntitySerialization.Systems; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Map.Components; -using Robust.Shared.Map.Events; using Robust.Shared.Maths; using Robust.Shared.Timing; -using Robust.Shared.Utility; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; namespace Content.MapRenderer.Painters { - public sealed class MapPainter + public sealed class MapPainter : IAsyncDisposable { - public static async IAsyncEnumerable> Paint(string map, - bool mapIsFilename = false, - bool showMarkers = false) - { - var stopwatch = new Stopwatch(); - stopwatch.Start(); + private readonly RenderMap _map; + private readonly ITestContextLike _testContextLike; - await using var pair = await PoolManager.GetServerClient(new PoolSettings + private TestPair? _pair; + private Entity[] _grids = []; + + public MapPainter(RenderMap map, ITestContextLike testContextLike) + { + _map = map; + _testContextLike = testContextLike; + } + + public async Task Initialize() + { + var stopwatch = RStopwatch.StartNew(); + + var poolSettings = new PoolSettings { DummyTicker = false, Connected = true, + Destructive = true, Fresh = true, // Seriously whoever made MapPainter use GameMapPrototype I wish you step on a lego one time. - Map = mapIsFilename ? "Empty" : map, - }); - - var server = pair.Server; - var client = pair.Client; + Map = _map is RenderMapPrototype prototype ? prototype.Prototype : PoolManager.TestMap, + }; + _pair = await PoolManager.GetServerClient(poolSettings, _testContextLike); Console.WriteLine($"Loaded client and server in {(int)stopwatch.Elapsed.TotalMilliseconds} ms"); - stopwatch.Restart(); - - var cEntityManager = client.ResolveDependency(); - var cPlayerManager = client.ResolveDependency(); - - await client.WaitPost(() => + if (_map is RenderMapFile mapFile) { - if (cEntityManager.TryGetComponent(cPlayerManager.LocalEntity, out SpriteComponent? sprite)) + using var stream = File.OpenRead(mapFile.FileName); + + await _pair.Server.WaitPost(() => { - cEntityManager.System().SetVisible((cPlayerManager.LocalEntity.Value, sprite), false); + var loadOptions = new MapLoadOptions + { + // Accept loading both maps and grids without caring about what the input file truly is. + DeserializationOptions = + { + LogOrphanedGrids = false, + }, + }; + + if (!_pair.Server.System().TryLoadGeneric(stream, mapFile.FileName, out var loadResult, loadOptions)) + throw new IOException($"File {mapFile.FileName} could not be read"); + + _grids = loadResult.Grids.ToArray(); + }); + } + } + + public async Task SetupView(bool showMarkers) + { + if (_pair == null) + throw new InvalidOperationException("Instance not initialized!"); + + await _pair.Client.WaitPost(() => + { + if (_pair.Client.EntMan.TryGetComponent(_pair.Client.PlayerMan.LocalEntity, out SpriteComponent? sprite)) + { + _pair.Client.System() + .SetVisible((_pair.Client.PlayerMan.LocalEntity.Value, sprite), false); } }); if (showMarkers) - await pair.WaitClientCommand("showmarkers"); + { + await _pair.Client.WaitPost(() => + { + _pair.Client.System().MarkersVisible = true; + }); + } + } + + public async Task GenerateMapViewerData(ParallaxOutput? parallaxOutput) + { + if (_pair == null) + throw new InvalidOperationException("Instance not initialized!"); + + var mapShort = _map.ShortName; + + string fullName; + if (_map is RenderMapPrototype prototype) + { + fullName = _pair.Server.ProtoMan.Index(prototype.Prototype).MapName; + } + else + { + fullName = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(mapShort); + } + + var mapViewerData = new MapViewerData + { + Id = mapShort, + Name = fullName, + }; + + if (parallaxOutput != null) + { + await _pair.Client.WaitPost(() => + { + var res = _pair.Client.InstanceDependencyCollection.Resolve(); + mapViewerData.ParallaxLayers.Add(LayerGroup.DefaultParallax(res, parallaxOutput)); + }); + } + + return mapViewerData; + } + + public async IAsyncEnumerable> Paint() + { + if (_pair == null) + throw new InvalidOperationException("Instance not initialized!"); + + var client = _pair.Client; + var server = _pair.Server; var sEntityManager = server.ResolveDependency(); var sPlayerManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); - var mapLoader = entityManager.System(); var mapSys = entityManager.System(); - var deps = server.ResolveDependency().DependencyCollection; - Entity[] grids = []; - - if (mapIsFilename) - { - var resPath = new ResPath(map); - - if (!mapLoader.TryReadFile(resPath, out var data)) - throw new IOException($"File {map} could not be read"); - - var ev = new BeforeEntityReadEvent(); - server.EntMan.EventBus.RaiseEvent(EventSource.Local, ev); - - var deserializer = new EntityDeserializer(deps, - data, - DeserializationOptions.Default, - ev.RenamedPrototypes, - ev.DeletedPrototypes); - - if (!deserializer.TryProcessData()) - { - throw new IOException($"Failed to process entity data in {map}"); - } - - if (deserializer.Result.Category == FileCategory.Unknown && deserializer.Result.Version < 7) - { - var mapCount = 0; - var gridCount = 0; - foreach (var (entId, ent) in deserializer.YamlEntities) - { - if (ent.Components != null && ent.Components.ContainsKey("MapGrid")) - { - gridCount++; - } - if (ent.Components != null && ent.Components.ContainsKey("Map")) - { - mapCount++; - } - } - if (mapCount == 1) - deserializer.Result.Category = FileCategory.Map; - else if (mapCount == 0 && gridCount == 1) - deserializer.Result.Category = FileCategory.Grid; - } - - switch (deserializer.Result.Category) - { - case FileCategory.Map: - await server.WaitPost(() => - { - if (mapLoader.TryLoadMap(resPath, out _, out var loadedGrids)) - { - grids = loadedGrids.ToArray(); - } - }); - break; - - case FileCategory.Grid: - await server.WaitPost(() => - { - if (mapLoader.TryLoadGrid(resPath, out _, out var loadedGrids)) - { - grids = [(Entity)loadedGrids]; - } - }); - break; - - default: - throw new IOException($"Unknown category {deserializer.Result.Category}"); - - } - } - - await pair.RunTicksSync(10); + await _pair.RunTicksSync(10); await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); var sMapManager = server.ResolveDependency(); @@ -162,23 +171,23 @@ namespace Content.MapRenderer.Painters sEntityManager.DeleteEntity(playerEntity.Value); } - if (!mapIsFilename) + if (_map is RenderMapPrototype) { var mapId = sEntityManager.System().DefaultMap; - grids = sMapManager.GetAllGrids(mapId).ToArray(); + _grids = sMapManager.GetAllGrids(mapId).ToArray(); } - foreach (var (uid, _) in grids) + foreach (var (uid, _) in _grids) { var gridXform = xformQuery.GetComponent(uid); xformSystem.SetWorldRotation(gridXform, Angle.Zero); } }); - await pair.RunTicksSync(10); + await _pair.RunTicksSync(10); await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); - foreach (var (uid, grid) in grids) + foreach (var (uid, grid) in _grids) { var tiles = mapSys.GetAllTiles(uid, grid).ToList(); if (tiles.Count == 0) @@ -219,16 +228,20 @@ namespace Content.MapRenderer.Painters yield return renderedImage; } + } - // We don't care if it fails as we have already saved the images. - try - { - await pair.CleanReturnAsync(); - } - catch - { - // ignored - } + public async Task CleanReturnAsync() + { + if (_pair == null) + throw new InvalidOperationException("Instance not initialized!"); + + await _pair.CleanReturnAsync(); + } + + public async ValueTask DisposeAsync() + { + if (_pair != null) + await _pair.DisposeAsync(); } } } diff --git a/Content.MapRenderer/ParallaxOutput.cs b/Content.MapRenderer/ParallaxOutput.cs new file mode 100644 index 0000000000..bedbb1dc53 --- /dev/null +++ b/Content.MapRenderer/ParallaxOutput.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.IO; +using Robust.Shared.ContentPack; +using Robust.Shared.Utility; + +namespace Content.MapRenderer; + +/// +/// Helper class for collecting the files used for parallax output +/// +public sealed class ParallaxOutput +{ + public const string OutputDirectory = "_parallax"; + + public readonly HashSet FilesToCopy = []; + + private readonly string _outputPath; + + /// + /// Helper class for collecting the files used for parallax output + /// + public ParallaxOutput(string outputPath) + { + _outputPath = outputPath; + Directory.CreateDirectory(Path.Combine(_outputPath, OutputDirectory)); + } + + public string ReferenceResourceFile(IResourceManager resourceManager, ResPath path) + { + var fileName = Path.Combine(OutputDirectory, path.Filename); + if (FilesToCopy.Add(path)) + { + using var file = resourceManager.ContentFileRead(path); + using var target = File.Create(Path.Combine(_outputPath, fileName)); + + file.CopyTo(target); + } + + return fileName; + } +} diff --git a/Content.MapRenderer/Program.cs b/Content.MapRenderer/Program.cs index 115d83e65e..9d7843bcd0 100644 --- a/Content.MapRenderer/Program.cs +++ b/Content.MapRenderer/Program.cs @@ -3,12 +3,11 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading; +using System.Text.Json; using System.Threading.Tasks; using Content.IntegrationTests; using Content.MapRenderer.Painters; using Content.Server.Maps; -using Newtonsoft.Json; using Robust.Shared.Prototypes; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Webp; @@ -21,20 +20,19 @@ namespace Content.MapRenderer private static readonly Func ChosenMapIdNotIntMessage = id => $"The chosen id is not a valid integer: {id}"; private static readonly Func NoMapFoundWithIdMessage = id => $"No map found with chosen id: {id}"; - private static readonly MapPainter MapPainter = new(); - internal static async Task Main(string[] args) { - if (!CommandLineArguments.TryParse(args, out var arguments)) return; + var testContext = new ExternalTestContext("Content.MapRenderer", Console.Out); + PoolManager.Startup(); if (arguments.Maps.Count == 0) { Console.WriteLine("Didn't specify any maps to paint! Loading the map list..."); - await using var pair = await PoolManager.GetServerClient(); + await using var pair = await PoolManager.GetServerClient(testContext: testContext); var mapIds = pair.Server .ResolveDependency() .EnumeratePrototypes() @@ -104,45 +102,118 @@ namespace Content.MapRenderer Console.WriteLine($"Selected maps: {string.Join(", ", selectedMapPrototypes)}"); } + var maps = new List(); + if (arguments.ArgumentsAreFileNames) { Console.WriteLine("Retrieving maps by file names..."); + + // + // Handle legacy command line processing: + // Ideally, people pass file names that are relative to the process working directory. + // i.e. regular command-line behavior. + // + // However, the map renderer was originally written to only handle gameMap prototypes, + // so it would actually go through the list of prototypes and match file name arguments + // via a *very* coarse check. + // + // So if we have any input filenames that don't exist... we run the old behavior. + // Yes by the way this means a typo means spinning up an entire integration pool pair + // before the map renderer can report a proper failure. + // + // Note that this legacy processing is very important! The map server currently relies on it, + // because it wants to work with file names, but we *need* to resolve the input to a prototype + // to properly export viewer JSON data. + // + + var lookupPrototypeFiles = new List(); + + foreach (var map in arguments.Maps) + { + if (File.Exists(map)) + { + maps.Add(new RenderMapFile { FileName = map }); + } + else + { + lookupPrototypeFiles.Add(map); + } + } + + if (lookupPrototypeFiles.Count > 0) + { + Console.Write($"Following map files did not exist on disk directly, searching through prototypes: {string.Join(", ", lookupPrototypeFiles)}"); + + await using var pair = await PoolManager.GetServerClient(); + var mapPrototypes = pair.Server + .ResolveDependency() + .EnumeratePrototypes() + .ToArray(); + + foreach (var toFind in lookupPrototypeFiles) + { + foreach (var mapPrototype in mapPrototypes) + { + if (mapPrototype.MapPath.Filename == toFind) + { + maps.Add(new RenderMapPrototype { Prototype = mapPrototype, }); + Console.WriteLine($"Found matching map prototype: {mapPrototype.MapName}"); + goto found; + } + } + + await Console.Error.WriteLineAsync($"Found no map prototype for file '{toFind}'!"); + + found: ; + } + } + } + else + { + foreach (var map in arguments.Maps) + { + maps.Add(new RenderMapPrototype { Prototype = map }); + } } - await Run(arguments); + await Run(arguments, maps, testContext); PoolManager.Shutdown(); } - private static async Task Run(CommandLineArguments arguments) + private static async Task Run( + CommandLineArguments arguments, + List toRender, + ExternalTestContext testContext) { - Console.WriteLine($"Creating images for {arguments.Maps.Count} maps"); + Console.WriteLine($"Creating images for {toRender.Count} maps"); + + var parallaxOutput = arguments.OutputParallax ? new ParallaxOutput(arguments.OutputPath) : null; var mapNames = new List(); - foreach (var map in arguments.Maps) + foreach (var map in toRender) { Console.WriteLine($"Painting map {map}"); - var mapViewerData = new MapViewerData - { - Id = map, - Name = Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(map) - }; + await using var painter = new MapPainter(map, testContext); + await painter.Initialize(); + await painter.SetupView(showMarkers: arguments.ShowMarkers); - mapViewerData.ParallaxLayers.Add(LayerGroup.DefaultParallax()); - var directory = Path.Combine(arguments.OutputPath, Path.GetFileNameWithoutExtension(map)); + var mapViewerData = await painter.GenerateMapViewerData(parallaxOutput); + + var mapShort = map.ShortName; + var directory = Path.Combine(arguments.OutputPath, mapShort); + + mapNames.Add(mapShort); var i = 0; try { - await foreach (var renderedGrid in MapPainter.Paint(map, - arguments.ArgumentsAreFileNames, - arguments.ShowMarkers)) + await foreach (var renderedGrid in painter.Paint()) { var grid = renderedGrid.Image; Directory.CreateDirectory(directory); - var fileName = Path.GetFileNameWithoutExtension(map); - var savePath = $"{directory}{Path.DirectorySeparatorChar}{fileName}-{i}.{arguments.Format}"; + var savePath = $"{directory}{Path.DirectorySeparatorChar}{mapShort}-{i}.{arguments.Format}"; Console.WriteLine($"Writing grid of size {grid.Width}x{grid.Height} to {savePath}"); @@ -167,9 +238,7 @@ namespace Content.MapRenderer grid.Dispose(); - mapViewerData.Grids.Add(new GridLayer(renderedGrid, Path.Combine(map, Path.GetFileName(savePath)))); - - mapNames.Add(fileName); + mapViewerData.Grids.Add(new GridLayer(renderedGrid, Path.Combine(mapShort, Path.GetFileName(savePath)))); i++; } } @@ -182,8 +251,17 @@ namespace Content.MapRenderer if (arguments.ExportViewerJson) { - var json = JsonConvert.SerializeObject(mapViewerData); - await File.WriteAllTextAsync(Path.Combine(arguments.OutputPath, map, "map.json"), json); + var json = JsonSerializer.Serialize(mapViewerData); + await File.WriteAllTextAsync(Path.Combine(directory, "map.json"), json); + } + + try + { + await painter.CleanReturnAsync(); + } + catch (Exception e) + { + Console.WriteLine($"Exception while shutting down painter: {e}"); } } diff --git a/Content.MapRenderer/RenderMap.cs b/Content.MapRenderer/RenderMap.cs new file mode 100644 index 0000000000..4ebf4ee5d4 --- /dev/null +++ b/Content.MapRenderer/RenderMap.cs @@ -0,0 +1,55 @@ +using System.IO; +using Content.Server.Maps; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.MapRenderer; + +/// +/// A single target map that the map renderer should render. +/// +/// +/// +public abstract class RenderMap +{ + /// + /// Short identifier of the map that should be unique-ish. Used in file names and other important stuff. + /// + public abstract string ShortName { get; } +} + +/// +/// Specifies a map prototype that the map renderer should render. +/// +public sealed class RenderMapPrototype : RenderMap +{ + /// + /// The ID of the prototype to render. + /// + public required ProtoId Prototype; + + public override string ShortName => Prototype; + + public override string ToString() + { + return $"{nameof(RenderMapPrototype)}({Prototype})"; + } +} + +/// +/// Specifies a map file on disk that the map renderer should render. +/// +public sealed class RenderMapFile : RenderMap +{ + /// + /// The path to the file that should be rendered. This is an OS disk path, *not* a . + /// + public required string FileName; + + public override string ShortName => Path.GetFileNameWithoutExtension(FileName); + + public override string ToString() + { + return $"{nameof(RenderMapFile)}({FileName})"; + } +} diff --git a/Resources/Textures/Parallaxes/attributions.yml b/Resources/Textures/Parallaxes/attributions.yml index 131e096862..f980eafaec 100644 --- a/Resources/Textures/Parallaxes/attributions.yml +++ b/Resources/Textures/Parallaxes/attributions.yml @@ -32,3 +32,8 @@ license: "CC-BY-NC-SA-3.0" copyright: "Made by SlamBamActionman" source: "https://github.com/space-wizards/space-station-14/blob/master/Resources/Textures/Parallaxes/space_map3.png" + +- files: [ "layer1.png", "layer2.png", "layer3.png" ] + license: "CC-BY-SA-3.0" + copyright: "Taken from TGstation13" + source: "https://github.com/tgstation/tgstation/blob/bce9afc2cf39c229f2799c6ad2124f4d69a2dbbf/icons/effects/parallax.dmi" diff --git a/Resources/Textures/Parallaxes/layer2.png b/Resources/Textures/Parallaxes/layer2.png new file mode 100644 index 0000000000..43443fa045 Binary files /dev/null and b/Resources/Textures/Parallaxes/layer2.png differ diff --git a/Resources/Textures/Parallaxes/layer2.png.yml b/Resources/Textures/Parallaxes/layer2.png.yml new file mode 100644 index 0000000000..a2cfb0a1fe --- /dev/null +++ b/Resources/Textures/Parallaxes/layer2.png.yml @@ -0,0 +1 @@ +preload: false diff --git a/Resources/Textures/Parallaxes/layer3.png b/Resources/Textures/Parallaxes/layer3.png new file mode 100644 index 0000000000..ca5589e7fe Binary files /dev/null and b/Resources/Textures/Parallaxes/layer3.png differ diff --git a/Resources/Textures/Parallaxes/layer3.png.yml b/Resources/Textures/Parallaxes/layer3.png.yml new file mode 100644 index 0000000000..a2cfb0a1fe --- /dev/null +++ b/Resources/Textures/Parallaxes/layer3.png.yml @@ -0,0 +1 @@ +preload: false diff --git a/Resources/Textures/Parallaxes/meta.json b/Resources/Textures/Parallaxes/meta.json deleted file mode 100644 index cacbbf2659..0000000000 --- a/Resources/Textures/Parallaxes/meta.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "version": 1, - "license": "CC-BY-SA-3.0", - "copyright": "https://github.com/tgstation/tgstation/blob/master/icons/effects/parallax.dmi.", - "size": { - "x": 480, - "y": 480 - }, - "states": [ - { - "name": "layer1" - } - ] -} \ No newline at end of file