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:
@@ -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<string> 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");
|
||||
}
|
||||
|
||||
@@ -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<IPrototypeManager>();
|
||||
}
|
||||
|
||||
public void Run(Image canvas, Span<DecalData> decals)
|
||||
public void Run(Image canvas, Span<DecalData> 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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();
|
||||
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)
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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<RenderedGridImage<Rgba32>> Paint(string map)
|
||||
public static async IAsyncEnumerable<RenderedGridImage<Rgba32>> 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<IServerEntityManager>();
|
||||
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 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<MapGridComponent>[] grids = null!;
|
||||
var xformQuery = sEntityManager.GetEntityQuery<TransformComponent>();
|
||||
var xformSystem = sEntityManager.System<SharedTransformSystem>();
|
||||
|
||||
@@ -77,8 +162,11 @@ namespace Content.MapRenderer.Painters
|
||||
sEntityManager.DeleteEntity(playerEntity.Value);
|
||||
}
|
||||
|
||||
if (!mapIsFilename)
|
||||
{
|
||||
var mapId = sEntityManager.System<GameTicker>().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 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;
|
||||
var right = bounds.Right;
|
||||
var top = bounds.Top;
|
||||
var bottom = bounds.Bottom;
|
||||
|
||||
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<Rgba32>(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));
|
||||
});
|
||||
|
||||
@@ -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<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();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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;
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user