diff --git a/Content.MapRenderer/Logger.cs b/Content.MapRenderer/Logger.cs new file mode 100644 index 000000000000..e9dd22e9d892 --- /dev/null +++ b/Content.MapRenderer/Logger.cs @@ -0,0 +1,42 @@ +using System; +using System.IO; + +namespace Content.MapRenderer +{ + internal static class Logger + { + private static readonly string LogFilePath = "RenderLog.txt"; + + public static void Init() + { + try + { + File.WriteAllText(LogFilePath, string.Empty); + Log("Started", sendInConsole: false); + } + catch {} + } + + public static void Log(string message, Exception? ex = null, bool sendInConsole = true) + { + try + { + if (sendInConsole) + { + if (ex != null) + Console.Error.WriteLine($"{message}\n{ex}"); + else + Console.WriteLine(message); + } + + var line = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {message}{Environment.NewLine}"; + if (ex != null) + line += $"Exception details: {ex}{Environment.NewLine}"; + + + File.AppendAllText(LogFilePath, line); + } + catch {} + } + } +} diff --git a/Content.MapRenderer/Painters/DecalPainter.cs b/Content.MapRenderer/Painters/DecalPainter.cs index acc9e5615a39..38e20f7fbaf0 100644 --- a/Content.MapRenderer/Painters/DecalPainter.cs +++ b/Content.MapRenderer/Painters/DecalPainter.cs @@ -51,7 +51,7 @@ public void Run(Image canvas, Span decals, Vector2 customOffset = def Run(canvas, decal, customOffset); } - Console.WriteLine($"{nameof(DecalPainter)} painted {decals.Length} decals in {(int) stopwatch.Elapsed.TotalMilliseconds} ms"); + Logger.Log($"{nameof(DecalPainter)} painted {decals.Length} decals in {(int) stopwatch.Elapsed.TotalMilliseconds} ms"); } private void Run(Image canvas, DecalData data, Vector2 customOffset = default) @@ -59,7 +59,7 @@ private void Run(Image canvas, DecalData data, Vector2 customOffset = default) var decal = data.Decal; if (!_decalTextures.TryGetValue(decal.Id, out var sprite)) { - Console.WriteLine($"Decal {decal.Id} did not have an associated prototype."); + Logger.Log($"Decal {decal.Id} did not have an associated prototype."); return; } diff --git a/Content.MapRenderer/Painters/EntityPainter.cs b/Content.MapRenderer/Painters/EntityPainter.cs index 0c751b858399..3e4c4ce01f3b 100644 --- a/Content.MapRenderer/Painters/EntityPainter.cs +++ b/Content.MapRenderer/Painters/EntityPainter.cs @@ -49,7 +49,7 @@ public void Run(Image canvas, List entities, Vector2 customOffset = Run(canvas, entity, xformSystem, customOffset); } - Console.WriteLine($"{nameof(EntityPainter)} painted {entities.Count} entities in {(int)stopwatch.Elapsed.TotalMilliseconds} ms"); + Logger.Log($"{nameof(EntityPainter)} painted {entities.Count} entities in {(int)stopwatch.Elapsed.TotalMilliseconds} ms"); } public void Run(Image canvas, EntityData entity, SharedTransformSystem xformSystem, Vector2 customOffset = default) @@ -120,7 +120,7 @@ public void Run(Image canvas, EntityData entity, SharedTransformSystem xformSyst var rect = new Rectangle(x, y, width, height); if (!new Rectangle(Point.Empty, image.Size).Contains(rect)) { - Console.WriteLine($"Invalid layer {rsi!.Path}/{layer.RsiState.Name}.png for entity {_sEntityManager.ToPrettyString(entity.Owner)} at ({entity.X}, {entity.Y})"); + Logger.Log($"Invalid layer {rsi!.Path}/{layer.RsiState.Name}.png for entity {_sEntityManager.ToPrettyString(entity.Owner)} at ({entity.X}, {entity.Y})"); return; } diff --git a/Content.MapRenderer/Painters/GridPainter.cs b/Content.MapRenderer/Painters/GridPainter.cs index ed17d0d3d66e..9aee0f098f71 100644 --- a/Content.MapRenderer/Painters/GridPainter.cs +++ b/Content.MapRenderer/Painters/GridPainter.cs @@ -50,7 +50,7 @@ public void Run(Image gridCanvas, EntityUid gridUid, MapGridComponent grid, Vect if (!_entities.TryGetValue(gridUid, out var entities)) { - Console.WriteLine($"No entities found on grid {gridUid}"); + Logger.Log($"No entities found on grid {gridUid}"); return; } @@ -60,7 +60,7 @@ public void Run(Image gridCanvas, EntityUid gridUid, MapGridComponent grid, Vect _entityPainter.Run(gridCanvas, entities, customOffset); - Console.WriteLine($"{nameof(GridPainter)} painted grid {gridUid} in {(int) stopwatch.Elapsed.TotalMilliseconds} ms"); + Logger.Log($"{nameof(GridPainter)} painted grid {gridUid} in {(int) stopwatch.Elapsed.TotalMilliseconds} ms"); } private ConcurrentDictionary> GetEntities() @@ -96,7 +96,7 @@ private ConcurrentDictionary> GetEntities() } } - Console.WriteLine($"Found {components.Values.Sum(l => l.Count)} entities on {components.Count} grids in {(int) stopwatch.Elapsed.TotalMilliseconds} ms"); + Logger.Log($"Found {components.Values.Sum(l => l.Count)} entities on {components.Count} grids in {(int) stopwatch.Elapsed.TotalMilliseconds} ms"); return components; } @@ -128,7 +128,7 @@ private Dictionary> GetDecals() } } - Console.WriteLine($"Found {decals.Values.Sum(l => l.Count)} decals on {decals.Count} grids in {(int) stopwatch.Elapsed.TotalMilliseconds} ms"); + Logger.Log($"Found {decals.Values.Sum(l => l.Count)} decals on {decals.Count} grids in {(int) stopwatch.Elapsed.TotalMilliseconds} ms"); return decals; } diff --git a/Content.MapRenderer/Painters/MapPainter.cs b/Content.MapRenderer/Painters/MapPainter.cs index 991fa74fe1c8..0e39a6391bcd 100644 --- a/Content.MapRenderer/Painters/MapPainter.cs +++ b/Content.MapRenderer/Painters/MapPainter.cs @@ -20,6 +20,7 @@ using Robust.Shared.Map.Components; using Robust.Shared.Maths; using Robust.Shared.Timing; +using Robust.Shared.Utility; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -44,21 +45,27 @@ public async Task Initialize() { var stopwatch = RStopwatch.StartNew(); + Logger.Log($"Map type: {_map.GetType().Name}"); + Logger.Log($"Map: {_map}"); + 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 = _map is RenderMapPrototype prototype ? prototype.Prototype : PoolManager.TestMap, }; + + Logger.Log($"Pool map setting: {poolSettings.Map}"); + _pair = await PoolManager.GetServerClient(poolSettings, _testContextLike); - Console.WriteLine($"Loaded client and server in {(int)stopwatch.Elapsed.TotalMilliseconds} ms"); + Logger.Log($"Loaded client and server in {(int)stopwatch.Elapsed.TotalMilliseconds} ms"); if (_map is RenderMapFile mapFile) { + Logger.Log($"Loading RenderMapFile: {mapFile.FileName}"); using var stream = File.OpenRead(mapFile.FileName); await _pair.Server.WaitPost(() => @@ -76,8 +83,37 @@ await _pair.Server.WaitPost(() => throw new IOException($"File {mapFile.FileName} could not be read"); _grids = loadResult.Grids.ToArray(); + Logger.Log($"Loaded {_grids.Length} grids from RenderMapFile"); }); } + else if (_map is RenderMapGrid gridFile) + { + Logger.Log($"Loading RenderMapGrid: {gridFile.FileName}"); + await _pair.Server.WaitPost(() => + { + var mapSystem = _pair.Server.System(); + var mapManager = _pair.Server.ResolveDependency(); + var mapLoader = _pair.Server.System(); + + Logger.Log($"Creating empty map"); + var mapUid = mapSystem.CreateMap(out var mapId, false); + Logger.Log($"Created map entity {mapUid} with ID: {mapId}"); + + var path = new ResPath(gridFile.FileName); + var opts = new DeserializationOptions { StoreYamlUids = true }; + + Logger.Log($"Loading grid from: {path}"); + if (!mapLoader.TryLoadGrid(mapId, path, out var loadedGrid, opts)) + throw new IOException($"Grid file {gridFile.FileName} could not be loaded"); + + _grids = mapManager.GetAllGrids(mapId).ToArray(); + Logger.Log($"Loaded {_grids.Length} grids from RenderMapGrid"); + }); + } + else + { + Logger.Log($"Using RenderMapPrototype with default map loading"); + } } public async Task SetupView(bool showMarkers) @@ -192,7 +228,7 @@ await server.WaitPost(() => var tiles = mapSys.GetAllTiles(uid, grid).ToList(); if (tiles.Count == 0) { - Console.WriteLine($"Warning: Grid {uid} was empty. Skipping image rendering."); + Logger.Log($"Warning: Grid {uid} was empty. Skipping image rendering."); continue; } var tileXSize = grid.TileSize * TilePainter.TileImageSize; diff --git a/Content.MapRenderer/Painters/TilePainter.cs b/Content.MapRenderer/Painters/TilePainter.cs index 114db8cb5eb8..6295f54f2839 100644 --- a/Content.MapRenderer/Painters/TilePainter.cs +++ b/Content.MapRenderer/Painters/TilePainter.cs @@ -81,7 +81,7 @@ public void Run(Image gridCanvas, EntityUid gridUid, MapGridComponent grid, Vect i++; }); - Console.WriteLine($"{nameof(TilePainter)} painted {i} tiles on grid {gridUid} in {(int) stopwatch.Elapsed.TotalMilliseconds} ms"); + Logger.Log($"{nameof(TilePainter)} painted {i} tiles on grid {gridUid} in {(int) stopwatch.Elapsed.TotalMilliseconds} ms"); } private Dictionary> GetTileImages( @@ -119,7 +119,7 @@ private Dictionary> GetTileImages( } } - Console.WriteLine($"Indexed all tile images in {(int) stopwatch.Elapsed.TotalMilliseconds} ms"); + Logger.Log($"Indexed all tile images in {(int) stopwatch.Elapsed.TotalMilliseconds} ms"); return images; } diff --git a/Content.MapRenderer/Program.cs b/Content.MapRenderer/Program.cs index 9d7843bcd07b..f839d1ad0225 100644 --- a/Content.MapRenderer/Program.cs +++ b/Content.MapRenderer/Program.cs @@ -7,10 +7,13 @@ using System.Threading.Tasks; using Content.IntegrationTests; using Content.MapRenderer.Painters; +using Robust.Shared.ContentPack; using Content.Server.Maps; using Robust.Shared.Prototypes; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Webp; +using Robust.Shared.Utility; +//using Robust.Shared.Log; namespace Content.MapRenderer { @@ -22,17 +25,42 @@ internal sealed class Program internal static async Task Main(string[] args) { + //LogManager.GlobalSawmill.Subscribe((level, message, exception) => + //{ + // if (level >= LogLevel.Error) + // { + // Logger.Log($"{message}", exception, false); + // } + //}); + Logger.Init(); + + AppDomain.CurrentDomain.UnhandledException += (sender, e) => + { + if (e.ExceptionObject is Exception ex) + Logger.Log("Unhandled exception", ex); + }; + + TaskScheduler.UnobservedTaskException += (sender, e) => + { + Logger.Log("Unobserved task exception", e.Exception); + e.SetObserved(); + }; + if (!CommandLineArguments.TryParse(args, out var arguments)) return; var testContext = new ExternalTestContext("Content.MapRenderer", Console.Out); PoolManager.Startup(); + + var maps = new List(); + 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(testContext: testContext); + var protoManager = pair.Server.ResolveDependency(); var mapIds = pair.Server .ResolveDependency() .EnumeratePrototypes() @@ -49,7 +77,7 @@ internal static async Task Main(string[] args) var input = Console.ReadLine(); if (input == null) { - Console.WriteLine(NoMapsChosenMessage); + Logger.Log(NoMapsChosenMessage); return; } @@ -63,7 +91,7 @@ internal static async Task Main(string[] args) var inputArray = input.Split(','); if (inputArray.Length == 0) { - Console.WriteLine(NoMapsChosenMessage); + Logger.Log(NoMapsChosenMessage); return; } @@ -71,7 +99,7 @@ internal static async Task Main(string[] args) { if (!int.TryParse(idString.Trim(), out var id)) { - Console.WriteLine(ChosenMapIdNotIntMessage(idString)); + Logger.Log(ChosenMapIdNotIntMessage(idString)); return; } @@ -84,29 +112,66 @@ internal static async Task Main(string[] args) { if (id < 0 || id >= mapIds.Length) { - Console.WriteLine(NoMapFoundWithIdMessage(id)); + Logger.Log(NoMapFoundWithIdMessage(id)); return; } selectedMapPrototypes.Add(mapIds[id]); } - arguments.Maps.AddRange(selectedMapPrototypes); - if (selectedMapPrototypes.Count == 0) { - Console.WriteLine(NoMapsChosenMessage); + Logger.Log(NoMapsChosenMessage); return; } - Console.WriteLine($"Selected maps: {string.Join(", ", selectedMapPrototypes)}"); - } + foreach (var protoId in selectedMapPrototypes) + { + Logger.Log($"Processing prototype: {protoId}"); + if (protoManager.TryIndex(protoId, out var proto)) + { + Logger.Log($"Found prototype '{protoId}': IsGrid={proto.IsGrid}, MapPath={proto.MapPath}"); - var maps = new List(); + var treatAsGrid = proto.IsGrid; + try + { + if (!treatAsGrid) + { + var resMgr = pair.Server.ResolveDependency(); + if (IsGridResource(resMgr, proto.MapPath)) + { + Logger.Log($"Detected grid content in resource {proto.MapPath}"); + treatAsGrid = true; + } + } + } + catch (Exception e) + { + Logger.Log($"Error while checking resource {proto.MapPath}: {e.Message}"); + } + + if (treatAsGrid) + { + Logger.Log($"Creating RenderMapGrid for '{protoId}'"); + maps.Add(new RenderMapGrid { FileName = proto.MapPath.ToString() }); + } + else + { + Logger.Log($"Creating RenderMapPrototype for '{protoId}'"); + maps.Add(new RenderMapPrototype { Prototype = protoId }); + } + } + else + { + Logger.Log($"Failed to find prototype: {protoId}"); + } + } - if (arguments.ArgumentsAreFileNames) + Logger.Log($"Selected maps: {string.Join(", ", selectedMapPrototypes)}. Total maps created: {maps.Count}"); + } + else if (arguments.ArgumentsAreFileNames) { - Console.WriteLine("Retrieving maps by file names..."); + Logger.Log("Retrieving maps by file names..."); // // Handle legacy command line processing: @@ -132,7 +197,14 @@ internal static async Task Main(string[] args) { if (File.Exists(map)) { - maps.Add(new RenderMapFile { FileName = map }); + if (IsGridFile(map)) + { + maps.Add(new RenderMapGrid { FileName = map }); + } + else + { + maps.Add(new RenderMapFile { FileName = map }); + } } else { @@ -142,7 +214,7 @@ internal static async Task Main(string[] args) if (lookupPrototypeFiles.Count > 0) { - Console.Write($"Following map files did not exist on disk directly, searching through prototypes: {string.Join(", ", lookupPrototypeFiles)}"); + Logger.Log($"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 @@ -157,12 +229,12 @@ internal static async Task Main(string[] args) if (mapPrototype.MapPath.Filename == toFind) { maps.Add(new RenderMapPrototype { Prototype = mapPrototype, }); - Console.WriteLine($"Found matching map prototype: {mapPrototype.MapName}"); + Logger.Log($"Found matching map prototype: {mapPrototype.MapName}"); goto found; } } - await Console.Error.WriteLineAsync($"Found no map prototype for file '{toFind}'!"); + Logger.Log($"Found no map prototype for file '{toFind}'!"); // was async found: ; } @@ -170,9 +242,31 @@ internal static async Task Main(string[] args) } else { + Logger.Log("Processing prototypes from arguments"); + await using var pair = await PoolManager.GetServerClient(); + var protoManager = pair.Server.ResolveDependency(); + foreach (var map in arguments.Maps) { - maps.Add(new RenderMapPrototype { Prototype = map }); + Logger.Log($"Processing prototype: {map}"); + if (protoManager.TryIndex(map, out var proto)) + { + Logger.Log($"Found prototype '{map}': IsGrid={proto.IsGrid}, MapPath={proto.MapPath}"); + if (proto.IsGrid) + { + Logger.Log($"Creating RenderMapGrid for '{map}'"); + maps.Add(new RenderMapGrid { FileName = proto.MapPath.ToString() }); + } + else + { + Logger.Log($"Creating RenderMapPrototype for '{map}'"); + maps.Add(new RenderMapPrototype { Prototype = map }); + } + } + else + { + Logger.Log($"Failed to find prototype: {map}"); + } } } @@ -185,14 +279,19 @@ private static async Task Run( List toRender, ExternalTestContext testContext) { - Console.WriteLine($"Creating images for {toRender.Count} maps"); + Logger.Log($"Creating images for {toRender.Count} maps"); + Logger.Log($"Map types to render:"); + foreach (var map in toRender) + { + Logger.Log($"{map.GetType().Name}: {map}"); + } var parallaxOutput = arguments.OutputParallax ? new ParallaxOutput(arguments.OutputPath) : null; var mapNames = new List(); foreach (var map in toRender) { - Console.WriteLine($"Painting map {map}"); + Logger.Log($"Painting map {map}"); await using var painter = new MapPainter(map, testContext); await painter.Initialize(); @@ -215,7 +314,7 @@ private static async Task Run( var savePath = $"{directory}{Path.DirectorySeparatorChar}{mapShort}-{i}.{arguments.Format}"; - Console.WriteLine($"Writing grid of size {grid.Width}x{grid.Height} to {savePath}"); + Logger.Log($"Writing grid of size {grid.Width}x{grid.Height} to {savePath}"); switch (arguments.Format) { @@ -244,8 +343,7 @@ private static async Task Run( } catch (Exception ex) { - Console.WriteLine($"Painting map {map} failed due to an internal exception:"); - Console.WriteLine(ex); + Logger.Log($"Painting map {map} failed due to an internal exception:", ex); continue; } @@ -261,14 +359,77 @@ private static async Task Run( } catch (Exception e) { - Console.WriteLine($"Exception while shutting down painter: {e}"); + Logger.Log($"Exception while shutting down painter: {e}"); } } var mapNamesString = $"[{string.Join(',', mapNames.Select(s => $"\"{s}\""))}]"; - Console.WriteLine($@"::set-output name=map_names::{mapNamesString}"); - Console.WriteLine($"Processed {arguments.Maps.Count} maps."); + Logger.Log($@"::set-output name=map_names::{mapNamesString}"); + Logger.Log($"Processed {arguments.Maps.Count} maps."); Console.WriteLine($"It's now safe to manually exit the process (automatic exit in a few moments...)"); } + + private static bool IsGridFile(string filePath) + { + try + { + using (var reader = new StreamReader(filePath)) + { + string? line; + while ((line = reader.ReadLine()) != null) + { + if (line.Contains("meta:")) + { + while ((line = reader.ReadLine()) != null) + { + if (line.Contains("category:")) + return line.Contains("Grid"); + + if (!line.StartsWith(" ") && !string.IsNullOrWhiteSpace(line)) + break; + } + break; + } + } + } + } + catch (Exception ex) + { + Logger.Log($"Error checking if file is grid: {ex.Message}"); + } + + return false; + } + + private static bool IsGridResource(IResourceManager resMgr, ResPath path) + { + try + { + using var stream = resMgr.ContentFileRead(path); + using var reader = new StreamReader(stream); + string? line; + while ((line = reader.ReadLine()) != null) + { + if (line.Contains("meta:")) + { + while ((line = reader.ReadLine()) != null) + { + if (line.Contains("category:")) + return line.Contains("Grid"); + + if (!line.StartsWith(" ") && !string.IsNullOrWhiteSpace(line)) + break; + } + break; + } + } + } + catch + { + return false; + } + + return false; + } } } diff --git a/Content.MapRenderer/RenderMap.cs b/Content.MapRenderer/RenderMap.cs index 4ebf4ee5d4bc..9095bdfaa162 100644 --- a/Content.MapRenderer/RenderMap.cs +++ b/Content.MapRenderer/RenderMap.cs @@ -36,20 +36,18 @@ public override string ToString() } } -/// -/// Specifies a map file on disk that the map renderer should render. -/// -public sealed class RenderMapFile : RenderMap +public abstract class RenderMapFileBase : 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})"; - } + public override string ToString() => $"{GetType().Name}({FileName})"; } + +/// +/// Specifies a map file on disk that the map renderer should render. +/// +public sealed class RenderMapFile : RenderMapFileBase {} + +public sealed class RenderMapGrid : RenderMapFileBase {}