Files
RobustToolbox/Robust.Client/Placement/PlacementManager.cs
Paul Ritter 80f9f24243 Serialization v3 aka constant suffering (#1606)
* oops

* fixes serialization il

* copytest

* typo & misc fixes

* 139 moment

* boxing

* mesa dum

* stuff

* goodbye bad friend

* last commit before the big (4) rewrite

* adds datanodes

* kills yamlobjserializer in favor of the new system

* adds more serializers, actually implements them & removes most of the last of the old system

* changed yamlfieldattribute namespace

* adds back iselfserialize

* refactors consts&flags

* renames everything to data(field/definition)

* adds afterserialization

* help

* dataclassgen

* fuggen help me mannen

* Fix most errors on content

* Fix engine errors except map loader

* maploader & misc fix

* misc fixes

* thing

* help

* refactors datanodes

* help me mannen

* Separate ITypeSerializer into reader and writer

* Convert all type serializers

* priority

* adds alot

* il fixes

* adds robustgen

* argh

* adds array & enum serialization

* fixes dataclasses

* adds vec2i / misc fixes

* fixes inheritance

* a very notcursed todo

* fixes some custom dataclasses

* push dis

* Remove data classes

* boutta box

* yes

* Add angle and regex serializer tests

* Make TypeSerializerTest abstract

* sets up ioc etc

* remove pushinheritance

* fixes

* Merge fixes, fix yaml hot reloading

* General fixes2

* Make enum serialization ignore case

* Fix the tag not being copied in data nodes

* Fix not properly serializing flag enums

* Fix component serialization on startup

* Implement ValueDataNode ToString

* Serialization IL fixes, fix return and string equality

* Remove async from prototype manager

* Make serializing unsupported node as enum exception more descriptive

* Fix serv3 tryread casting to serializer instead of reader

* Add constructor for invalid node type exception

* Temporary fix for SERV3: Turn populate delegate into regular code

* Fix not copying the data of non primitive types

* Fix not using the data definition found in copying

* Make ISerializationHooks require explicit implementations

* Add test for serialization inheritance

* Improve IsOverridenIn method

* Fix error message when a data definition is null

* Add method to cast a read value in Serv3Manager

* Rename IServ3Manager to ISerializationManager

* Rename usages of serv3manager, add generic copy method

* Fix IL copy method lookup

* Rename old usages of serv3manager

* Add ITypeCopier

* resistance is futile

* we will conquer this codebase

* Add copy method to all serializers

* Make primitive mismatch error message more descriptive

* bing bong im going to freacking heck

* oopsie moment

* hello are you interested in my wares

* does generic serializers under new architecture

* Convert every non generic serializer to the new format, general fixes

* Update usgaes of generic serializers, cleanup

* does some pushinheritance logic

* finishes pushinheritance FRAMEWORK

* shed

* Add box2, color and component registry serializer tests

* Create more deserialized types and store prototypes with their deserialized results

* Fixes and serializer updates

* Add serialization manager extensions

* adds pushinheritance

* Update all prototypes to have a parent and have consistent id/parent properties

* Fix grammar component serialization

* Add generic serializer tests

* thonk

* Add array serializer test

* Replace logger warning calls with exceptions

* fixes

* Move redundant methods to serialization manager extensions, cleanup

* Add array serialization

* fixes context

* more fixes

* argh

* inheritance

* this should do it

* fixes

* adds copiers & fixes some stuff

* copiers use context v1

* finishing copy context

* more context fixes

* Test fixes

* funky maps

* Fix server user interface component serialization

* Fix value tuple serialization

* Add copying for value types and arrays. Fix copy internal for primitives, enums and strings

* fixes

* fixes more stuff

* yes

* Make abstract/interface skips debugs instead of warnings

* Fix typo

* Make some dictionaries readonly

* Add checks for the serialization manager initializing and already being initialized

* Add base type required and usage for MeansDataDefinition and ImplicitDataDefinitionForInheritorsAttribute

* copy by ref

* Fix exception wording

* Update data field required summary with the new forbidden docs

* Use extension in map loader

* wanna erp

* Change serializing to not use il temporarily

* Make writing work with nullable types

* pushing

* check

* cuddling slaps HARD

* Add serialization priority test

* important fix

* a serialization thing

* serializer moment

* Add validation for some type serializers

* adds context

* moar context

* fixes

* Do the thing for appearance

* yoo lmao

* push haha pp

* Temporarily make copy delegate regular c# code

* Create deserialized component registry to handle not inheriting conflicting references

* YAML LINTER BABY

* ayes

* Fix sprite component norot not being default true like in latest master

* Remove redundant todos

* Add summary doc to every ISerializationManager method

* icon fixes

* Add skip hook argument to readers and copiers

* Merge fixes

* Fix ordering of arguments in read and copy reflection call

* Fix user interface components deserialization

* pew pew

* i am going to HECK

* Add MustUseReturnValue to copy-over methods

* Make serialization log calls use the same sawmill

* gamin

* Fix doc errors in ISerializationManager.cs

* goodbye brave soldier

* fixes

* WIP merge fixes and entity serialization

* aaaaaaaaaaaaaaa

* aaaaaaaaaaaaaaa

* adds inheritancebehaviour

* test/datafield fixes

* forgot that one

* adds more verbose validation

* This fixes the YAML hot reloading

* Replace yield break with Enumerable.Empty

* adds copiers

* aaaaaaaaaaaaa

* array fix
priority fix
misc fixes

* fix(?)

* fix.

* funny map serialization (wip)

* funny map serialization (wip)

* Add TODO

* adds proper info the validation

* Make yaml linter 5 times faster (~80% less execution time)

* Improves the error message for missing fields in the linter

* Include component name in unknown component type error node

* adds alwaysrelevant usa

* fixes mapsaving

* moved surpressor to analyzers proj

* warning cleanup & moves surpressor

* removes old msbuild targets

* Revert "Make yaml linter 5 times faster (~80% less execution time)"

This reverts commit 2ee4cc2c26.

* Add serialization to RobustServerSimulation and mock reflection methods
Fixes container tests

* Fix nullability warnings

* Improve yaml linter message feedback

* oops moment

* Add IEquatable, IComparable, ToString and operators to DataPosition
Rename it to NodeMark
Make it a readonly struct

* Remove try catch from enum parsing

* Make dependency management in serialization less bad

* Make dependencies an argument instead of a property on the serialization manager

* Clean up type serializers

* Improve validation messages and resourc epath checking

* Fix sprite error message

* reached perfection

Co-authored-by: Paul <ritter.paul1+git@googlemail.com>
Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>
Co-authored-by: Vera Aguilera Puerto <zddm@outlook.es>
2021-03-04 15:59:14 -08:00

716 lines
24 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.Placement
{
public partial class PlacementManager : IPlacementManager, IDisposable
{
[Dependency] public readonly IPhysicsManager PhysicsManager = default!;
[Dependency] private readonly IClientNetManager NetworkManager = default!;
[Dependency] public readonly IPlayerManager PlayerManager = default!;
[Dependency] public readonly IResourceCache ResourceCache = default!;
[Dependency] private readonly IReflectionManager ReflectionManager = default!;
[Dependency] public readonly IMapManager MapManager = default!;
[Dependency] private readonly IGameTiming _time = default!;
[Dependency] public readonly IEyeManager eyeManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] public readonly IEntityManager EntityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IBaseClient _baseClient = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] public readonly IClyde _clyde = default!;
/// <summary>
/// How long before a pending tile change is dropped.
/// </summary>
private static readonly TimeSpan _pendingTileTimeout = TimeSpan.FromSeconds(2.0);
/// <summary>
/// Dictionary of all placement mode types
/// </summary>
private readonly Dictionary<string, Type> _modeDictionary = new();
private readonly List<Tuple<EntityCoordinates, TimeSpan>> _pendingTileChanges = new();
/// <summary>
/// Tells this system to try to handle placement of an entity during the next frame
/// </summary>
private bool _placenextframe;
/// <summary>
/// Allows various types of placement as singular, line, or grid placement where placement mode allows this type of placement
/// </summary>
public PlacementTypes PlacementType { get; set; }
/// <summary>
/// Holds the anchor that we can try to spawn in a line or a grid from
/// </summary>
public EntityCoordinates StartPoint { get; set; }
/// <summary>
/// Whether the placement manager is currently in a mode where it accepts actions
/// </summary>
public bool IsActive
{
get => _isActive;
private set
{
_isActive = value;
SwitchEditorContext(value);
}
}
/// <summary>
/// Determines whether we are using the mode to delete an entity on click
/// </summary>
public bool Eraser { get; private set; }
/// <summary>
/// Holds the selection rectangle for the eraser
/// </summary>
public Box2? EraserRect { get; set; }
/// <summary>
/// Drawing shader for drawing without being affected by lighting
/// </summary>
private ShaderInstance? _drawingShader { get; set; }
/// <summary>
/// The texture we use to show from our placement manager to represent the entity to place
/// </summary>
public List<IDirectionalTextureProvider>? CurrentTextures { get; set; }
/// <summary>
/// Which of the placement orientations we are trying to place with
/// </summary>
public PlacementMode? CurrentMode { get; set; }
public PlacementInformation? CurrentPermission { get; set; }
public PlacementHijack? Hijack { get; private set; }
private EntityPrototype? _currentPrototype;
/// <summary>
/// The prototype of the entity we are going to spawn on click
/// </summary>
public EntityPrototype? CurrentPrototype
{
get => _currentPrototype;
set
{
_currentPrototype = value;
if (value != null)
{
PlacementOffset = value.PlacementOffset;
}
_colliderAABB = new Box2(0f, 0f, 0f, 0f);
}
}
public Vector2i PlacementOffset { get; set; }
private Box2 _colliderAABB = new(0f, 0f, 0f, 0f);
/// <summary>
/// The box which certain placement modes collision checks will be done against
/// </summary>
public Box2 ColliderAABB
{
get => _colliderAABB;
set => _colliderAABB = value;
}
/// <summary>
/// The directional to spawn the entity in
/// </summary>
public Direction Direction { get; set; } = Direction.South;
private PlacementOverlay _drawOverlay = default!;
private bool _isActive;
public void Initialize()
{
_drawingShader = _prototypeManager.Index<ShaderPrototype>("unshaded").Instance();
NetworkManager.RegisterNetMessage<MsgPlacement>(MsgPlacement.NAME, HandlePlacementMessage);
_modeDictionary.Clear();
foreach (var type in ReflectionManager.GetAllChildren<PlacementMode>())
{
_modeDictionary.Add(type.Name, type);
}
MapManager.TileChanged += HandleTileChanged;
_drawOverlay = new PlacementOverlay(this);
_overlayManager.AddOverlay(_drawOverlay);
// a bit ugly, oh well
_baseClient.PlayerJoinedServer += (sender, args) => SetupInput();
_baseClient.PlayerLeaveServer += (sender, args) => TearDownInput();
}
private void SetupInput()
{
CommandBinds.Builder
.Bind(EngineKeyFunctions.EditorLinePlace, InputCmdHandler.FromDelegate(
session =>
{
if (IsActive && !Eraser) ActivateLineMode();
}))
.Bind(EngineKeyFunctions.EditorGridPlace, InputCmdHandler.FromDelegate(
session =>
{
if (IsActive)
{
if (Eraser)
{
EraseRectMode();
}
else
{
ActivateGridMode();
}
}
}))
.Bind(EngineKeyFunctions.EditorPlaceObject, new PointerStateInputCmdHandler(
(session, coords, uid) =>
{
if (!IsActive)
return false;
if (EraserRect.HasValue)
{
HandleRectDeletion(StartPoint, EraserRect.Value);
EraserRect = null;
return true;
}
if (Eraser)
{
if (HandleDeletion(coords))
return true;
if (uid == EntityUid.Invalid)
{
return false;
}
HandleDeletion(EntityManager.GetEntity(uid));
}
else
{
_placenextframe = true;
}
return true;
},
(session, coords, uid) =>
{
if (!IsActive || Eraser || !_placenextframe)
return false;
//Places objects for non-tile entities
if (!CurrentPermission!.IsTile)
HandlePlacement();
_placenextframe = false;
return true;
}))
.Bind(EngineKeyFunctions.EditorRotateObject, InputCmdHandler.FromDelegate(
session =>
{
if (IsActive && !Eraser) Rotate();
}))
.Bind(EngineKeyFunctions.EditorCancelPlace, InputCmdHandler.FromDelegate(
session =>
{
if (!IsActive)
return;
if (DeactivateSpecialPlacement())
return;
Clear();
}))
.Register<PlacementManager>();
var localPlayer = PlayerManager.LocalPlayer;
localPlayer!.EntityAttached += OnEntityAttached;
}
private void TearDownInput()
{
CommandBinds.Unregister<PlacementManager>();
if (PlayerManager.LocalPlayer != null)
{
PlayerManager.LocalPlayer.EntityAttached -= OnEntityAttached;
}
}
private void OnEntityAttached(EntityAttachedEventArgs eventArgs)
{
// player attached to a new entity, basically disable the editor
Clear();
}
private void SwitchEditorContext(bool enabled)
{
if (enabled)
{
_inputManager.Contexts.SetActiveContext("editor");
}
else
{
_entitySystemManager.GetEntitySystem<InputSystem>().SetEntityContextActive();
}
}
public void Dispose()
{
_drawOverlay?.Dispose();
}
private void HandlePlacementMessage(MsgPlacement msg)
{
switch (msg.PlaceType)
{
case PlacementManagerMessage.StartPlacement:
HandleStartPlacement(msg);
break;
case PlacementManagerMessage.CancelPlacement:
Clear();
break;
}
}
private void HandleTileChanged(object? sender, TileChangedEventArgs args)
{
var coords = MapManager.GetGrid(args.NewTile.GridIndex).GridTileToLocal(args.NewTile.GridIndices);
_pendingTileChanges.RemoveAll(c => c.Item1 == coords);
}
/// <inheritdoc />
public event EventHandler? PlacementChanged;
public void Clear()
{
PlacementChanged?.Invoke(this, EventArgs.Empty);
Hijack = null;
CurrentTextures = null;
CurrentPrototype = null;
CurrentPermission = null;
CurrentMode = null;
DeactivateSpecialPlacement();
_placenextframe = false;
IsActive = false;
Eraser = false;
EraserRect = null;
PlacementOffset = Vector2i.Zero;
}
public void Rotate()
{
if (Hijack != null && !Hijack.CanRotate)
return;
switch (Direction)
{
case Direction.North:
Direction = Direction.East;
break;
case Direction.East:
Direction = Direction.South;
break;
case Direction.South:
Direction = Direction.West;
break;
case Direction.West:
Direction = Direction.North;
break;
}
CurrentMode?.SetSprite();
}
public void HandlePlacement()
{
if (!IsActive || Eraser)
return;
switch (PlacementType)
{
case PlacementTypes.None:
RequestPlacement(CurrentMode!.MouseCoords);
break;
case PlacementTypes.Line:
foreach (var coordinate in CurrentMode!.LineCoordinates())
{
RequestPlacement(coordinate);
}
DeactivateSpecialPlacement();
break;
case PlacementTypes.Grid:
foreach (var coordinate in CurrentMode!.GridCoordinates())
{
RequestPlacement(coordinate);
}
DeactivateSpecialPlacement();
break;
}
}
public bool HandleDeletion(EntityCoordinates coordinates)
{
if (!IsActive || !Eraser) return false;
if (Hijack != null)
return Hijack.HijackDeletion(coordinates);
return false;
}
public void HandleDeletion(IEntity entity)
{
if (!IsActive || !Eraser) return;
if (Hijack != null && Hijack.HijackDeletion(entity)) return;
var msg = NetworkManager.CreateNetMessage<MsgPlacement>();
msg.PlaceType = PlacementManagerMessage.RequestEntRemove;
msg.EntityUid = entity.Uid;
NetworkManager.ClientSendMessage(msg);
}
public void HandleRectDeletion(EntityCoordinates start, Box2 rect)
{
var msg = NetworkManager.CreateNetMessage<MsgPlacement>();
msg.PlaceType = PlacementManagerMessage.RequestRectRemove;
msg.EntityCoordinates = new EntityCoordinates(StartPoint.EntityId, rect.BottomLeft);
msg.RectSize = rect.Size;
NetworkManager.ClientSendMessage(msg);
}
public void ToggleEraser()
{
if (!Eraser && !IsActive)
{
IsActive = true;
Eraser = true;
}
else Clear();
}
public void ToggleEraserHijacked(PlacementHijack hijack)
{
if (!Eraser && !IsActive)
{
IsActive = true;
Eraser = true;
Hijack = hijack;
}
else Clear();
}
public void BeginPlacing(PlacementInformation info, PlacementHijack? hijack = null)
{
BeginHijackedPlacing(info, hijack);
}
public void BeginHijackedPlacing(PlacementInformation info, PlacementHijack? hijack = null)
{
Clear();
CurrentPermission = info;
if (!_modeDictionary.Any(pair => pair.Key.Equals(CurrentPermission.PlacementOption)))
{
Clear();
return;
}
var modeType = _modeDictionary.First(pair => pair.Key.Equals(CurrentPermission.PlacementOption)).Value;
CurrentMode = (PlacementMode) Activator.CreateInstance(modeType, this)!;
if (hijack != null)
{
Hijack = hijack;
Hijack.StartHijack(this);
IsActive = true;
return;
}
if (info.IsTile)
PreparePlacementTile();
else
PreparePlacement(info.EntityType!);
}
private bool CurrentMousePosition(out ScreenCoordinates coordinates)
{
// Try to get current map.
var map = MapId.Nullspace;
var ent = PlayerManager.LocalPlayer!.ControlledEntity;
if (ent != null)
{
map = ent.Transform.MapID;
}
if (map == MapId.Nullspace || CurrentPermission == null || CurrentMode == null)
{
coordinates = new ScreenCoordinates(Vector2.Zero);
return false;
}
coordinates = new ScreenCoordinates(_inputManager.MouseScreenPosition);
return true;
}
private bool CurrentEraserMouseCoordinates(out EntityCoordinates coordinates)
{
var ent = PlayerManager.LocalPlayer?.ControlledEntity;
if (ent == null)
{
coordinates = new EntityCoordinates();
return false;
}
else
{
var map = ent.Transform.MapID;
if (map == MapId.Nullspace || !Eraser)
{
coordinates = new EntityCoordinates();
return false;
}
coordinates = EntityCoordinates.FromMap(ent.EntityManager, MapManager,
eyeManager.ScreenToMap(new ScreenCoordinates(_inputManager.MouseScreenPosition)));
return true;
}
}
/// <inheritdoc />
public void FrameUpdate(FrameEventArgs e)
{
if (!CurrentMousePosition(out var mouseScreen))
{
if (EraserRect.HasValue)
{
if (!CurrentEraserMouseCoordinates(out EntityCoordinates end))
return;
float b, l, t, r;
if (StartPoint.X < end.X)
{
l = StartPoint.X;
r = end.X;
}
else
{
l = end.X;
r = StartPoint.X;
}
if (StartPoint.Y < end.Y)
{
b = StartPoint.Y;
t = end.Y;
}
else
{
b = end.Y;
t = StartPoint.Y;
}
EraserRect = new Box2(l, b, r, t);
}
return;
}
CurrentMode!.AlignPlacementMode(mouseScreen);
// purge old unapproved tile changes
_pendingTileChanges.RemoveAll(c => c.Item2 < _time.RealTime);
// continues tile placement but placement of entities only occurs on mouseUp
if (_placenextframe && CurrentPermission!.IsTile)
HandlePlacement();
}
private void ActivateLineMode()
{
if (!CurrentMode!.HasLineMode)
return;
if (!CurrentMousePosition(out var mouseScreen))
return;
CurrentMode.AlignPlacementMode(mouseScreen);
StartPoint = CurrentMode.MouseCoords;
PlacementType = PlacementTypes.Line;
}
private void ActivateGridMode()
{
if (!CurrentMode!.HasGridMode)
return;
if (!CurrentMousePosition(out var mouseScreen))
return;
CurrentMode.AlignPlacementMode(mouseScreen);
StartPoint = CurrentMode.MouseCoords;
PlacementType = PlacementTypes.Grid;
}
private void EraseRectMode()
{
if (!CurrentEraserMouseCoordinates(out EntityCoordinates coordinates))
return;
StartPoint = coordinates;
EraserRect = new Box2(coordinates.Position, Vector2.Zero);
}
private bool DeactivateSpecialPlacement()
{
if (PlacementType == PlacementTypes.None)
return false;
PlacementType = PlacementTypes.None;
return true;
}
private void Render(DrawingHandleWorld handle)
{
if (CurrentMode == null || !IsActive)
{
if (EraserRect.HasValue)
{
handle.UseShader(_drawingShader);
handle.DrawRect(EraserRect.Value, new Color(255, 0, 0, 50));
}
return;
}
CurrentMode.Render(handle);
if (CurrentPermission == null || CurrentPermission.Range <= 0 || !CurrentMode.RangeRequired
|| PlayerManager.LocalPlayer?.ControlledEntity == null)
return;
var worldPos = PlayerManager.LocalPlayer.ControlledEntity.Transform.WorldPosition;
handle.DrawCircle(worldPos, CurrentPermission.Range, new Color(1, 1, 1, 0.25f));
}
private void HandleStartPlacement(MsgPlacement msg)
{
CurrentPermission = new PlacementInformation
{
Range = msg.Range,
IsTile = msg.IsTile,
};
CurrentPermission.EntityType = msg.ObjType; // tile or ent type
CurrentPermission.PlacementOption = msg.AlignOption;
BeginPlacing(CurrentPermission);
}
private void PreparePlacement(string templateName)
{
var prototype = _prototypeManager.Index<EntityPrototype>(templateName);
CurrentTextures = SpriteComponent.GetPrototypeTextures(prototype, ResourceCache).ToList();
CurrentPrototype = prototype;
IsActive = true;
}
private void PreparePlacementTile()
{
CurrentTextures = new List<IDirectionalTextureProvider>
{ResourceCache
.GetResource<TextureResource>(new ResourcePath("/Textures/UserInterface/tilebuildoverlay.png")).Texture};
IsActive = true;
}
private void RequestPlacement(EntityCoordinates coordinates)
{
if (CurrentPermission == null) return;
if (!CurrentMode!.IsValidPosition(coordinates)) return;
if (Hijack != null && Hijack.HijackPlacementRequest(coordinates)) return;
if (CurrentPermission.IsTile)
{
var gridId = coordinates.GetGridId(EntityManager);
// If we have actually placed something on a valid grid...
if (gridId.IsValid())
{
var grid = MapManager.GetGrid(gridId);
// no point changing the tile to the same thing.
if (grid.GetTileRef(coordinates).Tile.TypeId == CurrentPermission.TileType)
return;
}
foreach (var tileChange in _pendingTileChanges)
{
// if change already pending, ignore it
if (tileChange.Item1 == coordinates)
return;
}
var tuple = new Tuple<EntityCoordinates, TimeSpan>(coordinates, _time.RealTime + _pendingTileTimeout);
_pendingTileChanges.Add(tuple);
}
var message = NetworkManager.CreateNetMessage<MsgPlacement>();
message.PlaceType = PlacementManagerMessage.RequestPlacement;
message.Align = CurrentMode.ModeName;
message.IsTile = CurrentPermission.IsTile;
if (CurrentPermission.IsTile)
message.TileType = CurrentPermission.TileType;
else
message.EntityTemplateName = CurrentPermission.EntityType;
// world x and y
message.EntityCoordinates = coordinates;
message.DirRcv = Direction;
NetworkManager.ClientSendMessage(message);
}
public enum PlacementTypes : byte
{
None = 0,
Line = 1,
Grid = 2
}
}
}