Map renderer rework (#37306)

* Update TilePainter.cs

* Add support for custom offsets, grid files, and markers

* Dynamic file category handling
This commit is contained in:
Vlad
2025-05-11 10:06:09 -04:00
committed by GitHub
parent ca39645b69
commit d9542ae700
7 changed files with 146 additions and 68 deletions

View File

@@ -12,6 +12,7 @@ public sealed class CommandLineArguments
public bool ExportViewerJson { get; set; } = false; public bool ExportViewerJson { get; set; } = false;
public string OutputPath { get; set; } = DirectoryExtensions.MapImages().FullName; public string OutputPath { get; set; } = DirectoryExtensions.MapImages().FullName;
public bool ArgumentsAreFileNames { get; set; } = false; public bool ArgumentsAreFileNames { get; set; } = false;
public bool ShowMarkers { get; set; } = false;
public static bool TryParse(IReadOnlyList<string> args, [NotNullWhen(true)] out CommandLineArguments? parsed) public static bool TryParse(IReadOnlyList<string> args, [NotNullWhen(true)] out CommandLineArguments? parsed)
{ {
@@ -59,6 +60,11 @@ public sealed class CommandLineArguments
parsed.ArgumentsAreFileNames = true; parsed.ArgumentsAreFileNames = true;
break; break;
case "-m":
case "--markers":
parsed.ShowMarkers = true;
break;
case "-h": case "-h":
case "--help": case "--help":
PrintHelp(); PrintHelp();
@@ -95,7 +101,9 @@ Options:
Defaults to Resources/MapImages Defaults to Resources/MapImages
-f / --files -f / --files
This option tells the map renderer that you supplied a list of map file names instead of their ids. 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 -h / --help
Displays this help text"); Displays this help text");
} }

View File

@@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Numerics;
using Content.Shared.Decals; using Content.Shared.Decals;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement; using Robust.Client.ResourceManagement;
using Robust.Client.Utility; using Robust.Client.Utility;
using Robust.Shared.ContentPack; using Robust.Shared.ContentPack;
@@ -29,7 +31,7 @@ public sealed class DecalPainter
_sPrototypeManager = server.ResolveDependency<IPrototypeManager>(); _sPrototypeManager = server.ResolveDependency<IPrototypeManager>();
} }
public void Run(Image canvas, Span<DecalData> decals) public void Run(Image canvas, Span<DecalData> decals, Vector2 customOffset = default)
{ {
var stopwatch = new Stopwatch(); var stopwatch = new Stopwatch();
stopwatch.Start(); stopwatch.Start();
@@ -46,13 +48,13 @@ public sealed class DecalPainter
foreach (var decal in decals) 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"); 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; var decal = data.Decal;
if (!_decalTextures.TryGetValue(decal.Id, out var sprite)) if (!_decalTextures.TryGetValue(decal.Id, out var sprite))
@@ -94,8 +96,10 @@ public sealed class DecalPainter
.DrawImage(coloredImage, PixelColorBlendingMode.Multiply, PixelAlphaCompositionMode.SrcAtop, 1.0f) .DrawImage(coloredImage, PixelColorBlendingMode.Multiply, PixelAlphaCompositionMode.SrcAtop, 1.0f)
.Flip(FlipMode.Vertical)); .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! // 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));
} }
} }

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.ResourceManagement; using Robust.Client.ResourceManagement;
@@ -32,7 +33,7 @@ public sealed class EntityPainter
_errorImage = Image.Load<Rgba32>(_resManager.ContentFileRead("/Textures/error.rsi/error.png")); _errorImage = Image.Load<Rgba32>(_resManager.ContentFileRead("/Textures/error.rsi/error.png"));
} }
public void Run(Image canvas, List<EntityData> entities) public void Run(Image canvas, List<EntityData> entities, Vector2 customOffset = default)
{ {
var stopwatch = new Stopwatch(); var stopwatch = new Stopwatch();
stopwatch.Start(); stopwatch.Start();
@@ -43,13 +44,13 @@ public sealed class EntityPainter
foreach (var entity in entities) 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"); 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) if (!entity.Sprite.Visible || entity.Sprite.ContainerOccluded)
{ {
@@ -135,8 +136,8 @@ public sealed class EntityPainter
coloredImage.Mutate(o => o.BackgroundColor(imageColor)); coloredImage.Mutate(o => o.BackgroundColor(imageColor));
var (imgX, imgY) = rsi?.Size ?? (EyeManager.PixelsPerMeter, EyeManager.PixelsPerMeter); var (imgX, imgY) = rsi?.Size ?? (EyeManager.PixelsPerMeter, EyeManager.PixelsPerMeter);
var offsetX = (int) (entity.Sprite.Offset.X * EyeManager.PixelsPerMeter); var offsetX = (int) (entity.Sprite.Offset.X + customOffset.X) * EyeManager.PixelsPerMeter;
var offsetY = (int) (entity.Sprite.Offset.Y * EyeManager.PixelsPerMeter); var offsetY = (int) (entity.Sprite.Offset.Y + customOffset.X) * EyeManager.PixelsPerMeter;
image.Mutate(o => o image.Mutate(o => o
.DrawImage(coloredImage, PixelColorBlendingMode.Multiply, PixelAlphaCompositionMode.SrcAtop, 1) .DrawImage(coloredImage, PixelColorBlendingMode.Multiply, PixelAlphaCompositionMode.SrcAtop, 1)
.Resize(imgX, imgY) .Resize(imgX, imgY)

View File

@@ -43,7 +43,7 @@ namespace Content.MapRenderer.Painters
_decals = GetDecals(); _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(); var stopwatch = new Stopwatch();
stopwatch.Start(); stopwatch.Start();
@@ -56,10 +56,10 @@ namespace Content.MapRenderer.Painters
// Decals are always painted before entities, and are also optional. // Decals are always painted before entities, and are also optional.
if (_decals.TryGetValue(gridUid, out var decals)) 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"); Console.WriteLine($"{nameof(GridPainter)} painted grid {gridUid} in {(int) stopwatch.Elapsed.TotalMilliseconds} ms");
} }

View File

@@ -1,19 +1,23 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.IntegrationTests; using Content.IntegrationTests;
using Content.Server.GameTicking; using Content.Server.GameTicking;
using Content.Server.Maps;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Map.Components; using Robust.Shared.Map.Components;
using Robust.Shared.Map.Events;
using Robust.Shared.Maths; using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.Utility;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
@@ -22,7 +26,9 @@ namespace Content.MapRenderer.Painters
{ {
public sealed class MapPainter public sealed class MapPainter
{ {
public static async IAsyncEnumerable<RenderedGridImage<Rgba32>> Paint(string map) public static async IAsyncEnumerable<RenderedGridImage<Rgba32>> Paint(string map,
bool mapIsFilename = false,
bool showMarkers = false)
{ {
var stopwatch = new Stopwatch(); var stopwatch = new Stopwatch();
stopwatch.Start(); stopwatch.Start();
@@ -33,7 +39,7 @@ namespace Content.MapRenderer.Painters
Connected = true, Connected = true,
Fresh = true, Fresh = true,
// Seriously whoever made MapPainter use GameMapPrototype I wish you step on a lego one time. // 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; var server = pair.Server;
@@ -54,9 +60,89 @@ namespace Content.MapRenderer.Painters
} }
}); });
if (showMarkers)
await pair.WaitClientCommand("showmarkers");
var sEntityManager = server.ResolveDependency<IServerEntityManager>(); var sEntityManager = server.ResolveDependency<IServerEntityManager>();
var sPlayerManager = server.ResolveDependency<IPlayerManager>(); var sPlayerManager = server.ResolveDependency<IPlayerManager>();
var entityManager = server.ResolveDependency<IEntityManager>();
var mapLoader = entityManager.System<MapLoaderSystem>();
var mapSys = entityManager.System<SharedMapSystem>();
var deps = server.ResolveDependency<IEntitySystemManager>().DependencyCollection;
Entity<MapGridComponent>[] 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<MapGridComponent>)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()); await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
@@ -64,7 +150,6 @@ namespace Content.MapRenderer.Painters
var tilePainter = new TilePainter(client, server); var tilePainter = new TilePainter(client, server);
var entityPainter = new GridPainter(client, server); var entityPainter = new GridPainter(client, server);
Entity<MapGridComponent>[] grids = null!;
var xformQuery = sEntityManager.GetEntityQuery<TransformComponent>(); var xformQuery = sEntityManager.GetEntityQuery<TransformComponent>();
var xformSystem = sEntityManager.System<SharedTransformSystem>(); var xformSystem = sEntityManager.System<SharedTransformSystem>();
@@ -77,8 +162,11 @@ namespace Content.MapRenderer.Painters
sEntityManager.DeleteEntity(playerEntity.Value); sEntityManager.DeleteEntity(playerEntity.Value);
} }
var mapId = sEntityManager.System<GameTicker>().DefaultMap; if (!mapIsFilename)
grids = sMapManager.GetAllGrids(mapId).ToArray(); {
var mapId = sEntityManager.System<GameTicker>().DefaultMap;
grids = sMapManager.GetAllGrids(mapId).ToArray();
}
foreach (var (uid, _) in grids) foreach (var (uid, _) in grids)
{ {
@@ -92,32 +180,33 @@ namespace Content.MapRenderer.Painters
foreach (var (uid, grid) in grids) foreach (var (uid, grid) in grids)
{ {
// Skip empty grids var tiles = mapSys.GetAllTiles(uid, grid).ToList();
if (grid.LocalAABB.IsEmpty()) if (tiles.Count == 0)
{ {
Console.WriteLine($"Warning: Grid {uid} was empty. Skipping image rendering."); Console.WriteLine($"Warning: Grid {uid} was empty. Skipping image rendering.");
continue; continue;
} }
var tileXSize = grid.TileSize * TilePainter.TileImageSize; var tileXSize = grid.TileSize * TilePainter.TileImageSize;
var tileYSize = grid.TileSize * TilePainter.TileImageSize; var tileYSize = grid.TileSize * TilePainter.TileImageSize;
var bounds = grid.LocalAABB; 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 left = bounds.Left; //MapGrids don't have LocalAABB, so we offset them to align the bottom left corner with 0,0 coordinates
var right = bounds.Right; if (grid.LocalAABB.IsEmpty())
var top = bounds.Top; customOffset = new Vector2(-minX, -minY);
var bottom = bounds.Bottom;
var w = (int) Math.Ceiling(right - left) * tileXSize;
var h = (int) Math.Ceiling(top - bottom) * tileYSize;
var gridCanvas = new Image<Rgba32>(w, h); var gridCanvas = new Image<Rgba32>(w, h);
await server.WaitPost(() => await server.WaitPost(() =>
{ {
tilePainter.Run(gridCanvas, uid, grid); tilePainter.Run(gridCanvas, uid, grid, customOffset);
entityPainter.Run(gridCanvas, uid, grid); entityPainter.Run(gridCanvas, uid, grid, customOffset);
gridCanvas.Mutate(e => e.Flip(FlipMode.Vertical)); gridCanvas.Mutate(e => e.Flip(FlipMode.Vertical));
}); });

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.ResourceManagement; using Robust.Client.ResourceManagement;
using Robust.Shared.ContentPack; using Robust.Shared.ContentPack;
@@ -31,7 +32,7 @@ namespace Content.MapRenderer.Painters
_sMapSystem = esm.GetEntitySystem<SharedMapSystem>(); _sMapSystem = esm.GetEntitySystem<SharedMapSystem>();
} }
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(); var stopwatch = new Stopwatch();
stopwatch.Start(); stopwatch.Start();
@@ -51,8 +52,8 @@ namespace Content.MapRenderer.Painters
if (string.IsNullOrWhiteSpace(path)) if (string.IsNullOrWhiteSpace(path))
return; return;
var x = (int) (tile.X + xOffset); var x = (int) (tile.X + xOffset + customOffset.X);
var y = (int) (tile.Y + yOffset); var y = (int) (tile.Y + yOffset + customOffset.Y);
var image = images[path][tile.Tile.Variant]; var image = images[path][tile.Tile.Variant];
gridCanvas.Mutate(o => o.DrawImage(image, new Point(x * tileSize, y * tileSize), 1)); 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++) for (var i = 0; i < definition.Variants; i++)
{ {
var index = 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); images[path].Add(tileImage);
} }
} }

View File

@@ -106,34 +106,7 @@ namespace Content.MapRenderer
if (arguments.ArgumentsAreFileNames) if (arguments.ArgumentsAreFileNames)
{ {
Console.WriteLine("Retrieving map ids by map file names..."); Console.WriteLine("Retrieving maps by file names...");
Console.Write("Fetching map prototypes... ");
await using var pair = await PoolManager.GetServerClient();
var mapPrototypes = pair.Server
.ResolveDependency<IPrototypeManager>()
.EnumeratePrototypes<GameMapPrototype>()
.ToArray();
Console.WriteLine("[Done]");
var ids = new List<string>();
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;
} }
await Run(arguments); await Run(arguments);
@@ -156,12 +129,14 @@ namespace Content.MapRenderer
}; };
mapViewerData.ParallaxLayers.Add(LayerGroup.DefaultParallax()); mapViewerData.ParallaxLayers.Add(LayerGroup.DefaultParallax());
var directory = Path.Combine(arguments.OutputPath, map); var directory = Path.Combine(arguments.OutputPath, Path.GetFileNameWithoutExtension(map));
var i = 0; var i = 0;
try 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; var grid = renderedGrid.Image;
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);