mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
417 lines
15 KiB
C#
417 lines
15 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
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.Serialization.Markdown.Mapping;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Robust.Shared.EntitySerialization.Systems;
|
|
|
|
// This partial class file contains methods for loading generic entities and grids. Map specific methods are in another
|
|
// file
|
|
public sealed partial class MapLoaderSystem
|
|
{
|
|
/// <summary>
|
|
/// Tries to load entities from a YAML file. Whenever possible, you should try to use <see cref="TryLoadMap"/>,
|
|
/// <see cref="TryLoadGrid"/>, or <see cref="TryLoadEntity"/> instead.
|
|
/// </summary>
|
|
public bool TryLoadGeneric(
|
|
ResPath file,
|
|
[NotNullWhen(true)] out HashSet<Entity<MapComponent>>? maps,
|
|
[NotNullWhen(true)] out HashSet<Entity<MapGridComponent>>? grids,
|
|
MapLoadOptions? options = null)
|
|
{
|
|
grids = null;
|
|
maps = null;
|
|
|
|
if (!TryLoadGeneric(file, out var data, options))
|
|
return false;
|
|
|
|
maps = data.Maps;
|
|
grids = data.Grids;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to load entities from a YAML text stream. Whenever possible, you should try to use <see cref="TryLoadMap"/>,
|
|
/// <see cref="TryLoadGrid"/>, or <see cref="TryLoadEntity"/> instead.
|
|
/// </summary>
|
|
public bool TryLoadGeneric(
|
|
TextReader reader,
|
|
string source,
|
|
[NotNullWhen(true)] out HashSet<Entity<MapComponent>>? maps,
|
|
[NotNullWhen(true)] out HashSet<Entity<MapGridComponent>>? grids,
|
|
MapLoadOptions? options = null)
|
|
{
|
|
grids = null;
|
|
maps = null;
|
|
if (!TryLoadGeneric(reader, source, out var data, options))
|
|
return false;
|
|
|
|
maps = data.Maps;
|
|
grids = data.Grids;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to load entities from a YAML file. Whenever possible, you should try to use <see cref="TryLoadMap"/>,
|
|
/// <see cref="TryLoadGrid"/>, or <see cref="TryLoadEntity"/> instead.
|
|
/// </summary>
|
|
/// <param name="file">The file to load.</param>
|
|
/// <param name="result">Data class containing information about the loaded entities</param>
|
|
/// <param name="options">Optional Options for configuring loading behaviour.</param>
|
|
public bool TryLoadGeneric(ResPath file, [NotNullWhen(true)] out LoadResult? result, MapLoadOptions? options = null)
|
|
{
|
|
result = null;
|
|
|
|
if (!TryReadFile(file.ToRootedPath(), out var data))
|
|
return false;
|
|
|
|
return TryLoadGeneric(data, file.ToString(), out result, options);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to load entities from a YAML text stream. Whenever possible, you should try to use <see cref="TryLoadMap"/>,
|
|
/// <see cref="TryLoadGrid"/>, or <see cref="TryLoadEntity"/> instead.
|
|
/// </summary>
|
|
/// <param name="reader">The text to load.</param>
|
|
/// <param name="source">The name of the source, if any. This should be your file path (for example)</param>
|
|
/// <param name="result">Data class containing information about the loaded entities</param>
|
|
/// <param name="options">Optional Options for configuring loading behaviour.</param>
|
|
public bool TryLoadGeneric(TextReader reader, string source, [NotNullWhen(true)] out LoadResult? result, MapLoadOptions? options = null)
|
|
{
|
|
result = null;
|
|
|
|
if (!TryReadFile(reader, out var data))
|
|
return false;
|
|
|
|
return TryLoadGeneric(data, source, out result, options);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to load entities from a YAML text stream. Whenever possible, you should try to use <see cref="TryLoadMap"/>,
|
|
/// <see cref="TryLoadGrid"/>, or <see cref="TryLoadEntity"/> instead.
|
|
/// </summary>
|
|
/// <param name="stream">The stream containing the text to load.</param>
|
|
/// <param name="source">The name of the source, if any. This should be your file path (for example)</param>
|
|
/// <param name="result">Data class containing information about the loaded entities</param>
|
|
/// <param name="options">Optional Options for configuring loading behaviour.</param>
|
|
public bool TryLoadGeneric(Stream stream, string source, [NotNullWhen(true)] out LoadResult? result, MapLoadOptions? options = null)
|
|
{
|
|
result = null;
|
|
|
|
if (!TryReadFile(new StreamReader(stream, leaveOpen: true), out var data))
|
|
return false;
|
|
|
|
return TryLoadGeneric(data, source, out result, options);
|
|
}
|
|
|
|
public bool TryLoadGeneric(
|
|
MappingDataNode data,
|
|
string source,
|
|
[NotNullWhen(true)] out LoadResult? result,
|
|
MapLoadOptions? options = null)
|
|
{
|
|
result = null;
|
|
|
|
_stopwatch.Restart();
|
|
var ev = new BeforeEntityReadEvent();
|
|
RaiseLocalEvent(ev);
|
|
|
|
var opts = options ?? MapLoadOptions.Default;
|
|
|
|
// If we are forcing a map id, we cannot auto-assign ids.
|
|
opts.DeserializationOptions.AssignMapIds = opts.ForceMapId == null;
|
|
|
|
if (opts.MergeMap is { } targetId && !_mapSystem.MapExists(targetId))
|
|
throw new Exception($"Target map {targetId} does not exist");
|
|
|
|
if (opts.MergeMap != null && opts.ForceMapId != null)
|
|
throw new Exception($"Invalid combination of MapLoadOptions");
|
|
|
|
if (_mapSystem.MapExists(opts.ForceMapId))
|
|
throw new Exception($"Target map already exists");
|
|
|
|
// Using a local deserializer instead of a cached value, both to ensure that we don't accidentally carry over
|
|
// data from a previous serializations, and because some entities cause other maps/grids to be loaded during
|
|
// mapinit.
|
|
var deserializer = new EntityDeserializer(
|
|
_dependency,
|
|
data,
|
|
opts.DeserializationOptions,
|
|
ev.RenamedPrototypes,
|
|
ev.DeletedPrototypes);
|
|
|
|
if (!deserializer.TryProcessData())
|
|
{
|
|
Log.Debug($"Failed to process entity data in {source}");
|
|
return false;
|
|
}
|
|
|
|
// If the file isn't of the expected category, stop before we ever create any entities.
|
|
if (opts.ExpectedCategory is { } expected
|
|
&& expected != deserializer.Result.Category
|
|
&& deserializer.Result.Category != FileCategory.Unknown)
|
|
{
|
|
// Did someone try to load a map file as a grid or vice versa?
|
|
Log.Error($"Map {source} does not contain the expected data. Expected {expected} but got {deserializer.Result.Category}");
|
|
Delete(deserializer.Result);
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
deserializer.CreateEntities();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Error($"Caught exception while creating entities for map {source}: {e}");
|
|
Delete(deserializer.Result);
|
|
throw;
|
|
}
|
|
|
|
// If the map file was an older version, the category has to be inferred from the file's contents in CreateEntities()
|
|
// Hence the category is checked again here.
|
|
if (opts.ExpectedCategory is { } exp && exp != deserializer.Result.Category)
|
|
{
|
|
// Did someone try to load a map file as a grid or vice versa?
|
|
Log.Error($"Map {source} does not contain the expected data. Expected {exp} but got {deserializer.Result.Category}");
|
|
Delete(deserializer.Result);
|
|
return false;
|
|
}
|
|
|
|
// Reparent entities if loading entities onto an existing map.
|
|
var merged = new HashSet<EntityUid>();
|
|
MergeMaps(deserializer, opts, merged);
|
|
|
|
if (!SetMapId(deserializer, opts))
|
|
return false;
|
|
|
|
// Apply any offsets & rotations specified by the load options
|
|
ApplyTransform(deserializer, opts);
|
|
|
|
try
|
|
{
|
|
deserializer.StartEntities();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Error($"Caught exception while starting entities: {e}");
|
|
Delete(deserializer.Result);
|
|
throw;
|
|
}
|
|
|
|
if (opts.MergeMap is {} map)
|
|
MapInitalizeMerged(merged, map);
|
|
|
|
result = deserializer.Result;
|
|
Log.Debug($"Loaded map in {_stopwatch.Elapsed}");
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to load a regular (non-map, non-grid) entity from a YAML file.
|
|
/// The loaded entity will initially be in null-space.
|
|
/// If the file does not contain exactly one orphaned entity, this will return false and delete loaded entities.
|
|
/// </summary>
|
|
public bool TryLoadEntity(
|
|
ResPath file,
|
|
[NotNullWhen(true)] out Entity<TransformComponent>? entity,
|
|
DeserializationOptions? options = null)
|
|
{
|
|
entity = null;
|
|
if (!TryGetReader(file.ToRootedPath(), out var reader))
|
|
return false;
|
|
|
|
using (reader)
|
|
{
|
|
return TryLoadEntity(reader, file.ToString(), out entity, options);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to load a regular (non-map, non-grid) entity from a YAML text stream.
|
|
/// The loaded entity will initially be in null-space.
|
|
/// If the file does not contain exactly one orphaned entity, this will return false and delete loaded entities.
|
|
/// </summary>
|
|
public bool TryLoadEntity(
|
|
TextReader reader,
|
|
string source,
|
|
[NotNullWhen(true)] out Entity<TransformComponent>? entity,
|
|
DeserializationOptions? options = null)
|
|
{
|
|
var opts = new MapLoadOptions
|
|
{
|
|
DeserializationOptions = options ?? DeserializationOptions.Default,
|
|
ExpectedCategory = FileCategory.Entity
|
|
};
|
|
|
|
entity = null;
|
|
if (!TryLoadGeneric(reader, source, out var result, opts))
|
|
return false;
|
|
|
|
if (result.Orphans.Count == 1)
|
|
{
|
|
var uid = result.Orphans.Single();
|
|
entity = (uid, Transform(uid));
|
|
return true;
|
|
}
|
|
|
|
Delete(result);
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to load a grid entity from a YAML file and parent it to the given map.
|
|
/// If the file does not contain exactly one grid, this will return false and delete loaded entities.
|
|
/// </summary>
|
|
public bool TryLoadGrid(
|
|
MapId map,
|
|
ResPath file,
|
|
[NotNullWhen(true)] out Entity<MapGridComponent>? grid,
|
|
DeserializationOptions? options = null,
|
|
Vector2 offset = default,
|
|
Angle rot = default)
|
|
{
|
|
grid = null;
|
|
if (!TryGetReader(file.ToRootedPath(), out var reader))
|
|
return false;
|
|
|
|
using (reader)
|
|
{
|
|
return TryLoadGrid(map, reader, file.ToString(), out grid, options, offset, rot);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to load a grid entity from a YAML text stream and parent it to the given map.
|
|
/// If the file does not contain exactly one grid, this will return false and delete loaded entities.
|
|
/// </summary>
|
|
public bool TryLoadGrid(
|
|
MapId map,
|
|
TextReader reader,
|
|
string source,
|
|
[NotNullWhen(true)] out Entity<MapGridComponent>? grid,
|
|
DeserializationOptions? options = null,
|
|
Vector2 offset = default,
|
|
Angle rot = default)
|
|
{
|
|
var opts = new MapLoadOptions
|
|
{
|
|
MergeMap = map,
|
|
Offset = offset,
|
|
Rotation = rot,
|
|
DeserializationOptions = options ?? DeserializationOptions.Default,
|
|
ExpectedCategory = FileCategory.Grid
|
|
};
|
|
|
|
grid = null;
|
|
if (!TryLoadGeneric(reader, source, out var result, opts))
|
|
return false;
|
|
|
|
if (result.Grids.Count == 1)
|
|
{
|
|
grid = result.Grids.Single();
|
|
return true;
|
|
}
|
|
|
|
Delete(result);
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to load a grid entity from a YAML file and parent it to a newly created map.
|
|
/// If the file does not contain exactly one grid, this will return false and delete loaded entities.
|
|
/// </summary>
|
|
public bool TryLoadGrid(
|
|
ResPath file,
|
|
[NotNullWhen(true)] out Entity<MapComponent>? map,
|
|
[NotNullWhen(true)] out Entity<MapGridComponent>? grid,
|
|
DeserializationOptions? options = null,
|
|
Vector2 offset = default,
|
|
Angle rot = default)
|
|
{
|
|
grid = null;
|
|
map = null;
|
|
if (!TryGetReader(file.ToRootedPath(), out var reader))
|
|
return false;
|
|
|
|
using (reader)
|
|
{
|
|
return TryLoadGrid(reader, file.ToString(), out map, out grid, options, offset, rot);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to load a grid entity from a YAML text stream and parent it to a newly created map.
|
|
/// If the file does not contain exactly one grid, this will return false and delete loaded entities.
|
|
/// </summary>
|
|
public bool TryLoadGrid(
|
|
TextReader reader,
|
|
string source,
|
|
[NotNullWhen(true)] out Entity<MapComponent>? map,
|
|
[NotNullWhen(true)] out Entity<MapGridComponent>? grid,
|
|
DeserializationOptions? options = null,
|
|
Vector2 offset = default,
|
|
Angle rot = default)
|
|
{
|
|
var opts = options ?? DeserializationOptions.Default;
|
|
|
|
var mapUid = _mapSystem.CreateMap(out var mapId, runMapInit: opts.InitializeMaps);
|
|
if (opts.PauseMaps)
|
|
_mapSystem.SetPaused(mapUid, true);
|
|
|
|
if (!TryLoadGrid(mapId, reader, source, out grid, options, offset, rot))
|
|
{
|
|
Del(mapUid);
|
|
map = null;
|
|
return false;
|
|
}
|
|
|
|
map = new(mapUid, Comp<MapComponent>(mapUid));
|
|
return true;
|
|
}
|
|
|
|
private void ApplyTransform(EntityDeserializer deserializer, MapLoadOptions opts)
|
|
{
|
|
if (opts.Rotation == Angle.Zero && opts.Offset == Vector2.Zero)
|
|
return;
|
|
|
|
// If merging onto a single map, the transformation was already applied by SwapRootNode()
|
|
if (opts.MergeMap != null)
|
|
return;
|
|
|
|
var matrix = Matrix3Helpers.CreateTransform(opts.Offset, opts.Rotation);
|
|
|
|
// We want to apply the transforms to all children of any loaded maps. However, we can't just iterate over the
|
|
// children of loaded maps, as transform component has not yet been initialized. I.e. xform.Children is empty.
|
|
// Hence we iterate over all entities and check which ones are attached to maps.
|
|
foreach (var uid in deserializer.Result.Entities)
|
|
{
|
|
var xform = Transform(uid);
|
|
|
|
if (!_mapQuery.HasComp(xform.ParentUid))
|
|
continue;
|
|
|
|
// The original comment around this bit of logic was just:
|
|
// > Smelly
|
|
// I don't know what sloth meant by that, but I guess applying transforms to grid-maps is a no-no?
|
|
// Or more generally, loading a mapgrid onto another (potentially non-mapgrid) map is just generally kind of weird.
|
|
if (_gridQuery.HasComponent(xform.ParentUid))
|
|
continue;
|
|
|
|
var rot = xform.LocalRotation + opts.Rotation;
|
|
var pos = Vector2.Transform(xform.LocalPosition, matrix);
|
|
_xform.SetLocalPositionRotation(uid, pos, rot, xform);
|
|
DebugTools.Assert(!xform.NoLocalRotation || xform.LocalRotation == 0);
|
|
}
|
|
}
|
|
}
|