Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 198 additions & 0 deletions Content.Server/_LuaM/Mapping/LenientGridLoaderSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
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<string> _pendingMissing = new();

private readonly HashSet<string> _substituted = new();

public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BeforeEntityReadEvent>(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<MapGridComponent>? 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<string, int> 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<string, int> CollectMissingPrototypes(MappingDataNode data)
{
var missing = new Dictionary<string, int>();
var version = data.Get<MappingDataNode>("meta").Get<ValueDataNode>("format").AsInt();
var key = version >= 4 ? "proto" : "type";

foreach (var node in data.Get<SequenceDataNode>("entities").Cast<MappingDataNode>())
{
if (!node.TryGet<ValueDataNode>(key, out var protoNode) || string.IsNullOrWhiteSpace(protoNode.Value))
continue;

if (_proto.HasIndex<EntityPrototype>(protoNode.Value))
continue;

var count = version >= 4 && node.TryGet<SequenceDataNode>("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<EntityUid>();
foreach (var uid in result.Entities)
{
if (!TerminatingOrDeleted(uid) && MetaData(uid).EntityPrototype?.ID == Placeholder)
placeholders.Add(uid);
}

var children = new List<EntityUid>();
foreach (var uid in placeholders)
{
if (TerminatingOrDeleted(uid))
continue;

if (TryComp<ContainerManagerComponent>(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<string, int> Missing = new();

public int Removed;
public int Rescued;
}
129 changes: 129 additions & 0 deletions Content.Server/_LuaM/Mapping/LoadGridLenientCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
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;

[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<SharedMapSystem>();
if (!mapSystem.MapExists(mapId))
{
shell.WriteLine(_loc.GetString("cmd-loadgrid_lenient-map-created", ("map", intMapId)));
mapSystem.CreateMap(mapId, false);
}

var loader = _entManager.System<LenientGridLoaderSystem>();
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);
}
}
11 changes: 11 additions & 0 deletions Resources/Locale/en-US/_LuaM/commands/loadgrid_lenient.ftl
Original file line number Diff line number Diff line change
@@ -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 <MapID> <Path> [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 }.
11 changes: 11 additions & 0 deletions Resources/Locale/ru-RU/_LuaM/commands/loadgrid_lenient.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
cmd-loadgrid_lenient-desc = Загружает грид из файла, пропуская сущности, прототипов которых нет в этом билде. Их содержимое выкладывается рядом.
cmd-loadgrid_lenient-help = loadgrid_lenient <MapID> <Path> [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 }.
Original file line number Diff line number Diff line change
@@ -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
Loading