From bebc077fcc91e9807c2d9d42621a1f58dc181600 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Thu, 26 Jun 2025 14:47:39 +0200 Subject: [PATCH] MapRenderer code fixes (#38357) * Fix MapRenderer integration test usage to properly show output. Added an ITestContextLike interface that can be used to properly run the integration test infrastructure OUTSIDE A TEST. * Use System.Test.Json instead of Newtonsoft.Json for MapRenderer * Fix map renderer JSON output being broken I love not testing or even reading the surrounding code. * Fix un-reusable integration instances getting leaked. The pair state was always getting set to Ready even if the instance was killed, meaning it was getting put back into the pool even if killed. * Mark map renderer integration instances as destructive to avoid memory leak. * Fix file specification handling. Map file specification is now backwards compatible again (loose filename match to search prototypes). It also supports proper direct OS filename arguments. The former is the fallback scenario is extremely important for the map server still. Cleaned up the way that target map files are passed through the application, so mixed file/prototype specifications are now handled properly (which can be caused by the fallback behavior). Fixes JSON data export to use the proper user-facing map name. This only works if a prototype ID is specified *or* the legacy file behavior is used. Restructured MapPainter into an instance that has multiple functions called on it, so not all data has to be passed through a single Paint() call. Clean up the godawful map/grid detection code. Now we just load both in a single call, because yes you can do that. This relies on LogOrphanedGrids = false in the map loader options, which I think is fine for our purposes. Improved error handling in much of the program. * Fix duplicate map names in map renderer output I'm not sure *what* this output is used for, but I'm sure having it duplicated per grid isn't intentional. * Make maprenderer command line parsing bail on unknown - options * Fix incorrect docs for --viewer maprenderer argument It doesn't change directory layout * Fix parallax layer specification to not use imgur as a fucking CDN Files are now copied to a separate folder _parallax, and these files are referenced by the parallax configuration. Parallax data is only output when instructed to via --parallax. This will break parallax on current map server builds, but it should be graceful. Also, that's fucking good considering we shouldn't be using imgur links. Purge it. * Fix incorrect assert in test pair clean return * Restore other map viewer parallax layers, fix attribution. * This isn't a valid copyright statement but the validator forces me to enter something here. --- .../ExternalTestContext.cs | 12 + Content.IntegrationTests/ITestContextLike.cs | 13 + .../NUnitTestContextWrap.cs | 12 + .../Pair/TestPair.Recycle.cs | 4 +- Content.IntegrationTests/PoolManager.cs | 19 +- Content.MapRenderer/CommandLineArguments.cs | 14 +- Content.MapRenderer/MapViewerData.cs | 22 +- Content.MapRenderer/Painters/MapPainter.cs | 239 +++++++++--------- Content.MapRenderer/ParallaxOutput.cs | 41 +++ Content.MapRenderer/Program.cs | 132 ++++++++-- Content.MapRenderer/RenderMap.cs | 55 ++++ .../Textures/Parallaxes/attributions.yml | 5 + Resources/Textures/Parallaxes/layer2.png | Bin 0 -> 49049 bytes Resources/Textures/Parallaxes/layer2.png.yml | 1 + Resources/Textures/Parallaxes/layer3.png | Bin 0 -> 1580 bytes Resources/Textures/Parallaxes/layer3.png.yml | 1 + Resources/Textures/Parallaxes/meta.json | 14 - 17 files changed, 414 insertions(+), 170 deletions(-) create mode 100644 Content.IntegrationTests/ExternalTestContext.cs create mode 100644 Content.IntegrationTests/ITestContextLike.cs create mode 100644 Content.IntegrationTests/NUnitTestContextWrap.cs create mode 100644 Content.MapRenderer/ParallaxOutput.cs create mode 100644 Content.MapRenderer/RenderMap.cs create mode 100644 Resources/Textures/Parallaxes/layer2.png create mode 100644 Resources/Textures/Parallaxes/layer2.png.yml create mode 100644 Resources/Textures/Parallaxes/layer3.png create mode 100644 Resources/Textures/Parallaxes/layer3.png.yml delete mode 100644 Resources/Textures/Parallaxes/meta.json 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 0000000000000000000000000000000000000000..43443fa0459770c5111bd92962a32be2ac9fa7eb GIT binary patch literal 49049 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 CTzicG literal 0 HcmV?d00001 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