From: Vlad Date: Sun, 11 May 2025 14:06:09 +0000 (-0400) Subject: Map renderer rework (#37306) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=d9542ae700457e2743f1c989962925143c9445b7;p=space-station-14.git Map renderer rework (#37306) * Update TilePainter.cs * Add support for custom offsets, grid files, and markers * Dynamic file category handling --- diff --git a/Content.MapRenderer/CommandLineArguments.cs b/Content.MapRenderer/CommandLineArguments.cs index b0db9298f8..f75b671dcb 100644 --- a/Content.MapRenderer/CommandLineArguments.cs +++ b/Content.MapRenderer/CommandLineArguments.cs @@ -12,6 +12,7 @@ public sealed class CommandLineArguments public bool ExportViewerJson { get; set; } = false; public string OutputPath { get; set; } = DirectoryExtensions.MapImages().FullName; public bool ArgumentsAreFileNames { get; set; } = false; + public bool ShowMarkers { get; set; } = false; public static bool TryParse(IReadOnlyList args, [NotNullWhen(true)] out CommandLineArguments? parsed) { @@ -59,6 +60,11 @@ public sealed class CommandLineArguments parsed.ArgumentsAreFileNames = true; break; + case "-m": + case "--markers": + parsed.ShowMarkers = true; + break; + case "-h": case "--help": PrintHelp(); @@ -95,7 +101,9 @@ Options: Defaults to Resources/MapImages -f / --files This option tells the map renderer that you supplied a list of map file names instead of their ids. - Example: Content.MapRenderer -f box.yml bagel.yml + Example: Content.MapRenderer -f /Maps/box.yml /Maps/bagel.yml + -m / --markers + Show hidden markers on map render. Defaults to false. -h / --help Displays this help text"); } diff --git a/Content.MapRenderer/Painters/DecalPainter.cs b/Content.MapRenderer/Painters/DecalPainter.cs index 6dfb26e1ef..acc9e5615a 100644 --- a/Content.MapRenderer/Painters/DecalPainter.cs +++ b/Content.MapRenderer/Painters/DecalPainter.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.IO; +using System.Numerics; using Content.Shared.Decals; +using Robust.Client.Graphics; using Robust.Client.ResourceManagement; using Robust.Client.Utility; using Robust.Shared.ContentPack; @@ -29,7 +31,7 @@ public sealed class DecalPainter _sPrototypeManager = server.ResolveDependency(); } - public void Run(Image canvas, Span decals) + public void Run(Image canvas, Span decals, Vector2 customOffset = default) { var stopwatch = new Stopwatch(); stopwatch.Start(); @@ -46,13 +48,13 @@ public sealed class DecalPainter foreach (var decal in decals) { - Run(canvas, decal); + Run(canvas, decal, customOffset); } Console.WriteLine($"{nameof(DecalPainter)} painted {decals.Length} decals in {(int) stopwatch.Elapsed.TotalMilliseconds} ms"); } - private void Run(Image canvas, DecalData data) + private void Run(Image canvas, DecalData data, Vector2 customOffset = default) { var decal = data.Decal; if (!_decalTextures.TryGetValue(decal.Id, out var sprite)) @@ -94,8 +96,10 @@ public sealed class DecalPainter .DrawImage(coloredImage, PixelColorBlendingMode.Multiply, PixelAlphaCompositionMode.SrcAtop, 1.0f) .Flip(FlipMode.Vertical)); - // Very unsure why the - 1 is needed in the first place but all decals are off by exactly one pixel otherwise + var pointX = (int) data.X + (int) (customOffset.X * EyeManager.PixelsPerMeter); + var pointY = (int) data.Y + (int) (customOffset.Y * EyeManager.PixelsPerMeter); + // Woohoo! - canvas.Mutate(o => o.DrawImage(image, new Point((int) data.X, (int) data.Y - 1), alpha)); + canvas.Mutate(o => o.DrawImage(image, new Point(pointX, pointY), alpha)); } } diff --git a/Content.MapRenderer/Painters/EntityPainter.cs b/Content.MapRenderer/Painters/EntityPainter.cs index 0eef467f9a..13c7512ea5 100644 --- a/Content.MapRenderer/Painters/EntityPainter.cs +++ b/Content.MapRenderer/Painters/EntityPainter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Numerics; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; @@ -32,7 +33,7 @@ public sealed class EntityPainter _errorImage = Image.Load(_resManager.ContentFileRead("/Textures/error.rsi/error.png")); } - public void Run(Image canvas, List entities) + public void Run(Image canvas, List entities, Vector2 customOffset = default) { var stopwatch = new Stopwatch(); stopwatch.Start(); @@ -43,13 +44,13 @@ public sealed class EntityPainter foreach (var entity in entities) { - Run(canvas, entity, xformSystem); + Run(canvas, entity, xformSystem, customOffset);; } Console.WriteLine($"{nameof(EntityPainter)} painted {entities.Count} entities in {(int) stopwatch.Elapsed.TotalMilliseconds} ms"); } - public void Run(Image canvas, EntityData entity, SharedTransformSystem xformSystem) + public void Run(Image canvas, EntityData entity, SharedTransformSystem xformSystem, Vector2 customOffset = default) { if (!entity.Sprite.Visible || entity.Sprite.ContainerOccluded) { @@ -135,8 +136,8 @@ public sealed class EntityPainter coloredImage.Mutate(o => o.BackgroundColor(imageColor)); var (imgX, imgY) = rsi?.Size ?? (EyeManager.PixelsPerMeter, EyeManager.PixelsPerMeter); - var offsetX = (int) (entity.Sprite.Offset.X * EyeManager.PixelsPerMeter); - var offsetY = (int) (entity.Sprite.Offset.Y * EyeManager.PixelsPerMeter); + var offsetX = (int) (entity.Sprite.Offset.X + customOffset.X) * EyeManager.PixelsPerMeter; + var offsetY = (int) (entity.Sprite.Offset.Y + customOffset.X) * EyeManager.PixelsPerMeter; image.Mutate(o => o .DrawImage(coloredImage, PixelColorBlendingMode.Multiply, PixelAlphaCompositionMode.SrcAtop, 1) .Resize(imgX, imgY) diff --git a/Content.MapRenderer/Painters/GridPainter.cs b/Content.MapRenderer/Painters/GridPainter.cs index d8d6e15378..ed17d0d3d6 100644 --- a/Content.MapRenderer/Painters/GridPainter.cs +++ b/Content.MapRenderer/Painters/GridPainter.cs @@ -43,7 +43,7 @@ namespace Content.MapRenderer.Painters _decals = GetDecals(); } - public void Run(Image gridCanvas, EntityUid gridUid, MapGridComponent grid) + public void Run(Image gridCanvas, EntityUid gridUid, MapGridComponent grid, Vector2 customOffset = default) { var stopwatch = new Stopwatch(); stopwatch.Start(); @@ -56,10 +56,10 @@ namespace Content.MapRenderer.Painters // Decals are always painted before entities, and are also optional. if (_decals.TryGetValue(gridUid, out var decals)) - _decalPainter.Run(gridCanvas, CollectionsMarshal.AsSpan(decals)); + _decalPainter.Run(gridCanvas, CollectionsMarshal.AsSpan(decals), customOffset); - _entityPainter.Run(gridCanvas, entities); + _entityPainter.Run(gridCanvas, entities, customOffset); Console.WriteLine($"{nameof(GridPainter)} painted grid {gridUid} in {(int) stopwatch.Elapsed.TotalMilliseconds} ms"); } diff --git a/Content.MapRenderer/Painters/MapPainter.cs b/Content.MapRenderer/Painters/MapPainter.cs index 94d58b35a0..795cbc04ee 100644 --- a/Content.MapRenderer/Painters/MapPainter.cs +++ b/Content.MapRenderer/Painters/MapPainter.cs @@ -1,19 +1,23 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Numerics; +using System.IO; using System.Threading.Tasks; using Content.IntegrationTests; using Content.Server.GameTicking; -using Content.Server.Maps; using Robust.Client.GameObjects; using Robust.Server.GameObjects; using Robust.Server.Player; +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.Prototypes; using Robust.Shared.Timing; +using Robust.Shared.Utility; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -22,7 +26,9 @@ namespace Content.MapRenderer.Painters { public sealed class MapPainter { - public static async IAsyncEnumerable> Paint(string map) + public static async IAsyncEnumerable> Paint(string map, + bool mapIsFilename = false, + bool showMarkers = false) { var stopwatch = new Stopwatch(); stopwatch.Start(); @@ -33,7 +39,7 @@ namespace Content.MapRenderer.Painters Connected = true, Fresh = true, // Seriously whoever made MapPainter use GameMapPrototype I wish you step on a lego one time. - Map = map, + Map = mapIsFilename ? "Empty" : map, }); var server = pair.Server; @@ -54,9 +60,89 @@ namespace Content.MapRenderer.Painters } }); + if (showMarkers) + await pair.WaitClientCommand("showmarkers"); + 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 Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); @@ -64,7 +150,6 @@ namespace Content.MapRenderer.Painters var tilePainter = new TilePainter(client, server); var entityPainter = new GridPainter(client, server); - Entity[] grids = null!; var xformQuery = sEntityManager.GetEntityQuery(); var xformSystem = sEntityManager.System(); @@ -77,8 +162,11 @@ namespace Content.MapRenderer.Painters sEntityManager.DeleteEntity(playerEntity.Value); } - var mapId = sEntityManager.System().DefaultMap; - grids = sMapManager.GetAllGrids(mapId).ToArray(); + if (!mapIsFilename) + { + var mapId = sEntityManager.System().DefaultMap; + grids = sMapManager.GetAllGrids(mapId).ToArray(); + } foreach (var (uid, _) in grids) { @@ -92,32 +180,33 @@ namespace Content.MapRenderer.Painters foreach (var (uid, grid) in grids) { - // Skip empty grids - if (grid.LocalAABB.IsEmpty()) + var tiles = mapSys.GetAllTiles(uid, grid).ToList(); + if (tiles.Count == 0) { Console.WriteLine($"Warning: Grid {uid} was empty. Skipping image rendering."); continue; } - var tileXSize = grid.TileSize * TilePainter.TileImageSize; var tileYSize = grid.TileSize * TilePainter.TileImageSize; - var bounds = grid.LocalAABB; - - var left = bounds.Left; - var right = bounds.Right; - var top = bounds.Top; - var bottom = bounds.Bottom; + var minX = tiles.Min(t => t.X); + var minY = tiles.Min(t => t.Y); + var maxX = tiles.Max(t => t.X); + var maxY = tiles.Max(t => t.Y); + var w = (maxX - minX + 1) * tileXSize; + var h = (maxY - minY + 1) * tileYSize; + var customOffset = new Vector2(); - var w = (int) Math.Ceiling(right - left) * tileXSize; - var h = (int) Math.Ceiling(top - bottom) * tileYSize; + //MapGrids don't have LocalAABB, so we offset them to align the bottom left corner with 0,0 coordinates + if (grid.LocalAABB.IsEmpty()) + customOffset = new Vector2(-minX, -minY); var gridCanvas = new Image(w, h); await server.WaitPost(() => { - tilePainter.Run(gridCanvas, uid, grid); - entityPainter.Run(gridCanvas, uid, grid); + tilePainter.Run(gridCanvas, uid, grid, customOffset); + entityPainter.Run(gridCanvas, uid, grid, customOffset); gridCanvas.Mutate(e => e.Flip(FlipMode.Vertical)); }); diff --git a/Content.MapRenderer/Painters/TilePainter.cs b/Content.MapRenderer/Painters/TilePainter.cs index 7084d2caf4..4856c181da 100644 --- a/Content.MapRenderer/Painters/TilePainter.cs +++ b/Content.MapRenderer/Painters/TilePainter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Numerics; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; using Robust.Shared.ContentPack; @@ -31,7 +32,7 @@ namespace Content.MapRenderer.Painters _sMapSystem = esm.GetEntitySystem(); } - public void Run(Image gridCanvas, EntityUid gridUid, MapGridComponent grid) + public void Run(Image gridCanvas, EntityUid gridUid, MapGridComponent grid, Vector2 customOffset = default) { var stopwatch = new Stopwatch(); stopwatch.Start(); @@ -51,8 +52,8 @@ namespace Content.MapRenderer.Painters if (string.IsNullOrWhiteSpace(path)) return; - var x = (int) (tile.X + xOffset); - var y = (int) (tile.Y + yOffset); + var x = (int) (tile.X + xOffset + customOffset.X); + var y = (int) (tile.Y + yOffset + customOffset.Y); var image = images[path][tile.Tile.Variant]; gridCanvas.Mutate(o => o.DrawImage(image, new Point(x * tileSize, y * tileSize), 1)); @@ -93,7 +94,7 @@ namespace Content.MapRenderer.Painters for (var i = 0; i < definition.Variants; i++) { var index = i; - var tileImage = tileSheet.Clone(o => o.Crop(new Rectangle(tileSize * index, 0, 32, 32))); + var tileImage = tileSheet.Clone(o => o.Crop(new Rectangle(tileSize * index, 0, 32, 32)).Flip(FlipMode.Vertical)); images[path].Add(tileImage); } } diff --git a/Content.MapRenderer/Program.cs b/Content.MapRenderer/Program.cs index 5a4ccda0c8..115d83e65e 100644 --- a/Content.MapRenderer/Program.cs +++ b/Content.MapRenderer/Program.cs @@ -106,34 +106,7 @@ namespace Content.MapRenderer if (arguments.ArgumentsAreFileNames) { - Console.WriteLine("Retrieving map ids by map file names..."); - - Console.Write("Fetching map prototypes... "); - await using var pair = await PoolManager.GetServerClient(); - var mapPrototypes = pair.Server - .ResolveDependency() - .EnumeratePrototypes() - .ToArray(); - Console.WriteLine("[Done]"); - - var ids = new List(); - - foreach (var mapPrototype in mapPrototypes) - { - if (arguments.Maps.Contains(mapPrototype.MapPath.Filename)) - { - ids.Add(mapPrototype.ID); - Console.WriteLine($"Found map: {mapPrototype.MapName}"); - } - } - - if (ids.Count == 0) - { - await Console.Error.WriteLineAsync("Found no maps for the given file names!"); - return; - } - - arguments.Maps = ids; + Console.WriteLine("Retrieving maps by file names..."); } await Run(arguments); @@ -156,12 +129,14 @@ namespace Content.MapRenderer }; mapViewerData.ParallaxLayers.Add(LayerGroup.DefaultParallax()); - var directory = Path.Combine(arguments.OutputPath, map); + var directory = Path.Combine(arguments.OutputPath, Path.GetFileNameWithoutExtension(map)); var i = 0; try { - await foreach (var renderedGrid in MapPainter.Paint(map)) + await foreach (var renderedGrid in MapPainter.Paint(map, + arguments.ArgumentsAreFileNames, + arguments.ShowMarkers)) { var grid = renderedGrid.Image; Directory.CreateDirectory(directory);