From 3a1dcf6f1ad9de5cfc18b343a66c8f2857a34eb5 Mon Sep 17 00:00:00 2001 From: C_Quarian Date: Mon, 14 Sep 2026 04:51:45 +0400 Subject: [PATCH 1/3] loadgrid_lenient --- .../_LuaM/Mapping/LenientGridLoaderSystem.cs | 208 ++++++++++++++++++ .../_LuaM/Mapping/LoadGridLenientCommand.cs | 135 ++++++++++++ .../en-US/_LuaM/commands/loadgrid_lenient.ftl | 11 + .../ru-RU/_LuaM/commands/loadgrid_lenient.ftl | 11 + .../_LuaM/Entities/Markers/lenient_load.yml | 8 + 5 files changed, 373 insertions(+) create mode 100644 Content.Server/_LuaM/Mapping/LenientGridLoaderSystem.cs create mode 100644 Content.Server/_LuaM/Mapping/LoadGridLenientCommand.cs create mode 100644 Resources/Locale/en-US/_LuaM/commands/loadgrid_lenient.ftl create mode 100644 Resources/Locale/ru-RU/_LuaM/commands/loadgrid_lenient.ftl create mode 100644 Resources/Prototypes/_LuaM/Entities/Markers/lenient_load.yml diff --git a/Content.Server/_LuaM/Mapping/LenientGridLoaderSystem.cs b/Content.Server/_LuaM/Mapping/LenientGridLoaderSystem.cs new file mode 100644 index 00000000000..0ad626eb887 --- /dev/null +++ b/Content.Server/_LuaM/Mapping/LenientGridLoaderSystem.cs @@ -0,0 +1,208 @@ +// LuaCorp - This file is licensed under AGPLv3 +// Copyright (c) 2026 LuaCorp +// See AGPLv3.txt for details. + +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Numerics; +using Content.Server.Holiday; +using Content.Server.Maps; +using Robust.Shared.Containers; +using Robust.Shared.EntitySerialization; +using Robust.Shared.EntitySerialization.Systems; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Map.Events; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Markdown.Mapping; +using Robust.Shared.Serialization.Markdown.Sequence; +using Robust.Shared.Serialization.Markdown.Value; +using Robust.Shared.Utility; + +namespace Content.Server._LuaM.Mapping; + +public sealed class LenientGridLoaderSystem : EntitySystem +{ + [Dependency] private IPrototypeManager _proto = default!; + [Dependency] private MapLoaderSystem _mapLoader = default!; + [Dependency] private SharedContainerSystem _container = default!; + [Dependency] private SharedTransformSystem _transform = default!; + + public const string Placeholder = "LuaMLenientLoadPlaceholder"; + + private readonly HashSet _pendingMissing = new(); + + private readonly HashSet _substituted = new(); + + public override void Initialize() + { + base.Initialize(); + // -- Строго после MapMigrationSystem: она добавляет ключи через Dictionary.Add и упадёт на дубликате + SubscribeLocalEvent(OnBeforeEntityRead, + after: new[] { typeof(MapMigrationSystem), typeof(HolidaySystem) }); + } + + private void OnBeforeEntityRead(BeforeEntityReadEvent ev) + { + foreach (var id in _pendingMissing) + { + if (ev.DeletedPrototypes.Contains(id) || ev.RenamedPrototypes.ContainsKey(id)) + continue; + + if (ev.RenamedPrototypes.TryAdd(id, Placeholder)) + _substituted.Add(id); + } + } + + public bool TryLoadGrid( + MapId map, + ResPath path, + DeserializationOptions options, + Vector2 offset, + Angle rotation, + [NotNullWhen(true)] out Entity? grid, + out LenientLoadReport report, + out string? error) + { + grid = null; + report = new LenientLoadReport(); + error = null; + + if (!_mapLoader.TryReadFile(path, out var data)) + { + error = "file"; + return false; + } + + Dictionary missing; + try + { + missing = CollectMissingPrototypes(data); + } + catch (Exception e) + { + Log.Error($"Failed to read entities of {path}: {e}"); + error = "format"; + return false; + } + + var opts = new MapLoadOptions + { + MergeMap = map, + Offset = offset, + Rotation = rotation, + DeserializationOptions = options, + ExpectedCategory = FileCategory.Grid, + }; + + LoadResult? result; + _substituted.Clear(); + _pendingMissing.UnionWith(missing.Keys); + try + { + if (!_mapLoader.TryLoadGeneric(data, path.ToString(), out result, opts)) + { + error = "load"; + return false; + } + } + finally + { + // -- Очищать всегда, иначе подмена прототипов утечёт во все последующие загрузки карт и шаттлов + _pendingMissing.Clear(); + } + + foreach (var id in _substituted) + { + report.Missing[id] = missing[id]; + } + _substituted.Clear(); + + if (result.Grids.Count != 1) + { + _mapLoader.Delete(result); + error = "grids"; + return false; + } + + ReplacePlaceholders(result, report); + grid = result.Grids.Single(); + return true; + } + + // -- Проверяются только прототипы сущностей. Отсутствующий тайл по-прежнему ломает загрузку грида + private Dictionary CollectMissingPrototypes(MappingDataNode data) + { + var missing = new Dictionary(); + var version = data.Get("meta").Get("format").AsInt(); + var key = version >= 4 ? "proto" : "type"; + + foreach (var node in data.Get("entities").Cast()) + { + if (!node.TryGet(key, out var protoNode) || string.IsNullOrWhiteSpace(protoNode.Value)) + continue; + + if (_proto.HasIndex(protoNode.Value)) + continue; + + var count = version >= 4 && node.TryGet("entities", out var group) ? group.Count : 1; + missing[protoNode.Value] = missing.GetValueOrDefault(protoNode.Value) + count; + } + + return missing; + } + + private void ReplacePlaceholders(LoadResult result, LenientLoadReport report) + { + var placeholders = new List(); + foreach (var uid in result.Entities) + { + if (!TerminatingOrDeleted(uid) && MetaData(uid).EntityPrototype?.ID == Placeholder) + placeholders.Add(uid); + } + + var children = new List(); + foreach (var uid in placeholders) + { + if (TerminatingOrDeleted(uid)) + continue; + + if (TryComp(uid, out var manager)) + { + foreach (var container in manager.Containers.Values.ToArray()) + { + foreach (var contained in _container.EmptyContainer(container, force: true)) + { + _transform.DropNextTo(contained, uid); + report.Rescued++; + } + } + } + + children.Clear(); + var enumerator = Transform(uid).ChildEnumerator; + while (enumerator.MoveNext(out var child)) + { + children.Add(child); + } + + foreach (var child in children) + { + _transform.DropNextTo(child, uid); + report.Rescued++; + } + + // -- Удалять только после выгрузки: движок удаляет сущность вместе со всем содержимым + Del(uid); + report.Removed++; + } + } +} + +public sealed class LenientLoadReport +{ + public readonly Dictionary Missing = new(); + + public int Removed; + public int Rescued; +} diff --git a/Content.Server/_LuaM/Mapping/LoadGridLenientCommand.cs b/Content.Server/_LuaM/Mapping/LoadGridLenientCommand.cs new file mode 100644 index 00000000000..4ab6be8fdd5 --- /dev/null +++ b/Content.Server/_LuaM/Mapping/LoadGridLenientCommand.cs @@ -0,0 +1,135 @@ +// LuaCorp - This file is licensed under AGPLv3 +// Copyright (c) 2026 LuaCorp +// See AGPLv3.txt for details. + +using System.Globalization; +using System.Linq; +using System.Numerics; +using Content.Server.Administration; +using Content.Server.Administration.Logs; +using Content.Shared.Administration; +using Content.Shared.Database; +using Robust.Server.Console.Commands; +using Robust.Shared.Console; +using Robust.Shared.ContentPack; +using Robust.Shared.EntitySerialization; +using Robust.Shared.Map; +using Robust.Shared.Utility; + +namespace Content.Server._LuaM.Mapping; + +// -- Тот же флаг MAPPING, что у loadgrid. Не ослаблять: команда создаёт сущности на сервере +[AdminCommand(AdminFlags.Mapping)] +public sealed partial class LoadGridLenientCommand : IConsoleCommand +{ + [Dependency] private IEntityManager _entManager = default!; + [Dependency] private IResourceManager _resource = default!; + [Dependency] private ILocalizationManager _loc = default!; + [Dependency] private IAdminLogManager _adminLogger = default!; + + public string Command => "loadgrid_lenient"; + public string Description => _loc.GetString("cmd-loadgrid_lenient-desc"); + public string Help => _loc.GetString("cmd-loadgrid_lenient-help"); + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length < 2 || args.Length == 3 || args.Length > 6) + { + shell.WriteError(Help); + return; + } + + if (!int.TryParse(args[0], out var intMapId)) + { + shell.WriteError(_loc.GetString("cmd-loadgrid_lenient-bad-map", ("value", args[0]))); + return; + } + + var mapId = new MapId(intMapId); + if (mapId == MapId.Nullspace) + { + shell.WriteError(_loc.GetString("cmd-loadgrid_lenient-nullspace")); + return; + } + + var path = new ResPath(args[1]); + // -- Запрет выхода из папки данных сервера через ".." + if (path.EnumerateSegments().Any(segment => segment == "..")) + { + shell.WriteError(_loc.GetString("cmd-loadgrid_lenient-bad-path")); + return; + } + + var offset = Vector2.Zero; + if (args.Length >= 4) + { + if (!float.TryParse(args[2], NumberStyles.Float, CultureInfo.InvariantCulture, out var x) || + !float.TryParse(args[3], NumberStyles.Float, CultureInfo.InvariantCulture, out var y)) + { + shell.WriteError(_loc.GetString("cmd-loadgrid_lenient-bad-float")); + return; + } + + offset = new Vector2(x, y); + } + + var rotation = Angle.Zero; + if (args.Length >= 5) + { + if (!float.TryParse(args[4], NumberStyles.Float, CultureInfo.InvariantCulture, out var degrees)) + { + shell.WriteError(_loc.GetString("cmd-loadgrid_lenient-bad-float")); + return; + } + + rotation = Angle.FromDegrees(degrees); + } + + var options = DeserializationOptions.Default; + if (args.Length >= 6) + { + if (!bool.TryParse(args[5], out var storeUids)) + { + shell.WriteError(_loc.GetString("cmd-loadgrid_lenient-bad-bool", ("value", args[5]))); + return; + } + + options.StoreYamlUids = storeUids; + } + + var mapSystem = _entManager.System(); + if (!mapSystem.MapExists(mapId)) + { + shell.WriteLine(_loc.GetString("cmd-loadgrid_lenient-map-created", ("map", intMapId))); + mapSystem.CreateMap(mapId, false); + } + + var loader = _entManager.System(); + if (!loader.TryLoadGrid(mapId, path, options, offset, rotation, out var grid, out var report, out var error)) + { + shell.WriteError(_loc.GetString("cmd-loadgrid_lenient-failed", ("reason", error ?? "load"))); + return; + } + + foreach (var (id, count) in report.Missing.OrderBy(pair => pair.Key)) + { + shell.WriteLine(_loc.GetString("cmd-loadgrid_lenient-missing-entry", ("id", id), ("count", count))); + } + + shell.WriteLine(_loc.GetString("cmd-loadgrid_lenient-success", + ("grid", _entManager.GetNetEntity(grid.Value.Owner)), + ("types", report.Missing.Count), + ("removed", report.Removed), + ("rescued", report.Rescued))); + + _adminLogger.Add(LogType.Action, + LogImpact.High, + $"{shell.Player?.Name ?? "server console"} loaded grid {path} onto map {intMapId} with loadgrid_lenient: " + + $"{_entManager.ToPrettyString(grid.Value.Owner)}, skipped prototypes: {string.Join(", ", report.Missing.Keys)}"); + } + + public CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + return LoadMap.GetCompletionResult(shell, args, _resource, _loc); + } +} diff --git a/Resources/Locale/en-US/_LuaM/commands/loadgrid_lenient.ftl b/Resources/Locale/en-US/_LuaM/commands/loadgrid_lenient.ftl new file mode 100644 index 00000000000..d8c25e5e220 --- /dev/null +++ b/Resources/Locale/en-US/_LuaM/commands/loadgrid_lenient.ftl @@ -0,0 +1,11 @@ +cmd-loadgrid_lenient-desc = Loads a grid from a file, skipping entities whose prototypes this build does not have. Their contents are dropped next to them. +cmd-loadgrid_lenient-help = loadgrid_lenient [x y] [rotation] [storeUids] +cmd-loadgrid_lenient-bad-map = { $value } is not a valid map ID. +cmd-loadgrid_lenient-nullspace = Cannot load into nullspace. +cmd-loadgrid_lenient-bad-path = The path cannot contain "..". +cmd-loadgrid_lenient-bad-float = Coordinates and rotation must be numbers. +cmd-loadgrid_lenient-bad-bool = { $value } is not true/false. +cmd-loadgrid_lenient-map-created = Map { $map } did not exist, created without map init. +cmd-loadgrid_lenient-failed = Failed to load the grid ({ $reason }). See the server log for details. +cmd-loadgrid_lenient-missing-entry = Skipped prototype { $id } (entities: { $count }) +cmd-loadgrid_lenient-success = Grid { $grid } loaded. Skipped types: { $types }, removed entities: { $removed }, rescued items: { $rescued }. diff --git a/Resources/Locale/ru-RU/_LuaM/commands/loadgrid_lenient.ftl b/Resources/Locale/ru-RU/_LuaM/commands/loadgrid_lenient.ftl new file mode 100644 index 00000000000..3006c107643 --- /dev/null +++ b/Resources/Locale/ru-RU/_LuaM/commands/loadgrid_lenient.ftl @@ -0,0 +1,11 @@ +cmd-loadgrid_lenient-desc = Загружает грид из файла, пропуская сущности, прототипов которых нет в этом билде. Их содержимое выкладывается рядом. +cmd-loadgrid_lenient-help = loadgrid_lenient [x y] [вращение] [storeUids] +cmd-loadgrid_lenient-bad-map = { $value } не является корректным ID карты. +cmd-loadgrid_lenient-nullspace = Нельзя загружать в nullspace. +cmd-loadgrid_lenient-bad-path = Путь не может содержать "..". +cmd-loadgrid_lenient-bad-float = Координаты и вращение должны быть числами. +cmd-loadgrid_lenient-bad-bool = { $value } не является true/false. +cmd-loadgrid_lenient-map-created = Карта { $map } не существовала, создана без инициализации. +cmd-loadgrid_lenient-failed = Не удалось загрузить грид ({ $reason }). Подробности в логе сервера. +cmd-loadgrid_lenient-missing-entry = Пропущен прототип { $id } (сущностей: { $count }) +cmd-loadgrid_lenient-success = Грид { $grid } загружен. Пропущено типов: { $types }, удалено сущностей: { $removed }, спасено предметов: { $rescued }. diff --git a/Resources/Prototypes/_LuaM/Entities/Markers/lenient_load.yml b/Resources/Prototypes/_LuaM/Entities/Markers/lenient_load.yml new file mode 100644 index 00000000000..16b608f77c6 --- /dev/null +++ b/Resources/Prototypes/_LuaM/Entities/Markers/lenient_load.yml @@ -0,0 +1,8 @@ +# Stand-in for entities whose prototype is missing when a grid is loaded with loadgrid_lenient. +# Its contents are dropped next to it and it is deleted right after the load. +- type: entity + id: LuaMLenientLoadPlaceholder + name: missing prototype placeholder + categories: [ HideSpawnMenu ] + components: + - type: Transform From 601abc9eb6ab5286764780109da268361e537287 Mon Sep 17 00:00:00 2001 From: Pcol Date: Tue, 15 Sep 2026 13:53:44 +0300 Subject: [PATCH 2/3] Update LoadGridLenientCommand.cs --- Content.Server/_LuaM/Mapping/LoadGridLenientCommand.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Content.Server/_LuaM/Mapping/LoadGridLenientCommand.cs b/Content.Server/_LuaM/Mapping/LoadGridLenientCommand.cs index 4ab6be8fdd5..b4182fb57ac 100644 --- a/Content.Server/_LuaM/Mapping/LoadGridLenientCommand.cs +++ b/Content.Server/_LuaM/Mapping/LoadGridLenientCommand.cs @@ -1,7 +1,3 @@ -// LuaCorp - This file is licensed under AGPLv3 -// Copyright (c) 2026 LuaCorp -// See AGPLv3.txt for details. - using System.Globalization; using System.Linq; using System.Numerics; @@ -18,7 +14,6 @@ namespace Content.Server._LuaM.Mapping; -// -- Тот же флаг MAPPING, что у loadgrid. Не ослаблять: команда создаёт сущности на сервере [AdminCommand(AdminFlags.Mapping)] public sealed partial class LoadGridLenientCommand : IConsoleCommand { @@ -53,7 +48,6 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) } var path = new ResPath(args[1]); - // -- Запрет выхода из папки данных сервера через ".." if (path.EnumerateSegments().Any(segment => segment == "..")) { shell.WriteError(_loc.GetString("cmd-loadgrid_lenient-bad-path")); From cb2773d8c9c6c4fbd82de82b630fb6b42c37ea7a Mon Sep 17 00:00:00 2001 From: Pcol Date: Tue, 15 Sep 2026 13:54:13 +0300 Subject: [PATCH 3/3] Update LenientGridLoaderSystem.cs --- .../_LuaM/Mapping/LenientGridLoaderSystem.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Content.Server/_LuaM/Mapping/LenientGridLoaderSystem.cs b/Content.Server/_LuaM/Mapping/LenientGridLoaderSystem.cs index 0ad626eb887..0f903f3b227 100644 --- a/Content.Server/_LuaM/Mapping/LenientGridLoaderSystem.cs +++ b/Content.Server/_LuaM/Mapping/LenientGridLoaderSystem.cs @@ -1,7 +1,3 @@ -// LuaCorp - This file is licensed under AGPLv3 -// Copyright (c) 2026 LuaCorp -// See AGPLv3.txt for details. - using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; @@ -37,7 +33,6 @@ public sealed class LenientGridLoaderSystem : EntitySystem public override void Initialize() { base.Initialize(); - // -- Строго после MapMigrationSystem: она добавляет ключи через Dictionary.Add и упадёт на дубликате SubscribeLocalEvent(OnBeforeEntityRead, after: new[] { typeof(MapMigrationSystem), typeof(HolidaySystem) }); } @@ -108,7 +103,6 @@ public bool TryLoadGrid( } finally { - // -- Очищать всегда, иначе подмена прототипов утечёт во все последующие загрузки карт и шаттлов _pendingMissing.Clear(); } @@ -129,8 +123,6 @@ public bool TryLoadGrid( grid = result.Grids.Single(); return true; } - - // -- Проверяются только прототипы сущностей. Отсутствующий тайл по-прежнему ломает загрузку грида private Dictionary CollectMissingPrototypes(MappingDataNode data) { var missing = new Dictionary(); @@ -191,8 +183,6 @@ private void ReplacePlaceholders(LoadResult result, LenientLoadReport report) _transform.DropNextTo(child, uid); report.Rescued++; } - - // -- Удалять только после выгрузки: движок удаляет сущность вместе со всем содержимым Del(uid); report.Removed++; }