Files
RobustToolbox/Robust.Shared/Prototypes/EntityPrototype.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

443 lines
15 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Prototypes
{
/// <summary>
/// Prototype that represents game entities.
/// </summary>
[Prototype("entity", -1)]
public class EntityPrototype : IPrototype
{
/// <summary>
/// The "in code name" of the object. Must be unique.
/// </summary>
[ViewVariables]
[DataField("id")]
public string ID { get; private set; } = default!;
/// <summary>
/// The "in game name" of the object. What is displayed to most players.
/// </summary>
[ViewVariables, CanBeNull]
[DataField("name")]
public string Name {
get => _name;
private set
{
_nameModified = true;
_name = Loc.GetString(value);
}
}
private string _name = "";
private bool _nameModified;
[DataField("localizationId")]
string? _localizationId;
/// <summary>
/// Fluent messageId used to lookup the entity's name and localization attributes.
/// </summary>
[ViewVariables, CanBeNull]
public string? LocalizationID
{
get => _localizationId ??= $"ent-{CaseConversion.PascalToKebab(ID)}";
private set => _localizationId = value;
}
/// <summary>
/// Optional suffix to display in development menus like the entity spawn panel,
/// to provide additional info without ruining the Name property itself.
/// </summary>
[ViewVariables]
[DataField("suffix")]
public string? EditorSuffix
{
get => _editorSuffix;
private set => _editorSuffix = value != null ? Loc.GetString(value) : null;
}
private string? _editorSuffix;
/// <summary>
/// The description of the object that shows upon using examine
/// </summary>
[ViewVariables]
[DataField("description")]
public string Description
{
get => _description;
private set
{
_descriptionModified = true;
_description = Loc.GetString(value);
}
}
private string _description = "";
private bool _descriptionModified;
/// <summary>
/// If true, this object should not show up in the entity spawn panel.
/// </summary>
[ViewVariables]
[NeverPushInheritance]
[DataField("abstract")]
public bool Abstract { get; private set; }
[DataField("placement")]
private EntityPlacementProperties PlacementProperties = new();
/// <summary>
/// The different mounting points on walls. (If any).
/// </summary>
[ViewVariables]
public List<int>? MountingPoints => PlacementProperties.MountingPoints;
/// <summary>
/// The Placement mode used for client-initiated placement. This is used for admin and editor placement. The serverside version controls what type the server assigns in normal gameplay.
/// </summary>
[ViewVariables]
public string PlacementMode => PlacementProperties.PlacementMode;
/// <summary>
/// The Range this entity can be placed from. This is only used serverside since the server handles normal gameplay. The client uses unlimited range since it handles things like admin spawning and editing.
/// </summary>
[ViewVariables]
public int PlacementRange => PlacementProperties.PlacementRange;
private const int DEFAULT_RANGE = 200;
/// <summary>
/// Set to hold snapping categories that this object has applied to it such as pipe/wire/wallmount
/// </summary>
private HashSet<string> _snapFlags => PlacementProperties.SnapFlags;
private bool _snapOverriden => PlacementProperties.SnapOverriden;
/// <summary>
/// Offset that is added to the position when placing. (if any). Client only.
/// </summary>
[ViewVariables]
public Vector2i PlacementOffset => PlacementProperties.PlacementOffset;
private bool _placementOverriden => PlacementProperties.PlacementOverriden;
/// <summary>
/// True if this entity will be saved by the map loader.
/// </summary>
[ViewVariables]
[DataField("save")]
public bool MapSavable { get; protected set; } = true;
/// <summary>
/// The prototype we inherit from.
/// </summary>
[ViewVariables]
[DataField("parent")]
public string? Parent { get; private set; }
/// <summary>
/// A list of children inheriting from this prototype.
/// </summary>
[ViewVariables]
public List<EntityPrototype> Children { get; private set; } = new();
public bool IsRoot => Parent == null;
/// <summary>
/// A dictionary mapping the component type list to the YAML mapping containing their settings.
/// </summary>
[field: DataField("components")]
[field: AlwaysPushInheritance]
public ComponentRegistry Components { get; } = new();
private readonly HashSet<Type> ReferenceTypes = new();
string? CurrentDeserializingComponent;
readonly Dictionary<string, Dictionary<(string, Type), object?>> FieldCache =
new();
readonly Dictionary<string, object?> DataCache = new();
public EntityPrototype()
{
// Everybody gets a transform component!
Components.Add("Transform", new TransformComponent());
// And a metadata component too!
Components.Add("MetaData", new MetaDataComponent());
}
public bool TryGetComponent<T>(string name, [NotNullWhen(true)] out T? component) where T : IComponent
{
if (!Components.TryGetValue(name, out var componentUnCast))
{
component = default;
return false;
}
// There are no duplicate component names
// TODO Sanity check with names being in an attribute of the type instead
component = (T) componentUnCast;
return true;
}
public void UpdateEntity(Entity entity)
{
if (ID != entity.Prototype?.ID)
{
Logger.Error($"Reloaded prototype used to update entity did not match entity's existing prototype: Expected '{ID}', got '{entity.Prototype?.ID}'");
return;
}
var factory = IoCManager.Resolve<IComponentFactory>();
var componentManager = IoCManager.Resolve<IComponentManager>();
var oldPrototype = entity.Prototype;
var oldPrototypeComponents = oldPrototype.Components.Keys
.Where(n => n != "Transform" && n != "MetaData")
.Select(name => (name, factory.GetRegistration(name).Type))
.ToList();
var newPrototypeComponents = Components.Keys
.Where(n => n != "Transform" && n != "MetaData")
.Select(name => (name, factory.GetRegistration(name).Type))
.ToList();
var ignoredComponents = new List<string>();
// Find components to be removed, and remove them
foreach (var (name, type) in oldPrototypeComponents.Except(newPrototypeComponents))
{
if (Components.Keys.Contains(name))
{
ignoredComponents.Add(name);
continue;
}
componentManager.RemoveComponent(entity.Uid, type);
}
componentManager.CullRemovedComponents();
var componentDependencyManager = IoCManager.Resolve<IComponentDependencyManager>();
// Add new components
foreach (var (name, type) in newPrototypeComponents.Where(t => !ignoredComponents.Contains(t.name)).Except(oldPrototypeComponents))
{
var data = Components[name];
var component = (Component) factory.GetComponent(name);
CurrentDeserializingComponent = name;
component.Owner = entity;
componentDependencyManager.OnComponentAdd(entity, component);
entity.AddComponent(component);
}
// Update entity metadata
entity.MetaData.EntityPrototype = this;
}
internal static void LoadEntity(EntityPrototype? prototype, Entity entity, IComponentFactory factory, IEntityLoadContext? context) //yeah officer this method right here
{
/*YamlObjectSerializer.Context? defaultContext = null;
if (context == null)
{
defaultContext = new PrototypeSerializationContext(prototype);
}*/
if (prototype != null)
{
foreach (var (name, data) in prototype.Components)
{
var fullData = data;
if (context != null)
{
fullData = context.GetComponentData(name, data);
}
EnsureCompExistsAndDeserialize(entity, factory, name, fullData, context as ISerializationContext);
}
}
if (context != null)
{
foreach (var name in context.GetExtraComponentTypes())
{
if (prototype != null && prototype.Components.ContainsKey(name))
{
// This component also exists in the prototype.
// This means that the previous step already caught both the prototype data AND map data.
// Meaning that re-running EnsureCompExistsAndDeserialize would wipe prototype data.
continue;
}
var ser = context.GetComponentData(name, null);
EnsureCompExistsAndDeserialize(entity, factory, name, ser, context as ISerializationContext);
}
}
}
private static void EnsureCompExistsAndDeserialize(Entity entity, IComponentFactory factory, string compName, IComponent data, ISerializationContext? context)
{
var compType = factory.GetRegistration(compName).Type;
if (!entity.TryGetComponent(compType, out var component))
{
var newComponent = (Component) factory.GetComponent(compName);
newComponent.Owner = entity;
entity.AddComponent(newComponent);
component = newComponent;
}
// TODO use this value to support struct components
_ = IoCManager.Resolve<ISerializationManager>().Copy(data, component, context);
}
public override string ToString()
{
return $"EntityPrototype({ID})";
}
public class ComponentRegistry : Dictionary<string, IComponent>
{
public ComponentRegistry()
{
}
public ComponentRegistry(Dictionary<string, IComponent> components) : base(components)
{
}
}
[DataDefinition]
public class EntityPlacementProperties
{
public bool PlacementOverriden { get; private set; }
public bool SnapOverriden { get; private set; }
private string _placementMode = "PlaceFree";
private Vector2i _placementOffset;
[DataField("mode")]
public string PlacementMode
{
get => _placementMode;
set
{
PlacementOverriden = true;
_placementMode = value;
}
}
[DataField("offset")]
public Vector2i PlacementOffset
{
get => _placementOffset;
set
{
PlacementOverriden = true;
_placementOffset = value;
}
}
[DataField("nodes")] public List<int>? MountingPoints;
[DataField("range")] public int PlacementRange = DEFAULT_RANGE;
private HashSet<string> _snapFlags = new ();
[DataField("snap")]
public HashSet<string> SnapFlags
{
get => _snapFlags;
set
{
SnapOverriden = true;
_snapFlags = value;
}
}
}
/*private class PrototypeSerializationContext : YamlObjectSerializer.Context
{
readonly EntityPrototype? prototype;
public PrototypeSerializationContext(EntityPrototype? owner)
{
prototype = owner;
}
public override void SetCachedField<T>(string field, T value)
{
if (StackDepth != 0 || prototype?.CurrentDeserializingComponent == null)
{
base.SetCachedField<T>(field, value);
return;
}
if (!prototype.FieldCache.TryGetValue(prototype.CurrentDeserializingComponent, out var fieldList))
{
fieldList = new Dictionary<(string, Type), object?>();
prototype.FieldCache[prototype.CurrentDeserializingComponent] = fieldList;
}
fieldList[(field, typeof(T))] = value;
}
public override bool TryGetCachedField<T>(string field, [MaybeNullWhen(false)] out T value)
{
if (StackDepth != 0 || prototype?.CurrentDeserializingComponent == null)
{
return base.TryGetCachedField<T>(field, out value);
}
if (prototype.FieldCache.TryGetValue(prototype.CurrentDeserializingComponent, out var dict))
{
if (dict.TryGetValue((field, typeof(T)), out var theValue))
{
value = (T) theValue!;
return true;
}
}
value = default!;
return false;
}
public override void SetDataCache(string field, object value)
{
if (StackDepth != 0 || prototype == null)
{
base.SetDataCache(field, value);
return;
}
prototype.DataCache[field] = value;
}
public override bool TryGetDataCache(string field, out object? value)
{
if (StackDepth != 0 || prototype == null)
{
return base.TryGetDataCache(field, out value);
}
return prototype.DataCache.TryGetValue(field, out value);
}
}*/
}
}