mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 11:40:52 +01:00
* New Type Serializers * Delete NetCoordinatesSerializer.cs * Make EntityCoordinates and MapCoordinates use DataRecord * Turn them into actual record structs I'm somewhat surprised the DataRecord attribute doesn't check this * Allocate MapIds before deserializing components * Deserialize preallocated ids * fix map merge assert * remove old * Use TryGetMap * release notes
316 lines
11 KiB
C#
316 lines
11 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 file, taking in a raw byte stream.
|
|
/// </summary>
|
|
/// <param name="file">The file contents to load from.</param>
|
|
/// <param name="fileName">
|
|
/// The name of the file being loaded. This is used purely for logging/informational purposes.
|
|
/// </param>
|
|
/// <param name="result">The result of the load operation.</param>
|
|
/// <param name="options">Options for the load operation.</param>
|
|
/// <returns>True if the load succeeded, false otherwise.</returns>
|
|
/// <seealso cref="M:Robust.Shared.EntitySerialization.Systems.MapLoaderSystem.TryLoadGeneric(Robust.Shared.Utility.ResPath,Robust.Shared.EntitySerialization.LoadResult@,System.Nullable{Robust.Shared.EntitySerialization.MapLoadOptions})"/>
|
|
public bool TryLoadGeneric(
|
|
Stream file,
|
|
string fileName,
|
|
[NotNullWhen(true)] out LoadResult? result,
|
|
MapLoadOptions? options = null)
|
|
{
|
|
result = null;
|
|
|
|
if (!TryReadFile(new StreamReader(file), out var data))
|
|
return false;
|
|
|
|
return TryLoadGeneric(data, fileName, out result, options);
|
|
}
|
|
|
|
/// <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, out var data))
|
|
return false;
|
|
|
|
return TryLoadGeneric(data, file.ToString(), out result, options);
|
|
}
|
|
|
|
private bool TryLoadGeneric(
|
|
MappingDataNode data,
|
|
string fileName,
|
|
[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 {fileName}");
|
|
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 {fileName} 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 {fileName}: {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 {fileName} 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 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 path,
|
|
[NotNullWhen(true)] out Entity<TransformComponent>? entity,
|
|
DeserializationOptions? options = null)
|
|
{
|
|
var opts = new MapLoadOptions
|
|
{
|
|
DeserializationOptions = options ?? DeserializationOptions.Default,
|
|
ExpectedCategory = FileCategory.Entity
|
|
};
|
|
|
|
entity = null;
|
|
if (!TryLoadGeneric(path, 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 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 path,
|
|
[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(path, 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 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 path,
|
|
[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, path, 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);
|
|
}
|
|
}
|
|
}
|