Map Init & Map Loading improvements. (#801)

* MapInit v1, so people can criticize my code.

* Map init v1.

* Improve LocalPlayer to fix aghosting.

* Fix map saving.

* Map command improvements:

Implement loadbp
Made certain commands aware of uninitialized maps.

* Adds IMapManager.GetAllGrids()

* Add lsgrid and lsmap commands.

* MetaData component serialization fixes.

Serialize name and description default as null.
Don't serialize prototype.

* Explicit UID indices in map files.

* Update map format doc again.
This commit is contained in:
Pieter-Jan Briers
2019-04-29 12:50:28 +02:00
committed by GitHub
parent 1b3cc8aba6
commit f4b0b69cbb
13 changed files with 344 additions and 38 deletions

View File

@@ -278,7 +278,7 @@ namespace Robust.Client.Placement
}
}
private void OnEntityAttached(object o, EventArgs eventArgs)
private void OnEntityAttached(EntityAttachedEventArgs eventArgs)
{
// player attached to a new entity, basically disable the editor
Clear();

View File

@@ -26,12 +26,12 @@ namespace Robust.Client.Player
/// <summary>
/// An entity has been attached to the local player.
/// </summary>
public event EventHandler EntityAttached;
public event Action<EntityAttachedEventArgs> EntityAttached;
/// <summary>
/// An entity has been detached from the local player.
/// </summary>
public event EventHandler EntityDetached;
public event Action<EntityDetachedEventArgs> EntityDetached;
/// <summary>
/// Game entity that the local player is controlling. If this is null, the player
@@ -93,7 +93,7 @@ namespace Robust.Client.Player
var transform = ControlledEntity.Transform;
transform.OnMove += OnPlayerMoved;
EntityAttached?.Invoke(this, EventArgs.Empty);
EntityAttached?.Invoke(new EntityAttachedEventArgs(entity));
entity.SendMessage(null, new PlayerAttachedMsg());
// notify ECS Systems
@@ -105,6 +105,7 @@ namespace Robust.Client.Player
/// </summary>
public void DetachEntity()
{
var previous = ControlledEntity;
if (ControlledEntity != null && ControlledEntity.Initialized)
{
ControlledEntity.GetComponent<EyeComponent>().Current = false;
@@ -116,9 +117,13 @@ namespace Robust.Client.Player
// notify ECS Systems
ControlledEntity.EntityManager.RaiseEvent(this, new PlayerAttachSysMessage(null));
}
ControlledEntity = null;
EntityDetached?.Invoke(this, EventArgs.Empty);
if (previous != null)
{
EntityDetached?.Invoke(new EntityDetachedEventArgs(previous));
}
}
private void OnPlayerMoved(object sender, MoveEventArgs args)
@@ -169,4 +174,24 @@ namespace Robust.Client.Player
NewStatus = newStatus;
}
}
public class EntityDetachedEventArgs : EventArgs
{
public EntityDetachedEventArgs(IEntity oldEntity)
{
OldEntity = oldEntity;
}
public IEntity OldEntity { get; }
}
public class EntityAttachedEventArgs : EventArgs
{
public EntityAttachedEventArgs(IEntity newEntity)
{
NewEntity = newEntity;
}
public IEntity NewEntity { get; }
}
}

View File

@@ -1,4 +1,6 @@
using System.Globalization;
using System.Linq;
using System.Text;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Maps;
using Robust.Server.Interfaces.Player;
@@ -14,7 +16,7 @@ namespace Robust.Server.Console.Commands
{
public string Command => "addmap";
public string Description => "Adds a new empty map to the round. If the mapID already exists, this command does nothing.";
public string Help => "addmap <mapID>";
public string Help => "addmap <mapID> [initialize]";
public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
{
@@ -24,10 +26,15 @@ namespace Robust.Server.Console.Commands
var mapId = new MapId(int.Parse(args[0]));
var mapMgr = IoCManager.Resolve<IMapManager>();
var pauseMgr = IoCManager.Resolve<IPauseManager>();
if (!mapMgr.MapExists(mapId))
{
mapMgr.CreateMap(mapId);
if (args.Length >= 2 && args[1] == "false")
{
pauseMgr.AddUninitializedMap(mapId);
}
shell.SendText(player, $"Map with ID {mapId} created.");
return;
}
@@ -82,11 +89,38 @@ namespace Robust.Server.Console.Commands
{
public string Command => "loadbp";
public string Description => "Loads a blueprint from disk into the game.";
public string Help => "loadbp <MapID> <GridID> <Path>";
public string Help => "loadbp <MapID> <Path>";
public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
{
//TODO: Make me work after placement can create new grids.
if (args.Length < 2)
{
return;
}
if (!int.TryParse(args[0], out var intMapId))
{
return;
}
var mapId = new MapId(intMapId);
// no loading into null space
if (mapId == MapId.Nullspace)
{
shell.SendText(player, "Cannot load into nullspace.");
return;
}
var mapManager = IoCManager.Resolve<IMapManager>();
if (!mapManager.TryGetMap(mapId, out var map))
{
shell.SendText(player, "Target map does not exist.");
return;
}
var mapLoader = IoCManager.Resolve<IMapLoader>();
mapLoader.LoadBlueprint(map, args[1]);
}
}
@@ -233,6 +267,7 @@ namespace Robust.Server.Console.Commands
public string Command => "tpgrid";
public string Description => "Teleports a grid to a new location.";
public string Help => "tpgrid <gridId> <X> <Y> [<MapId>]";
public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
{
if (args.Length < 3 || args.Length > 4)
@@ -255,7 +290,89 @@ namespace Robust.Server.Console.Commands
shell.SendText(player, "Grid was teleported.");
}
}
}
internal sealed class RunMapInitCommand : IClientCommand
{
public string Command => "mapinit";
public string Description => default;
public string Help => "mapinit <mapID>";
public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
{
if (args.Length != 1)
{
shell.SendText(player, "Wrong number of args.");
return;
}
var mapManager = IoCManager.Resolve<IMapManager>();
var pauseManager = IoCManager.Resolve<IPauseManager>();
var arg = args[0];
var mapId = new MapId(int.Parse(arg, CultureInfo.InvariantCulture));
if (!mapManager.TryGetMap(mapId, out var map))
{
shell.SendText(player, "Map does not exist!");
return;
}
if (pauseManager.IsMapInitialized(map))
{
shell.SendText(player, "Map is already initialized!");
return;
}
pauseManager.DoMapInitialize(map);
}
}
internal sealed class ListMapsCommand : IClientCommand
{
public string Command => "lsmap";
public string Description => default;
public string Help => "lsmap";
public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
{
var mapManager = IoCManager.Resolve<IMapManager>();
var pauseManager = IoCManager.Resolve<IPauseManager>();
var msg = new StringBuilder();
foreach (var map in mapManager.GetAllMaps().OrderBy(map => map.Index.Value))
{
msg.AppendFormat("{0}: default grid: {1}, init: {2}, paused: {3} , grids: {4}\n",
map.Index, map.DefaultGrid.Index, pauseManager.IsMapInitialized(map),
pauseManager.IsMapPaused(map),
string.Join(",", map.GetAllGrids().Select(grid => grid.Index)));
}
shell.SendText(player, msg.ToString());
}
}
internal sealed class ListGridsCommand : IClientCommand
{
public string Command => "lsgrid";
public string Description => default;
public string Help => "lsgrid";
public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
{
var mapManager = IoCManager.Resolve<IMapManager>();
var msg = new StringBuilder();
foreach (var grid in mapManager.GetAllGrids().OrderBy(grid => grid.Index.Value))
{
msg.AppendFormat("{0}: map: {1}, default: {2}, pos: {3} \n",
grid.Index, grid.ParentMap.Index, grid.IsDefaultGrid, grid.WorldPosition);
}
shell.SendText(player, msg.ToString());
}
}
}

View File

@@ -27,6 +27,7 @@ namespace Robust.Server.GameObjects
{
var newEnt = CreateEntity(protoName);
InitializeAndStartEntity(newEnt);
newEnt.RunMapInit();
return newEnt;
}
@@ -39,6 +40,7 @@ namespace Robust.Server.GameObjects
var result = CreateEntity(entityType);
result.Transform.GridPosition = coordinates;
InitializeAndStartEntity(result);
result.RunMapInit();
entity = result;
return true;
}
@@ -60,6 +62,7 @@ namespace Robust.Server.GameObjects
var entity = CreateEntity(entityType);
entity.Transform.GridPosition = coordinates;
InitializeAndStartEntity(entity);
entity.RunMapInit();
return entity;
}

View File

@@ -0,0 +1,27 @@
using System.Linq;
using Robust.Server.GameObjects.Components;
using Robust.Shared.Interfaces.GameObjects;
namespace Robust.Server.Interfaces.GameObjects
{
/// <summary>
/// Defines a component that has "map initialization" behavior.
/// Basically irreversible behavior that moves the map from "map editor" to playable,
/// like spawning preset objects.
/// </summary>
public interface IMapInit
{
void MapInit();
}
public static class MapInitExt
{
public static void RunMapInit(this IEntity entity)
{
foreach (var init in entity.GetAllComponents<IMapInit>().ToList())
{
init.MapInit();
}
}
}
}

View File

@@ -11,6 +11,15 @@ namespace Robust.Server.Interfaces.Timing
void SetMapPaused(IMap map, bool paused);
void SetMapPaused(MapId mapId, bool paused);
void DoMapInitialize(MapId mapId);
void DoMapInitialize(IMap map);
void DoGridMapInitialize(GridId gridId);
void DoGridMapInitialize(IMapGrid grid);
void AddUninitializedMap(MapId mapId);
void AddUninitializedMap(IMap map);
[Pure]
bool IsMapPaused(IMap map);
@@ -22,6 +31,12 @@ namespace Robust.Server.Interfaces.Timing
[Pure]
bool IsGridPaused(GridId gridId);
[Pure]
bool IsMapInitialized(MapId mapId);
[Pure]
bool IsMapInitialized(IMap map);
}
public static class PauseManagerExt

View File

@@ -15,6 +15,7 @@ using Robust.Shared.GameObjects;
using System.Globalization;
using Robust.Shared.Interfaces.GameObjects;
using System.Linq;
using Robust.Server.Interfaces.Timing;
namespace Robust.Server.Maps
{
@@ -37,12 +38,14 @@ namespace Robust.Server.Maps
[Dependency]
private readonly IServerEntityManagerInternal _serverEntityManager;
[Dependency] private readonly IPauseManager _pauseManager;
/// <inheritdoc />
public void SaveBlueprint(GridId gridId, string yamlPath)
{
var grid = _mapManager.GetGrid(gridId);
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager);
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager);
context.RegisterGrid(grid);
var root = context.Serialize();
var document = new YamlDocument(root);
@@ -90,6 +93,7 @@ namespace Robust.Server.Maps
reader = new StreamReader(file);
}
IMapGrid grid;
using (reader)
{
Logger.InfoS("map", $"Loading Grid: {resPath}");
@@ -101,16 +105,26 @@ namespace Robust.Server.Maps
throw new InvalidDataException("Cannot instance map with multiple grids as blueprint.");
}
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, (YamlMappingNode)data.RootNode, map);
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager, (YamlMappingNode)data.RootNode, map);
context.Deserialize();
return context.Grids[0];
grid = context.Grids[0];
if (!context.MapIsPostInit && _pauseManager.IsMapInitialized(map))
{
foreach (var entity in context.Entities)
{
entity.RunMapInit();
}
}
}
return grid;
}
/// <inheritdoc />
public void SaveMap(IMap map, string yamlPath)
{
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager);
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager);
foreach (var grid in map.GetAllGrids())
{
context.RegisterGrid(grid);
@@ -172,8 +186,16 @@ namespace Robust.Server.Maps
throw new InvalidDataException("Cannot instance map with multiple grids as blueprint.");
}
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, (YamlMappingNode)data.RootNode, _mapManager.GetMap(mapId));
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager, (YamlMappingNode)data.RootNode, _mapManager.GetMap(mapId));
context.Deserialize();
if (!context.MapIsPostInit && _pauseManager.IsMapInitialized(mapId))
{
foreach (var entity in context.Entities)
{
entity.RunMapInit();
}
}
}
}
@@ -185,12 +207,17 @@ namespace Robust.Server.Maps
private readonly IMapManager _mapManager;
private readonly ITileDefinitionManager _tileDefinitionManager;
private readonly IServerEntityManagerInternal _serverEntityManager;
private readonly IPauseManager _pauseManager;
private readonly Dictionary<GridId, int> GridIDMap = new Dictionary<GridId, int>();
public readonly List<IMapGrid> Grids = new List<IMapGrid>();
private readonly Dictionary<EntityUid, int> EntityUidMap = new Dictionary<EntityUid, int>();
private readonly List<IEntity> Entities = new List<IEntity>();
private readonly Dictionary<int, EntityUid> UidEntityMap = new Dictionary<int, EntityUid>();
public readonly List<IEntity> Entities = new List<IEntity>();
private int uidCounter;
private readonly YamlMappingNode RootNode;
private readonly IMap TargetMap;
@@ -202,20 +229,24 @@ namespace Robust.Server.Maps
private Dictionary<ushort, string> _tileMap;
public MapContext(IMapManager maps, ITileDefinitionManager tileDefs, IServerEntityManagerInternal entities)
public bool MapIsPostInit { get; private set; }
public MapContext(IMapManager maps, ITileDefinitionManager tileDefs, IServerEntityManagerInternal entities, IPauseManager pauseManager)
{
_mapManager = maps;
_tileDefinitionManager = tileDefs;
_serverEntityManager = entities;
_pauseManager = pauseManager;
RootNode = new YamlMappingNode();
}
public MapContext(IMapManager maps, ITileDefinitionManager tileDefs, IServerEntityManagerInternal entities, YamlMappingNode node, IMap targetMap)
public MapContext(IMapManager maps, ITileDefinitionManager tileDefs, IServerEntityManagerInternal entities, IPauseManager pauseManager, YamlMappingNode node, IMap targetMap)
{
_mapManager = maps;
_tileDefinitionManager = tileDefs;
_serverEntityManager = entities;
_pauseManager = pauseManager;
RootNode = node;
TargetMap = targetMap;
@@ -253,6 +284,15 @@ namespace Robust.Server.Maps
{
throw new InvalidDataException("Cannot handle this map file version.");
}
if (meta.TryGetNode("postmapinit", out var mapInitNode))
{
MapIsPostInit = mapInitNode.AsBool();
}
else
{
MapIsPostInit = true;
}
}
private void ReadTileMapSection()
@@ -297,12 +337,14 @@ namespace Robust.Server.Maps
foreach (var entityDef in entities.Cast<YamlMappingNode>())
{
var type = entityDef.GetNode("type").AsString();
var uid = Entities.Count;
if (entityDef.TryGetNode("uid", out var uidNode))
{
uid = uidNode.AsInt();
}
var entity = _serverEntityManager.AllocEntity(type);
Entities.Add(entity);
if (entityDef.TryGetNode("name", out var nameNode))
{
entity.Name = nameNode.AsString();
}
UidEntityMap.Add(uid, entity.Uid);
}
}
@@ -373,6 +415,18 @@ namespace Robust.Server.Maps
// TODO: Make these values configurable.
meta.Add("name", "DemoStation");
meta.Add("author", "Space-Wizards");
var isPostInit = false;
foreach (var grid in Grids)
{
if (_pauseManager.IsMapInitialized(grid.ParentMap))
{
isPostInit = true;
break;
}
}
meta.Add("postmapinit", isPostInit ? "true" : "false");
}
private void WriteTileMapSection()
@@ -403,7 +457,8 @@ namespace Robust.Server.Maps
{
if (IsMapSavable(entity))
{
EntityUidMap.Add(entity.Uid, EntityUidMap.Count);
var uid = uidCounter++;
EntityUidMap.Add(entity.Uid, uid);
Entities.Add(entity);
}
}
@@ -417,13 +472,11 @@ namespace Robust.Server.Maps
foreach (var entity in Entities)
{
CurrentWritingEntity = entity;
var mapping = new YamlMappingNode();
mapping.Add("type", entity.Prototype.ID);
if (entity.Name != entity.Prototype.Name)
var mapping = new YamlMappingNode
{
// TODO: This shouldn't be hardcoded.
mapping.Add("name", entity.Prototype.Name);
}
{"type", entity.Prototype.ID},
{"uid", EntityUidMap[entity.Uid].ToString(CultureInfo.InvariantCulture)}
};
var components = new YamlSequenceNode();
// See engine#636 for why the Distinct() call.
@@ -477,7 +530,7 @@ namespace Robust.Server.Maps
}
else
{
obj = Entities[val].Uid;
obj = UidEntityMap[val];
return true;
}
}

View File

@@ -125,6 +125,7 @@
<Compile Include="GameObjects\Components\UserInterface\ServerUserInterfaceComponent.cs" />
<Compile Include="GameObjects\EntitySystems\InputSystem.cs" />
<Compile Include="GameObjects\EntitySystems\UserInterfaceSystem.cs" />
<Compile Include="Interfaces\GameObjects\IMapInit.cs" />
<Compile Include="Interfaces\Player\IPlayerData.cs" />
<Compile Include="Interfaces\ServerStatus\IStatusHost.cs" />
<Compile Include="Interfaces\Timing\IPauseManager.cs" />

View File

@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using Robust.Server.Interfaces.GameObjects;
using Robust.Server.Interfaces.Timing;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
@@ -8,14 +10,15 @@ using Robust.Shared.ViewVariables;
namespace Robust.Server.Timing
{
public class PauseManager : IPauseManager, IPostInjectInit
internal sealed class PauseManager : IPauseManager, IPostInjectInit
{
[Dependency] private IMapManager _mapManager;
[Dependency] private IEntityManager _entityManager;
[ViewVariables] private readonly HashSet<MapId> _pausedMaps = new HashSet<MapId>();
[ViewVariables] private readonly HashSet<MapId> _unInitializedMaps = new HashSet<MapId>();
public void SetMapPaused(IMap map, bool paused) => SetMapPaused(map.Index, paused);
public void SetMapPaused(MapId mapId, bool paused)
{
if (paused)
@@ -28,6 +31,47 @@ namespace Robust.Server.Timing
}
}
public void DoMapInitialize(IMap map) => DoMapInitialize(map.Index);
public void DoMapInitialize(MapId mapId)
{
if (IsMapInitialized(mapId))
{
throw new ArgumentException("That map is already initialized.");
}
_unInitializedMaps.Remove(mapId);
foreach (var entity in _entityManager.GetEntities())
{
if (entity.Transform.MapID != mapId)
{
continue;
}
entity.RunMapInit();
}
}
public void DoGridMapInitialize(IMapGrid grid) => DoGridMapInitialize(grid.Index);
public void DoGridMapInitialize(GridId gridId)
{
foreach (var entity in _entityManager.GetEntities())
{
if (entity.Transform.GridID != gridId)
{
continue;
}
entity.RunMapInit();
}
}
public void AddUninitializedMap(IMap map) => AddUninitializedMap(map.Index);
public void AddUninitializedMap(MapId mapId)
{
_unInitializedMaps.Add(mapId);
}
public bool IsMapPaused(IMap map) => IsMapPaused(map.Index);
public bool IsMapPaused(MapId mapId) => _pausedMaps.Contains(mapId);
public bool IsGridPaused(IMapGrid grid) => _pausedMaps.Contains(grid.ParentMapId);
@@ -38,9 +82,19 @@ namespace Robust.Server.Timing
return IsGridPaused(grid);
}
public bool IsMapInitialized(IMap map) => IsMapInitialized(map.Index);
public bool IsMapInitialized(MapId mapId)
{
return !_unInitializedMaps.Contains(mapId);
}
public void PostInject()
{
_mapManager.MapDestroyed += (sender, args) => _pausedMaps.Remove(args.Map.Index);
_mapManager.MapDestroyed += (sender, args) =>
{
_pausedMaps.Remove(args.Map.Index);
_unInitializedMaps.Add(args.Map.Index);
};
}
}
}

View File

@@ -164,11 +164,11 @@ namespace Robust.Shared.GameObjects
{
base.ExposeData(serializer);
serializer.DataField(ref _entityName, "name", string.Empty);
serializer.DataField(ref _entityDescription, "desc", string.Empty);
serializer.DataField(ref _entityPrototype, "proto", null,
s => _prototypes.Index<EntityPrototype>(s),
p => p.ID);
serializer.DataField(ref _entityName, "name", null);
serializer.DataField(ref _entityDescription, "desc", null);
//serializer.DataField(ref _entityPrototype, "proto", null,
// s => _prototypes.Index<EntityPrototype>(s),
// p => p.ID);
}
}
}

View File

@@ -28,6 +28,8 @@ namespace Robust.Shared.Interfaces.Map
IEnumerable<IMap> GetAllMaps();
IEnumerable<IMapGrid> GetAllGrids();
/// <summary>
/// Should the OnTileChanged event be suppressed? This is useful for initially loading the map
/// so that you don't spam an event for each of the million station tiles.

View File

@@ -186,6 +186,11 @@ namespace Robust.Shared.Map
return _maps.Values;
}
public IEnumerable<IMapGrid> GetAllGrids()
{
return _grids.Values;
}
public IMapGrid CreateGrid(MapId currentMapID, GridId? gridID = null, ushort chunkSize = 16, float snapSize = 1)
{
var map = _maps[currentMapID];

View File

@@ -19,6 +19,9 @@ Fields:
* `format`: Version identifier. **The current version is `2`.** Can be used to bail out early for unsupported map files.
* `name`: A name. Simple huh. Can be left out.
* `author`: Authorship info. Also simple. Can be left out.
* `postmapinit`: Whether this map is "post map init". This means that presets such as procedural generation have applied.
In general, maps touched only via map editing mode will have this false. Maps saved mid game will not.
Default value is true if left out.
### The `tilemap` section
@@ -42,7 +45,8 @@ Contains data for all the grids. The section is an ordered sequence. Each sequen
Contains data for all entities on the map. Just like grids these are stored in an indexed list, and an entity declaration is pretty much just like a prototype.
Each entity has a `type` field which specifies which prototype it is, and the components list works as overrides in the same way as entity parenting.
Each entity has a `type` field which specifies which prototype it is, and the components list works as overrides in the same way as prototype parenting.
Each entity also has a numerical `uid` field, which is used to give this entity an unique identifier when referenced by other entities.
#### Chunk Data
@@ -84,7 +88,7 @@ Direct hard `IEntity` references are stored as entity UID, it is simply decoded
In-game `EntityUid` instances are either:
* Serialized as YAML `null` if the entity referenced to is not included in the map saving. If it's on a different grid, for example.
* An integer representing the index in the `entities` section corresponding to the serialized entity.
* An integer representing the `uid` of the serialized entity.
### Grid IDs