From 80f9f24243fe9852d5010a99b40ad5daa5e42740 Mon Sep 17 00:00:00 2001 From: Paul Ritter Date: Fri, 5 Mar 2021 00:59:14 +0100 Subject: [PATCH] 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 2ee4cc2c26ae92d952254dfa78388345d1bc4cf5. * 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 Co-authored-by: DrSmugleaf Co-authored-by: Vera Aguilera Puerto --- Robust.Analyzers/Diagnostics.cs | 10 + .../MeansImplicitAssigmentSuppressor.cs | 42 + Robust.Client/Animations/Animation.cs | 2 +- .../Animations/AnimationTrackPlaySound.cs | 2 +- .../Animations/AnimationTrackProperty.cs | 2 +- .../Animations/AnimationTrackSpriteFlick.cs | 2 +- Robust.Client/ClientIoC.cs | 3 +- Robust.Client/Console/Commands/Debug.cs | 1 - Robust.Client/GameController.cs | 9 +- .../GameController/GameController.IoC.cs | 1 - .../GameObjects/ClientComponentFactory.cs | 1 - .../Appearance/AppearanceComponent.cs | 101 +- .../Components/Eye/EyeComponent.cs | 15 +- .../Components/Icon/IconComponent.cs | 94 +- .../Components/Input/InputComponent.cs | 13 +- .../Components/Light/PointLightComponent.cs | 45 +- .../Components/Renderable/SpriteComponent.cs | 447 ++--- .../ClientUserInterfaceComponent.cs | 38 +- .../Graphics/Clyde/Clyde.LightRendering.cs | 2 +- Robust.Client/Graphics/Clyde/Clyde.Shaders.cs | 2 +- .../Graphics/Shaders/ShaderPrototype.cs | 223 ++- Robust.Client/Input/InputManager.cs | 32 +- Robust.Client/Input/KeyBindingRegistration.cs | 33 +- Robust.Client/Placement/PlacementManager.cs | 30 +- .../ResourceTypes/ShaderSourceResource.cs | 2 +- .../AppearanceVisualizerSerializer.cs | 73 + Robust.Client/Utility/SpriteSpecifierExt.cs | 8 +- Robust.Generators.UnitTesting/AnalyzerTest.cs | 63 + .../DataClassTests.cs | 82 + .../Robust.Generators.UnitTesting.csproj | 21 + Robust.Generators/Robust.Generators.csproj | 11 + Robust.Server/BaseServer.cs | 31 +- .../Components/Eye/EyeComponent.cs | 17 +- .../Components/Light/PointLightComponent.cs | 25 +- .../Components/Renderable/SpriteComponent.cs | 116 +- .../ServerUserInterfaceComponent.cs | 18 +- .../Components/VisibilityComponent.cs | 10 +- .../GameObjects/ServerComponentFactory.cs | 1 - Robust.Server/Maps/MapLoader.cs | 417 +++-- Robust.Shared/Audio/AudioParams.cs | 34 +- Robust.Shared/Containers/BaseContainer.cs | 12 +- Robust.Shared/Containers/Container.cs | 30 +- .../Containers/ContainerManagerComponent.cs | 28 +- Robust.Shared/Containers/ContainerSlot.cs | 36 +- Robust.Shared/Containers/IContainer.cs | 4 +- Robust.Shared/GameObjects/Component.cs | 10 +- .../ComponentDependencyAttribute.cs | 1 + Robust.Shared/GameObjects/ComponentManager.cs | 7 - .../Collidable/PhysicsComponent.Collision.cs | 29 +- .../Collidable/PhysicsComponent.Physics.cs | 6 +- .../Components/DebugExceptionComponents.cs | 10 - .../Components/Light/OccluderComponent.cs | 12 +- .../Localization/GrammarComponent.cs | 23 +- .../Components/Map/MapComponent.cs | 13 +- .../Components/Map/MapGridComponent.cs | 13 +- .../Components/MetaDataComponent.cs | 15 +- .../Renderable/SharedSpriteComponent.cs | 34 +- .../Components/Transform/SnapGridComponent.cs | 10 +- .../Transform/TransformComponent.cs | 19 +- .../SharedUserInterfaceComponent.cs | 28 +- Robust.Shared/GameObjects/IComponent.cs | 7 - Robust.Shared/GameObjects/IEntity.cs | 4 +- .../GameObjects/IEntityLoadContext.cs | 6 +- Robust.Shared/Map/PhysShapeGrid.cs | 25 +- .../MeansImplicitAssignmentAttribute.cs | 6 + .../Handshake/MsgEncryptionRequest.cs | 2 +- .../Handshake/MsgEncryptionResponse.cs | 2 +- .../Messages/Handshake/MsgLoginStart.cs | 2 +- .../Messages/Handshake/MsgLoginSuccess.cs | 2 +- .../Network/NetManager.ClientConnect.cs | 1 + .../Network/NetManager.ServerAuth.cs | 1 + Robust.Shared/Physics/CollisionRay.cs | 3 +- Robust.Shared/Physics/IPhysShape.cs | 2 +- Robust.Shared/Physics/PhysShapeAabb.cs | 13 +- Robust.Shared/Physics/PhysShapeCircle.cs | 13 +- Robust.Shared/Physics/PhysShapeRect.cs | 12 +- Robust.Shared/Physics/Ray.cs | 7 +- Robust.Shared/Prototypes/EntityPrototype.cs | 541 ++---- Robust.Shared/Prototypes/IPrototype.cs | 27 +- .../Prototypes/PrototypeInheritanceTree.cs | 113 ++ Robust.Shared/Prototypes/PrototypeManager.cs | 250 ++- .../Reflection/IReflectionManager.cs | 7 + Robust.Shared/Reflection/ReflectionManager.cs | 8 +- .../Serialization/ConstantsForAttribute.cs | 34 + .../Serialization/CustomFormatManager.cs | 317 ---- .../Serialization/DefaultValueSerializer.cs | 66 - .../Serialization/FlagsForAttribute.cs | 36 + .../Serialization/ICustomFormatManager.cs | 32 - Robust.Shared/Serialization/IExposeData.cs | 23 - Robust.Shared/Serialization/ISelfSerialize.cs | 2 +- .../Serialization/ISerializationHooks.cs | 21 + .../Serialization/InvalidMappingException.cs | 13 + .../AlwaysPushInheritanceAttribute.cs | 9 + .../Manager/Attributes/CopyByRefAttribute.cs | 7 + .../Manager/Attributes/DataDefinition.cs | 12 + .../Manager/Attributes/DataFieldAttribute.cs | 32 + .../DataFieldWithConstantAttribute.cs | 16 + .../Attributes/DataFieldWithFlagAttribute.cs | 16 + ...citDataDefinitionForInheritorsAttribute.cs | 7 + .../Manager/Attributes/MeansDataDefinition.cs | 9 + .../NeverPushInheritanceAttribute.cs | 12 + .../Attributes/TypeSerializerAttribute.cs | 9 + .../Manager/IPopulateDefaultValues.cs | 7 + .../Manager/ISerializationContext.cs | 13 + .../Manager/ISerializationManager.cs | 265 +++ .../Manager/InvalidNodeTypeException.cs | 15 + .../RequiredDataFieldNotProvidedException.cs | 6 + .../Manager/Result/DeserializationResult.cs | 44 + .../Manager/Result/DeserializedArray.cs | 72 + .../Manager/Result/DeserializedCollection.cs | 73 + .../Result/DeserializedComponentRegistry.cs | 111 ++ .../Manager/Result/DeserializedDefinition.cs | 57 + .../Manager/Result/DeserializedDictionary.cs | 83 + .../Manager/Result/DeserializedFieldEntry.cs | 48 + .../Manager/Result/DeserializedValue.cs | 29 + .../Manager/Result/IDeserializedDefinition.cs | 7 + .../InvalidDeserializedResultTypeException.cs | 33 + .../Manager/SerializationDataDefinition.cs | 408 +++++ .../Manager/SerializationManager.cs | 602 +++++++ .../SerializationManagerReadExtensions.cs | 116 ++ .../SerializationManagerWriteExtensions.cs | 29 + .../SerializationManager_FlagsAndConstants.cs | 179 ++ .../SerializationManager_Typeserializers.cs | 392 +++++ .../Serialization/Markdown/DataNode.cs | 40 + .../Serialization/Markdown/MappingDataNode.cs | 204 +++ .../Serialization/Markdown/NodeMark.cs | 81 + .../Markdown/SequenceDataNode.cs | 126 ++ .../Markdown/Validation/ErrorNode.cs | 41 + .../Markdown/Validation/InconclusiveNode.cs | 24 + .../Validation/ValidatedMappingNode.cs | 32 + .../Validation/ValidatedSequenceNode.cs | 28 + .../Markdown/Validation/ValidatedValueNode.cs | 24 + .../Markdown/Validation/ValidationNode.cs | 11 + .../Serialization/Markdown/ValueDataNode.cs | 52 + .../Serialization/Markdown/YamlNodeHelpers.cs | 35 + .../Serialization/ObjectSerializer.cs | 407 ----- .../Implementations/AngleSerializer.cs | 56 + .../Implementations/Box2Serializer.cs | 76 + .../Implementations/ColorSerializer.cs | 51 + .../ComponentRegistrySerializer.cs | 182 ++ .../Implementations/EntitySerializer.cs | 51 + .../FormattedMessageSerializer.cs | 46 + .../Generic/DictionarySerializer.cs | 209 +++ .../Generic/HashSetSerializer.cs | 138 ++ .../Generic/ListSerializers.cs | 249 +++ .../Generic/ValueTupleSerializer.cs | 71 + .../Implementations/MapIdSerializer.cs | 50 + .../Implementations/RegexSerializer.cs | 55 + .../Implementations/ResourcePathSerializer.cs | 73 + .../SpriteSpecifierSerializer.cs | 193 +++ .../Implementations/StringSerializer.cs | 44 + .../Implementations/TimespanSerializer.cs | 49 + .../Implementations/UIBox2Serializer.cs | 74 + .../Implementations/Vector2Serializer.cs | 68 + .../Implementations/Vector2iSerializer.cs | 68 + .../Implementations/Vector3Serializer.cs | 71 + .../Implementations/Vector4Serializer.cs | 74 + .../TypeSerializers/Interfaces/ITypeCopier.cs | 13 + .../TypeSerializers/Interfaces/ITypeReader.cs | 23 + .../Interfaces/ITypeReaderWriter.cs | 12 + .../Interfaces/ITypeSerializer.cs | 12 + .../TypeSerializers/Interfaces/ITypeWriter.cs | 11 + Robust.Shared/Serialization/WithFormat.cs | 53 - .../Serialization/YamlConstantSerializer.cs | 88 - .../YamlCustomFormatSerializer.cs | 47 - .../Serialization/YamlFlagSerializer.cs | 114 -- .../Serialization/YamlObjectSerializer.cs | 1504 ----------------- Robust.Shared/SharedIoC.cs | 3 +- .../Utility/FormattedMessage.MarkupParser.cs | 5 + Robust.Shared/Utility/ILGeneratorExt.cs | 189 +++ Robust.Shared/Utility/NullableHelper.cs | 20 +- Robust.Shared/Utility/SpriteSpecifier.cs | 2 +- Robust.Shared/Utility/TypeHelpers.cs | 306 +++- .../GameObjects/Components/Transform_Test.cs | 2 + .../GameObjects/Components/Container_Test.cs | 6 +- .../GameObjects/Components/Transform_Test.cs | 2 + .../ThrowingEntityDeletion_Test.cs | 17 +- .../Server/Maps/MapLoaderTest.cs | 21 +- .../Server/RobustServerSimulation.cs | 38 +- .../ComponentDependencies_Tests.cs | 3 + .../Shared/Localization/LocalizationTests.cs | 7 +- .../Shared/Map/EntityCoordinates_Tests.cs | 2 + Robust.UnitTesting/Shared/Maths/Ray_Test.cs | 1 + .../Prototypes/PrototypeManager_Test.cs | 80 +- .../InheritanceSerializationTest.cs | 116 ++ .../SerializationPriorityTest.cs | 76 + .../TypeSerializers/AngleSerializerTest.cs | 37 + .../TypeSerializers/ArraySerializerTest.cs | 34 + .../TypeSerializers/Box2SerializerTest.cs | 54 + .../TypeSerializers/ColorSerializerTest.cs | 44 + .../ComponentRegistrySerializerTest.cs | 60 + .../DictionarySerializerTest.cs | 51 + .../TypeSerializers/HashSetSerializerTest.cs | 35 + .../TypeSerializers/ListSerializerTest.cs | 35 + .../TypeSerializers/RegexSerializerTest.cs | 37 + .../TypeSerializers/TypeSerializerTest.cs | 17 + .../YamlConstantSerializer_Test.cs | 70 - .../Serialization/YamlFlagSerializer_Test.cs | 185 -- .../ImmutableListSerializationTest.cs | 72 +- .../TypePropertySerialization_Test.cs | 51 +- .../TypeSerialization_Test.cs | 41 +- .../YamlObjectSerializer_Test.cs | 575 ------- 202 files changed, 8614 insertions(+), 5345 deletions(-) create mode 100644 Robust.Analyzers/Diagnostics.cs create mode 100644 Robust.Analyzers/MeansImplicitAssigmentSuppressor.cs create mode 100644 Robust.Client/Serialization/AppearanceVisualizerSerializer.cs create mode 100644 Robust.Generators.UnitTesting/AnalyzerTest.cs create mode 100644 Robust.Generators.UnitTesting/DataClassTests.cs create mode 100644 Robust.Generators.UnitTesting/Robust.Generators.UnitTesting.csproj create mode 100644 Robust.Generators/Robust.Generators.csproj create mode 100644 Robust.Shared/MeansImplicitAssignmentAttribute.cs create mode 100644 Robust.Shared/Prototypes/PrototypeInheritanceTree.cs create mode 100644 Robust.Shared/Serialization/ConstantsForAttribute.cs delete mode 100644 Robust.Shared/Serialization/CustomFormatManager.cs delete mode 100644 Robust.Shared/Serialization/DefaultValueSerializer.cs create mode 100644 Robust.Shared/Serialization/FlagsForAttribute.cs delete mode 100644 Robust.Shared/Serialization/ICustomFormatManager.cs delete mode 100644 Robust.Shared/Serialization/IExposeData.cs create mode 100644 Robust.Shared/Serialization/ISerializationHooks.cs create mode 100644 Robust.Shared/Serialization/InvalidMappingException.cs create mode 100644 Robust.Shared/Serialization/Manager/Attributes/AlwaysPushInheritanceAttribute.cs create mode 100644 Robust.Shared/Serialization/Manager/Attributes/CopyByRefAttribute.cs create mode 100644 Robust.Shared/Serialization/Manager/Attributes/DataDefinition.cs create mode 100644 Robust.Shared/Serialization/Manager/Attributes/DataFieldAttribute.cs create mode 100644 Robust.Shared/Serialization/Manager/Attributes/DataFieldWithConstantAttribute.cs create mode 100644 Robust.Shared/Serialization/Manager/Attributes/DataFieldWithFlagAttribute.cs create mode 100644 Robust.Shared/Serialization/Manager/Attributes/ImplicitDataDefinitionForInheritorsAttribute.cs create mode 100644 Robust.Shared/Serialization/Manager/Attributes/MeansDataDefinition.cs create mode 100644 Robust.Shared/Serialization/Manager/Attributes/NeverPushInheritanceAttribute.cs create mode 100644 Robust.Shared/Serialization/Manager/Attributes/TypeSerializerAttribute.cs create mode 100644 Robust.Shared/Serialization/Manager/IPopulateDefaultValues.cs create mode 100644 Robust.Shared/Serialization/Manager/ISerializationContext.cs create mode 100644 Robust.Shared/Serialization/Manager/ISerializationManager.cs create mode 100644 Robust.Shared/Serialization/Manager/InvalidNodeTypeException.cs create mode 100644 Robust.Shared/Serialization/Manager/RequiredDataFieldNotProvidedException.cs create mode 100644 Robust.Shared/Serialization/Manager/Result/DeserializationResult.cs create mode 100644 Robust.Shared/Serialization/Manager/Result/DeserializedArray.cs create mode 100644 Robust.Shared/Serialization/Manager/Result/DeserializedCollection.cs create mode 100644 Robust.Shared/Serialization/Manager/Result/DeserializedComponentRegistry.cs create mode 100644 Robust.Shared/Serialization/Manager/Result/DeserializedDefinition.cs create mode 100644 Robust.Shared/Serialization/Manager/Result/DeserializedDictionary.cs create mode 100644 Robust.Shared/Serialization/Manager/Result/DeserializedFieldEntry.cs create mode 100644 Robust.Shared/Serialization/Manager/Result/DeserializedValue.cs create mode 100644 Robust.Shared/Serialization/Manager/Result/IDeserializedDefinition.cs create mode 100644 Robust.Shared/Serialization/Manager/Result/InvalidDeserializedResultTypeException.cs create mode 100644 Robust.Shared/Serialization/Manager/SerializationDataDefinition.cs create mode 100644 Robust.Shared/Serialization/Manager/SerializationManager.cs create mode 100644 Robust.Shared/Serialization/Manager/SerializationManagerReadExtensions.cs create mode 100644 Robust.Shared/Serialization/Manager/SerializationManagerWriteExtensions.cs create mode 100644 Robust.Shared/Serialization/Manager/SerializationManager_FlagsAndConstants.cs create mode 100644 Robust.Shared/Serialization/Manager/SerializationManager_Typeserializers.cs create mode 100644 Robust.Shared/Serialization/Markdown/DataNode.cs create mode 100644 Robust.Shared/Serialization/Markdown/MappingDataNode.cs create mode 100644 Robust.Shared/Serialization/Markdown/NodeMark.cs create mode 100644 Robust.Shared/Serialization/Markdown/SequenceDataNode.cs create mode 100644 Robust.Shared/Serialization/Markdown/Validation/ErrorNode.cs create mode 100644 Robust.Shared/Serialization/Markdown/Validation/InconclusiveNode.cs create mode 100644 Robust.Shared/Serialization/Markdown/Validation/ValidatedMappingNode.cs create mode 100644 Robust.Shared/Serialization/Markdown/Validation/ValidatedSequenceNode.cs create mode 100644 Robust.Shared/Serialization/Markdown/Validation/ValidatedValueNode.cs create mode 100644 Robust.Shared/Serialization/Markdown/Validation/ValidationNode.cs create mode 100644 Robust.Shared/Serialization/Markdown/ValueDataNode.cs create mode 100644 Robust.Shared/Serialization/Markdown/YamlNodeHelpers.cs delete mode 100644 Robust.Shared/Serialization/ObjectSerializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/AngleSerializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/Box2Serializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/ColorSerializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/EntitySerializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/FormattedMessageSerializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/DictionarySerializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/HashSetSerializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/ListSerializers.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/ValueTupleSerializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/MapIdSerializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/RegexSerializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/ResourcePathSerializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/SpriteSpecifierSerializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/StringSerializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/TimespanSerializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/UIBox2Serializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/Vector2Serializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/Vector2iSerializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/Vector3Serializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/Vector4Serializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Interfaces/ITypeCopier.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Interfaces/ITypeReader.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Interfaces/ITypeReaderWriter.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Interfaces/ITypeSerializer.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Interfaces/ITypeWriter.cs delete mode 100644 Robust.Shared/Serialization/WithFormat.cs delete mode 100644 Robust.Shared/Serialization/YamlConstantSerializer.cs delete mode 100644 Robust.Shared/Serialization/YamlCustomFormatSerializer.cs delete mode 100644 Robust.Shared/Serialization/YamlFlagSerializer.cs delete mode 100644 Robust.Shared/Serialization/YamlObjectSerializer.cs create mode 100644 Robust.Shared/Utility/ILGeneratorExt.cs create mode 100644 Robust.UnitTesting/Shared/Serialization/InheritanceSerializationTest.cs create mode 100644 Robust.UnitTesting/Shared/Serialization/SerializationPriorityTest.cs create mode 100644 Robust.UnitTesting/Shared/Serialization/TypeSerializers/AngleSerializerTest.cs create mode 100644 Robust.UnitTesting/Shared/Serialization/TypeSerializers/ArraySerializerTest.cs create mode 100644 Robust.UnitTesting/Shared/Serialization/TypeSerializers/Box2SerializerTest.cs create mode 100644 Robust.UnitTesting/Shared/Serialization/TypeSerializers/ColorSerializerTest.cs create mode 100644 Robust.UnitTesting/Shared/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs create mode 100644 Robust.UnitTesting/Shared/Serialization/TypeSerializers/DictionarySerializerTest.cs create mode 100644 Robust.UnitTesting/Shared/Serialization/TypeSerializers/HashSetSerializerTest.cs create mode 100644 Robust.UnitTesting/Shared/Serialization/TypeSerializers/ListSerializerTest.cs create mode 100644 Robust.UnitTesting/Shared/Serialization/TypeSerializers/RegexSerializerTest.cs create mode 100644 Robust.UnitTesting/Shared/Serialization/TypeSerializers/TypeSerializerTest.cs delete mode 100644 Robust.UnitTesting/Shared/Serialization/YamlConstantSerializer_Test.cs delete mode 100644 Robust.UnitTesting/Shared/Serialization/YamlFlagSerializer_Test.cs delete mode 100644 Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/YamlObjectSerializer_Test.cs diff --git a/Robust.Analyzers/Diagnostics.cs b/Robust.Analyzers/Diagnostics.cs new file mode 100644 index 000000000..46504a663 --- /dev/null +++ b/Robust.Analyzers/Diagnostics.cs @@ -0,0 +1,10 @@ +using Microsoft.CodeAnalysis; + +namespace Robust.Generators +{ + public static class Diagnostics + { + public static SuppressionDescriptor MeansImplicitAssignment => + new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned."); + } +} diff --git a/Robust.Analyzers/MeansImplicitAssigmentSuppressor.cs b/Robust.Analyzers/MeansImplicitAssigmentSuppressor.cs new file mode 100644 index 000000000..37936b9ce --- /dev/null +++ b/Robust.Analyzers/MeansImplicitAssigmentSuppressor.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Robust.Generators; + +namespace Robust.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class MeansImplicitAssigmentSuppressor : DiagnosticSuppressor + { + const string MeansImplicitAssignmentAttribute = "Robust.Shared.MeansImplicitAssignmentAttribute"; + + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + var implAttr = context.Compilation.GetTypeByMetadataName(MeansImplicitAssignmentAttribute); + foreach (var reportedDiagnostic in context.ReportedDiagnostics) + { + if(reportedDiagnostic.Id != Diagnostics.MeansImplicitAssignment.SuppressedDiagnosticId) continue; + + var node = reportedDiagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(reportedDiagnostic.Location.SourceSpan); + if (node == null) continue; + + var symbol = context.GetSemanticModel(reportedDiagnostic.Location.SourceTree).GetDeclaredSymbol(node); + + if (symbol == null || !symbol.GetAttributes().Any(a => + a.AttributeClass?.GetAttributes().Any(attr => + SymbolEqualityComparer.Default.Equals(attr.AttributeClass, implAttr)) == true)) + { + continue; + } + + context.ReportSuppression(Suppression.Create( + Diagnostics.MeansImplicitAssignment, + reportedDiagnostic)); + } + } + + public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(Diagnostics.MeansImplicitAssignment); + } +} diff --git a/Robust.Client/Animations/Animation.cs b/Robust.Client/Animations/Animation.cs index 2f477ec9d..c9130f8c5 100644 --- a/Robust.Client/Animations/Animation.cs +++ b/Robust.Client/Animations/Animation.cs @@ -13,7 +13,7 @@ namespace Robust.Client.Animations /// public sealed class Animation { - public readonly List AnimationTracks = new(); + public List AnimationTracks { get; private set; } = new(); public TimeSpan Length { get; set; } } diff --git a/Robust.Client/Animations/AnimationTrackPlaySound.cs b/Robust.Client/Animations/AnimationTrackPlaySound.cs index b4cffde4e..1c61681f5 100644 --- a/Robust.Client/Animations/AnimationTrackPlaySound.cs +++ b/Robust.Client/Animations/AnimationTrackPlaySound.cs @@ -15,7 +15,7 @@ namespace Robust.Client.Animations /// /// A list of key frames for when to fire flicks. /// - public readonly List KeyFrames = new(); + public List KeyFrames { get; private set; } = new(); public override (int KeyFrameIndex, float FramePlayingTime) InitPlayback() { diff --git a/Robust.Client/Animations/AnimationTrackProperty.cs b/Robust.Client/Animations/AnimationTrackProperty.cs index 55cf54f8b..19e4ed376 100644 --- a/Robust.Client/Animations/AnimationTrackProperty.cs +++ b/Robust.Client/Animations/AnimationTrackProperty.cs @@ -10,7 +10,7 @@ namespace Robust.Client.Animations /// public abstract class AnimationTrackProperty : AnimationTrack { - public readonly List KeyFrames = new(); + public List KeyFrames { get; protected set; } = new(); /// /// How to interpolate values when between two keyframes. diff --git a/Robust.Client/Animations/AnimationTrackSpriteFlick.cs b/Robust.Client/Animations/AnimationTrackSpriteFlick.cs index eba3946dd..29e5fefe0 100644 --- a/Robust.Client/Animations/AnimationTrackSpriteFlick.cs +++ b/Robust.Client/Animations/AnimationTrackSpriteFlick.cs @@ -15,7 +15,7 @@ namespace Robust.Client.Animations /// /// A list of key frames for when to fire flicks. /// - public readonly List KeyFrames = new(); + public List KeyFrames { get; private set; } = new(); // TODO: Should this layer key be per keyframe maybe? /// diff --git a/Robust.Client/ClientIoC.cs b/Robust.Client/ClientIoC.cs index 23654c4ad..386751163 100644 --- a/Robust.Client/ClientIoC.cs +++ b/Robust.Client/ClientIoC.cs @@ -27,6 +27,8 @@ using Robust.Shared.Network; using Robust.Shared.Players; using Robust.Shared.Prototypes; using Robust.Shared.Reflection; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager; namespace Robust.Client { @@ -101,7 +103,6 @@ namespace Robust.Client IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); - //IoCManager.Register(); } } } diff --git a/Robust.Client/Console/Commands/Debug.cs b/Robust.Client/Console/Commands/Debug.cs index e53262df2..c62ca993a 100644 --- a/Robust.Client/Console/Commands/Debug.cs +++ b/Robust.Client/Console/Commands/Debug.cs @@ -12,7 +12,6 @@ using Robust.Client.Input; using Robust.Client.Debugging; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; -using Robust.Client.ResourceManagement.ResourceTypes; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; diff --git a/Robust.Client/GameController.cs b/Robust.Client/GameController.cs index 20028c863..4401e8828 100644 --- a/Robust.Client/GameController.cs +++ b/Robust.Client/GameController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Net; using System.Threading.Tasks; @@ -27,6 +27,7 @@ using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -44,7 +45,7 @@ namespace Robust.Client [Dependency] private readonly IUserInterfaceManagerInternal _userInterfaceManager = default!; [Dependency] private readonly IBaseClient _client = default!; [Dependency] private readonly IInputManager _inputManager = default!; - [Dependency] private readonly IClientConsoleHost _consoleHost = default!; + [Dependency] private readonly IClientConsoleHost _console = default!; [Dependency] private readonly ITimerManager _timerManager = default!; [Dependency] private readonly IClientEntityManager _entityManager = default!; [Dependency] private readonly IPlacementManager _placementManager = default!; @@ -154,6 +155,8 @@ namespace Robust.Client _configurationManager.LoadCVarsFromAssembly(loadedModule); } + IoCManager.Resolve().Initialize(); + // Call Init in game assemblies. _modLoader.BroadcastRunLevel(ModRunLevel.PreInit); _modLoader.BroadcastRunLevel(ModRunLevel.Init); @@ -163,7 +166,7 @@ namespace Robust.Client IoCManager.Resolve().SetupNetworking(); _serializer.Initialize(); _inputManager.Initialize(); - _consoleHost.Initialize(); + _console.Initialize(); _prototypeManager.Initialize(); _prototypeManager.LoadDirectory(new ResourcePath(@"/Prototypes/")); _prototypeManager.Resync(); diff --git a/Robust.Client/GameController/GameController.IoC.cs b/Robust.Client/GameController/GameController.IoC.cs index 490b1bfce..512d5c83d 100644 --- a/Robust.Client/GameController/GameController.IoC.cs +++ b/Robust.Client/GameController/GameController.IoC.cs @@ -17,7 +17,6 @@ namespace Robust.Client RegisterReflection(); } - internal static void RegisterReflection() { // Gets a handle to the shared and the current (client) dll. diff --git a/Robust.Client/GameObjects/ClientComponentFactory.cs b/Robust.Client/GameObjects/ClientComponentFactory.cs index a5b7bc96e..84fe37d24 100644 --- a/Robust.Client/GameObjects/ClientComponentFactory.cs +++ b/Robust.Client/GameObjects/ClientComponentFactory.cs @@ -54,7 +54,6 @@ namespace Robust.Client.GameObjects #if DEBUG Register(); - Register(); Register(); Register(); #endif diff --git a/Robust.Client/GameObjects/Components/Appearance/AppearanceComponent.cs b/Robust.Client/GameObjects/Components/Appearance/AppearanceComponent.cs index 6f988c838..f35e01a3c 100644 --- a/Robust.Client/GameObjects/Components/Appearance/AppearanceComponent.cs +++ b/Robust.Client/GameObjects/Components/Appearance/AppearanceComponent.cs @@ -4,8 +4,7 @@ using System.Diagnostics.CodeAnalysis; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Reflection; -using Robust.Shared.Serialization; -using Robust.Shared.Utility; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; using YamlDotNet.RepresentationModel; @@ -15,13 +14,11 @@ namespace Robust.Client.GameObjects { [ViewVariables] private Dictionary data = new(); + [ViewVariables] + [DataField("visuals")] internal List Visualizers = new(); - [Dependency] private readonly IReflectionManager _reflectionManager = default!; - - private static bool _didRegisterSerializer; - [ViewVariables] private bool _appearanceDirty; @@ -107,18 +104,6 @@ namespace Robust.Client.GameObjects _appearanceDirty = false; } - public override void ExposeData(ObjectSerializer serializer) - { - if (!_didRegisterSerializer) - { - YamlObjectSerializer.RegisterTypeSerializer(typeof(AppearanceVisualizer), - new VisualizerTypeSerializer(_reflectionManager)); - _didRegisterSerializer = true; - } - - serializer.DataFieldCached(ref Visualizers, "visuals", new List()); - } - public override void Initialize() { base.Initialize(); @@ -131,78 +116,6 @@ namespace Robust.Client.GameObjects MarkDirty(); } - class VisualizerTypeSerializer : YamlObjectSerializer.TypeSerializer - { - private readonly IReflectionManager _reflectionManager; - - public VisualizerTypeSerializer(IReflectionManager reflectionManager) - { - _reflectionManager = reflectionManager; - } - - public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer) - { - var mapping = (YamlMappingNode) node; - var nodeType = mapping.GetNode("type"); - switch (nodeType.AsString()) - { - case SpriteLayerToggle.NAME: - var keyString = mapping.GetNode("key").AsString(); - object key; - if (_reflectionManager.TryParseEnumReference(keyString, out var @enum)) - { - key = @enum; - } - else - { - key = keyString; - } - - var layer = mapping.GetNode("layer").AsInt(); - return new SpriteLayerToggle(key, layer); - - default: - var visType = _reflectionManager.LooseGetType(nodeType.AsString()); - if (!typeof(AppearanceVisualizer).IsAssignableFrom(visType)) - { - throw new InvalidOperationException(); - } - - var vis = (AppearanceVisualizer) Activator.CreateInstance(visType)!; - vis.LoadData(mapping); - return vis; - } - } - - public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer) - { - switch (obj) - { - case SpriteLayerToggle spriteLayerToggle: - YamlScalarNode key; - if (spriteLayerToggle.Key is Enum) - { - var name = spriteLayerToggle.Key.GetType().FullName; - key = new YamlScalarNode($"{name}.{spriteLayerToggle.Key}"); - } - else - { - key = new YamlScalarNode(spriteLayerToggle.Key.ToString()); - } - - return new YamlMappingNode - { - {new YamlScalarNode("type"), new YamlScalarNode(SpriteLayerToggle.NAME)}, - {new YamlScalarNode("key"), key}, - {new YamlScalarNode("layer"), new YamlScalarNode(spriteLayerToggle.SpriteLayer.ToString())}, - }; - default: - // TODO: A proper way to do serialization here. - // I can't use the ExposeData system here since that's specific to entity serializers. - return new YamlMappingNode(); - } - } - } internal class SpriteLayerToggle : AppearanceVisualizer { @@ -223,15 +136,9 @@ namespace Robust.Client.GameObjects /// Handles the visualization of data inside of an appearance component. /// Implementations of this class are NOT bound to a specific entity, they are flyweighted across multiple. /// + [ImplicitDataDefinitionForInheritors] public abstract class AppearanceVisualizer { - /// - /// Load data from the prototype declaring this visualizer, to configure settings and such. - /// - public virtual void LoadData(YamlMappingNode node) - { - } - /// /// Initializes an entity to be managed by this appearance controller. /// DO NOT assume this is your only entity. Visualizers are shared. diff --git a/Robust.Client/GameObjects/Components/Eye/EyeComponent.cs b/Robust.Client/GameObjects/Components/Eye/EyeComponent.cs index 19fb83b61..e10927757 100644 --- a/Robust.Client/GameObjects/Components/Eye/EyeComponent.cs +++ b/Robust.Client/GameObjects/Components/Eye/EyeComponent.cs @@ -3,7 +3,9 @@ using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Robust.Client.GameObjects @@ -20,7 +22,9 @@ namespace Robust.Client.GameObjects // Horrible hack to get around ordering issues. private bool _setCurrentOnInitialize; - private bool _setDrawFovOnInitialize; + [DataField("drawFov")] + private bool _setDrawFovOnInitialize = true; + [DataField("zoom")] private Vector2 _setZoomOnInitialize = Vector2.One/2f; private Vector2 _offset = Vector2.Zero; @@ -157,15 +161,6 @@ namespace Robust.Client.GameObjects Current = false; } - /// - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - - serializer.DataFieldCached(ref _setZoomOnInitialize, "zoom", Vector2.One/2f); - serializer.DataFieldCached(ref _setDrawFovOnInitialize, "drawFov", true); - } - /// /// Updates the Eye of this entity with the transform position. This has to be called every frame to /// keep the view following the entity. diff --git a/Robust.Client/GameObjects/Components/Icon/IconComponent.cs b/Robust.Client/GameObjects/Components/Icon/IconComponent.cs index 7814a44d1..70a4e2385 100644 --- a/Robust.Client/GameObjects/Components/Icon/IconComponent.cs +++ b/Robust.Client/GameObjects/Components/Icon/IconComponent.cs @@ -1,105 +1,51 @@ using Robust.Client.Graphics; using Robust.Client.ResourceManagement; +using Robust.Client.Utility; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Utility; namespace Robust.Client.GameObjects { [RegisterComponent] - public class IconComponent : Component + public class IconComponent : Component, ISerializationHooks { public override string Name => "Icon"; public IDirectionalTextureProvider? Icon { get; private set; } - [Dependency] private readonly IResourceCache _resourceCache = default!; + [DataField("sprite")] + private ResourcePath? rsi; + [DataField("state")] + private string? stateID; + + void ISerializationHooks.AfterDeserialization() + { + if (rsi != null && stateID != null) + { + Icon = new SpriteSpecifier.Rsi(rsi, stateID).Frame0(); + } + } public const string LogCategory = "go.comp.icon"; const string SerializationCache = "icon"; - public override void ExposeData(ObjectSerializer serializer) + private static IRsiStateLike TextureForConfig(IconComponent compData, IResourceCache resourceCache) { - base.ExposeData(serializer); - - // TODO: Does this need writing? - if (serializer.Reading) - { - Icon = TextureForConfig(serializer, _resourceCache); - } - } - - private static IRsiStateLike TextureForConfig(ObjectSerializer serializer, IResourceCache resourceCache) - { - DebugTools.Assert(serializer.Reading); - - if (serializer.TryGetCacheData(SerializationCache, out var dirTex)) - { - return dirTex; - } - - var tex = serializer.ReadDataField("texture", null); - if (!string.IsNullOrWhiteSpace(tex)) - { - dirTex = resourceCache.GetResource(SpriteComponent.TextureRoot / tex).Texture; - serializer.SetCacheData(SerializationCache, dirTex); - return dirTex; - } - - RSI rsi; - - var rsiPath = serializer.ReadDataField("sprite", null); - - if (string.IsNullOrWhiteSpace(rsiPath)) - { - dirTex = resourceCache.GetFallback().Texture; - serializer.SetCacheData(SerializationCache, dirTex); - return dirTex; - } - - var path = SpriteComponent.TextureRoot / rsiPath; - - try - { - rsi = resourceCache.GetResource(path).RSI; - } - catch - { - dirTex = resourceCache.GetFallback().Texture; - serializer.SetCacheData(SerializationCache, dirTex); - return dirTex; - } - - var stateId = serializer.ReadDataField("state", null); - if (string.IsNullOrWhiteSpace(stateId)) - { - Logger.ErrorS(LogCategory, "No state specified."); - dirTex = resourceCache.GetFallback().Texture; - serializer.SetCacheData(SerializationCache, dirTex); - return dirTex; - } - - if (rsi.TryGetState(stateId, out var state)) - { - serializer.SetCacheData(SerializationCache, state); - return state; - } - else - { - Logger.ErrorS(LogCategory, "State '{0}' does not exist on RSI.", stateId); - return resourceCache.GetFallback().Texture; - } + return compData.Icon?.Default ?? resourceCache.GetFallback().Texture; } public static IRsiStateLike? GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache) { - if (!prototype.Components.TryGetValue("Icon", out var mapping)) + if (!prototype.Components.TryGetValue("Icon", out var compData)) { return null; } - return TextureForConfig(YamlObjectSerializer.NewReader(mapping), resourceCache); + + return TextureForConfig((IconComponent)compData, resourceCache); } } } diff --git a/Robust.Client/GameObjects/Components/Input/InputComponent.cs b/Robust.Client/GameObjects/Components/Input/InputComponent.cs index 4ad37b7de..4e0a0c2cf 100644 --- a/Robust.Client/GameObjects/Components/Input/InputComponent.cs +++ b/Robust.Client/GameObjects/Components/Input/InputComponent.cs @@ -1,6 +1,8 @@ using Robust.Shared.GameObjects; using Robust.Shared.Input; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Robust.Client.GameObjects @@ -17,14 +19,7 @@ namespace Robust.Client.GameObjects /// The context that will be made active for a client that attaches to this entity. /// [ViewVariables(VVAccess.ReadWrite)] - public string ContextName { get; set; } = default!; - - /// - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - - serializer.DataReadWriteFunction("context", InputContextContainer.DefaultContextName, value => ContextName = value, () => ContextName); - } + [DataField("context")] + public string ContextName { get; set; } = InputContextContainer.DefaultContextName; } } diff --git a/Robust.Client/GameObjects/Components/Light/PointLightComponent.cs b/Robust.Client/GameObjects/Components/Light/PointLightComponent.cs index cab809928..d4b68b1b4 100644 --- a/Robust.Client/GameObjects/Components/Light/PointLightComponent.cs +++ b/Robust.Client/GameObjects/Components/Light/PointLightComponent.cs @@ -1,4 +1,4 @@ -using System; +using System; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; using Robust.Shared.Animations; @@ -7,13 +7,14 @@ using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Robust.Client.GameObjects { [RegisterComponent] [ComponentReference(typeof(IPointLightComponent))] - public class PointLightComponent : Component, IPointLightComponent + public class PointLightComponent : Component, IPointLightComponent, ISerializationHooks { [Dependency] private readonly IResourceCache _resourceCache = default!; @@ -134,16 +135,25 @@ namespace Robust.Client.GameObjects } } - private float _radius = 5; + [DataField("radius")] + private float _radius = 5f; + [DataField("nestedvisible")] private bool _visibleNested = true; private bool _lightOnParent; + [DataField("color")] private Color _color = Color.White; - private Vector2 _offset; + [DataField("offset")] + private Vector2 _offset = Vector2.Zero; + [DataField("enabled")] private bool _enabled = true; + [DataField("autoRot")] private bool _maskAutoRotate; private Angle _rotation; - private float _energy; - private float _softness; + [DataField("energy")] + private float _energy = 1f; + [DataField("softness")] + private float _softness = 1f; + [DataField("mask")] private string? _maskPath; /// @@ -169,6 +179,14 @@ namespace Robust.Client.GameObjects Mask = null; } + void ISerializationHooks.AfterDeserialization() + { + if (_maskPath != null) + { + Mask = IoCManager.Resolve().GetResource(_maskPath); + } + } + public override void Initialize() { base.Initialize(); @@ -180,7 +198,7 @@ namespace Robust.Client.GameObjects { base.HandleMessage(message, component); - if ((message is ParentChangedMessage msg)) + if (message is ParentChangedMessage msg) { HandleTransformParentChanged(msg); } @@ -204,19 +222,6 @@ namespace Robust.Client.GameObjects } } - public override void ExposeData(ObjectSerializer serializer) - { - serializer.DataFieldCached(ref _offset, "offset", Vector2.Zero); - serializer.DataFieldCached(ref _radius, "radius", 5f); - serializer.DataFieldCached(ref _color, "color", Color.White); - serializer.DataFieldCached(ref _enabled, "enabled", true); - serializer.DataFieldCached(ref _energy, "energy", 1f); - serializer.DataFieldCached(ref _softness, "softness", 1f); - serializer.DataFieldCached(ref _maskAutoRotate, "autoRot", false); - serializer.DataFieldCached(ref _visibleNested, "nestedvisible", true); - serializer.DataFieldCached(ref _maskPath, "mask", null); - } - public override void OnRemove() { base.OnRemove(); diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs index c4a7cf070..06c02062e 100644 --- a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs +++ b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -17,6 +17,8 @@ using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Reflection; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Timing; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; @@ -25,8 +27,12 @@ using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth; namespace Robust.Client.GameObjects { public sealed class SpriteComponent : SharedSpriteComponent, ISpriteComponent, - IComponentDebug + IComponentDebug, ISerializationHooks { + [Dependency] private readonly IResourceCache resourceCache = default!; + [Dependency] private readonly IPrototypeManager prototypes = default!; + + [DataField("visible")] private bool _visible = true; [ViewVariables(VVAccess.ReadWrite)] @@ -36,6 +42,7 @@ namespace Robust.Client.GameObjects set => _visible = value; } + [DataFieldWithConstant("drawdepth", typeof(DrawDepthTag))] private int drawDepth = DrawDepthTag.Default; /// @@ -48,6 +55,7 @@ namespace Robust.Client.GameObjects set => drawDepth = value; } + [DataField("scale")] private Vector2 scale = Vector2.One; /// @@ -61,7 +69,8 @@ namespace Robust.Client.GameObjects set => scale = value; } - private Angle rotation; + [DataField("rotation")] + private Angle rotation = Angle.Zero; [Animatable] [ViewVariables(VVAccess.ReadWrite)] @@ -71,6 +80,7 @@ namespace Robust.Client.GameObjects set => rotation = value; } + [DataField("offset")] private Vector2 offset = Vector2.Zero; /// @@ -84,6 +94,7 @@ namespace Robust.Client.GameObjects set => offset = value; } + [DataField("color")] private Color color = Color.White; [Animatable] @@ -108,18 +119,152 @@ namespace Robust.Client.GameObjects set => _directional = value; } + [DataField("directional")] private bool _directional = true; + [DataField("layerDatums")] + private List LayerDatums + { + get + { + var layerDatums = new List(); + foreach (var layer in Layers) + { + layerDatums.Add(layer.ToPrototypeData()); + } + + return layerDatums; + } + set + { + if(value == null) return; + + Layers.Clear(); + foreach (var layerDatum in value) + { + var anyTextureAttempted = false; + var layer = new Layer(this); + if (!string.IsNullOrWhiteSpace(layerDatum.RsiPath)) + { + var path = TextureRoot / layerDatum.RsiPath; + try + { + layer.RSI = IoCManager.Resolve().GetResource(path).RSI; + } + catch + { + Logger.ErrorS(LogCategory, "Unable to load layer RSI '{0}'.", path); + } + } + + if (!string.IsNullOrWhiteSpace(layerDatum.State)) + { + anyTextureAttempted = true; + var theRsi = layer.RSI ?? BaseRSI; + if (theRsi == null) + { + Logger.ErrorS(LogCategory, + "Layer has no RSI to load states from. Cannot use 'state' property. ({0})", + layerDatum.State); + } + else + { + var stateid = new RSI.StateId(layerDatum.State); + layer.State = stateid; + if (theRsi.TryGetState(stateid, out var state)) + { + // Always use south because this layer will be cached in the serializer. + layer.AnimationTimeLeft = state.GetDelay(0); + } + else + { + Logger.ErrorS(LogCategory, + $"State '{stateid}' not found in RSI: '{theRsi.Path}'.", + stateid); + } + } + } + + if (!string.IsNullOrWhiteSpace(layerDatum.TexturePath)) + { + anyTextureAttempted = true; + if (layer.State.IsValid) + { + Logger.ErrorS(LogCategory, + "Cannot specify 'texture' on a layer if it has an RSI state specified." + ); + } + else + { + layer.Texture = + IoCManager.Resolve().GetResource(TextureRoot / layerDatum.TexturePath); + } + } + + if (!string.IsNullOrWhiteSpace(layerDatum.Shader)) + { + if (IoCManager.Resolve().TryIndex(layerDatum.Shader, out var prototype)) + { + layer.Shader = prototype.Instance(); + } + else + { + Logger.ErrorS(LogCategory, + "Shader prototype '{0}' does not exist.", + layerDatum.Shader); + } + } + + layer.Color = layerDatum.Color; + layer.Rotation = layerDatum.Rotation; + // If neither state: nor texture: were provided we assume that they want a blank invisible layer. + layer.Visible = anyTextureAttempted && layerDatum.Visible; + layer.Scale = layerDatum.Scale; + + Layers.Add(layer); + + if (layerDatum.MapKeys != null) + { + var index = Layers.Count - 1; + foreach (var keyString in layerDatum.MapKeys) + { + object key; + if (IoCManager.Resolve().TryParseEnumReference(keyString, out var @enum)) + { + key = @enum; + } + else + { + key = keyString; + } + + if (LayerMap.ContainsKey(key)) + { + Logger.ErrorS(LogCategory, "Duplicate layer map key definition: {0}", key); + continue; + } + + LayerMap.Add(key, index); + } + } + } + + _layerMapShared = true; + UpdateIsInert(); + } + } + private RSI? _baseRsi; [ViewVariables(VVAccess.ReadWrite)] + [DataField("rsi", priority: 2)] public RSI? BaseRSI { get => _baseRsi; set { _baseRsi = value; - if (Layers == null || value == null) + if (value == null) { return; } @@ -147,6 +292,12 @@ namespace Robust.Client.GameObjects } } + [DataField("sprite", readOnly: true)] private string? rsi; + [DataField("layers", readOnly: true)] private List layerDatums = new (); + + [DataField("state", readOnly: true)] private string? state; + [DataField("texture", readOnly: true)] private string? texture; + [ViewVariables(VVAccess.ReadWrite)] public bool ContainerOccluded { get; set; } @@ -158,11 +309,7 @@ namespace Robust.Client.GameObjects [ViewVariables] private Dictionary LayerMap = new(); [ViewVariables] private bool _layerMapShared; - [ViewVariables] private List Layers = default!; - - [Dependency] private readonly IResourceCache resourceCache = default!; - [Dependency] private readonly IPrototypeManager prototypes = default!; - [Dependency] private readonly IReflectionManager reflectionManager = default!; + [ViewVariables] private List Layers = new(); [ViewVariables(VVAccess.ReadWrite)] public uint RenderOrder { get; set; } @@ -170,10 +317,9 @@ namespace Robust.Client.GameObjects private static ShaderInstance? _defaultShader; [ViewVariables] - private ShaderInstance? DefaultShader => _defaultShader ?? - (_defaultShader = prototypes - .Index("shaded") - .Instance()); + private ShaderInstance? DefaultShader => _defaultShader ??= prototypes + .Index("shaded") + .Instance(); public const string LogCategory = "go.comp.sprite"; const string LayerSerializationCache = "spritelayer"; @@ -181,6 +327,46 @@ namespace Robust.Client.GameObjects [ViewVariables(VVAccess.ReadWrite)] public bool IsInert { get; private set; } + void ISerializationHooks.AfterDeserialization() + { + { + if (!string.IsNullOrWhiteSpace(rsi)) + { + var rsiPath = TextureRoot / rsi; + try + { + BaseRSI = IoCManager.Resolve().GetResource(rsiPath).RSI; + } + catch (Exception e) + { + Logger.ErrorS(SpriteComponent.LogCategory, "Unable to load RSI '{0}'. Trace:\n{1}", rsiPath, e); + } + } + } + + if (layerDatums.Count == 0) + { + if (state != null || texture != null) + { + layerDatums.Insert(0, new PrototypeLayerData + { + TexturePath = string.IsNullOrWhiteSpace(texture) ? null : texture, + State = string.IsNullOrWhiteSpace(state) ? null : state, + Color = Color.White, + Scale = Vector2.One, + Visible = true, + }); + state = null; + texture = null; + } + } + + if (layerDatums.Count != 0) + { + LayerDatums = layerDatums; + } + } + /// /// Update this sprite component to visibly match the current state of other at the time /// this is called. Does not keep them perpetually in sync. @@ -1005,9 +1191,14 @@ namespace Robust.Client.GameObjects RenderInternal(drawingHandle, worldRotation, Vector2.Zero, overrideDirection); } - private bool _screenLock = false; - private Direction _overrideDirection = Direction.South; - private bool _enableOverrideDirection = false; + [DataField("noRot")] + private bool _screenLock = true; + + [DataField("overrideDir")] + private Direction _overrideDirection = Direction.East; + + [DataField("enableOverrideDir")] + private bool _enableOverrideDirection; /// [ViewVariables(VVAccess.ReadWrite)] @@ -1153,205 +1344,6 @@ namespace Robust.Client.GameObjects return texture; } - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - - serializer.DataFieldCached(ref scale, "scale", Vector2.One); - serializer.DataFieldCached(ref rotation, "rotation", Angle.Zero); - serializer.DataFieldCached(ref offset, "offset", Vector2.Zero); - serializer.DataFieldCached(ref drawDepth, "drawdepth", DrawDepthTag.Default, - WithFormat.Constants()); - serializer.DataFieldCached(ref color, "color", Color.White); - serializer.DataFieldCached(ref _visible, "visible", true); - serializer.DataFieldCached(ref _directional, "directional", true); //TODO: Kill ME - serializer.DataFieldCached(ref _screenLock, "noRot", true); - serializer.DataFieldCached(ref _enableOverrideDirection, "enableOverrideDir", false); - serializer.DataFieldCached(ref _overrideDirection, "overrideDir", Direction.East); - - // TODO: Writing? - if (!serializer.Reading) - { - return; - } - - { - var rsi = serializer.ReadDataField("sprite", null); - if (!string.IsNullOrWhiteSpace(rsi)) - { - var rsiPath = TextureRoot / rsi; - try - { - BaseRSI = resourceCache.GetResource(rsiPath).RSI; - } - catch (Exception e) - { - Logger.ErrorS(LogCategory, "Unable to load RSI '{0}'. Trace:\n{1}", rsiPath, e); - } - } - } - - List CloneLayers(List source) - { - var clone = new List(source.Count); - foreach (var layer in source) - { - clone.Add(new Layer(layer, this)); - } - - return clone; - } - - if (serializer.TryGetCacheData>(LayerSerializationCache, out var layers)) - { - LayerMap = serializer.GetCacheData>(LayerMapSerializationCache); - _layerMapShared = true; - Layers = CloneLayers(layers); - UpdateIsInert(); - return; - } - - layers = new List(); - - var layerMap = new Dictionary(); - - var layerData = - serializer.ReadDataField("layers", new List()); - - if(layerData.Count == 0){ - var baseState = serializer.ReadDataField("state", null); - var texturePath = serializer.ReadDataField("texture", null); - - if (baseState != null || texturePath != null) - { - layerData.Insert(0, new PrototypeLayerData - { - TexturePath = string.IsNullOrWhiteSpace(texturePath) ? null : texturePath, - State = string.IsNullOrWhiteSpace(baseState) ? null : baseState, - Color = Color.White, - Scale = Vector2.One, - Visible = true, - }); - } - } - - foreach (var layerDatum in layerData) - { - var anyTextureAttempted = false; - var layer = new Layer(this); - if (!string.IsNullOrWhiteSpace(layerDatum.RsiPath)) - { - var path = TextureRoot / layerDatum.RsiPath; - try - { - layer.RSI = resourceCache.GetResource(path).RSI; - } - catch - { - Logger.ErrorS(LogCategory, "Unable to load layer RSI '{0}'.", path); - } - } - - if (!string.IsNullOrWhiteSpace(layerDatum.State)) - { - anyTextureAttempted = true; - var theRsi = layer.RSI ?? BaseRSI; - if (theRsi == null) - { - Logger.ErrorS(LogCategory, - "Layer has no RSI to load states from." - + "cannot use 'state' property. Prototype: '{0}'", Owner.Prototype?.ID); - } - else - { - var stateid = new RSI.StateId(layerDatum.State); - layer.State = stateid; - if (theRsi.TryGetState(stateid, out var state)) - { - // Always use south because this layer will be cached in the serializer. - layer.AnimationTimeLeft = state.GetDelay(0); - } - else - { - Logger.ErrorS(LogCategory, - $"State '{stateid}' not found in RSI: '{theRsi.Path}'.", - stateid); - } - } - } - - if (!string.IsNullOrWhiteSpace(layerDatum.TexturePath)) - { - anyTextureAttempted = true; - if (layer.State.IsValid) - { - Logger.ErrorS(LogCategory, - "Cannot specify 'texture' on a layer if it has an RSI state specified." - ); - } - else - { - layer.Texture = - resourceCache.GetResource(TextureRoot / layerDatum.TexturePath); - } - } - - if (!string.IsNullOrWhiteSpace(layerDatum.Shader)) - { - if (prototypes.TryIndex(layerDatum.Shader, out var prototype)) - { - layer.Shader = prototype.Instance(); - } - else - { - Logger.ErrorS(LogCategory, - "Shader prototype '{0}' does not exist. Prototype: '{1}'", - layerDatum.Shader, Owner.Prototype?.ID); - } - } - - layer.Color = layerDatum.Color; - layer.Rotation = layerDatum.Rotation; - // If neither state: nor texture: were provided we assume that they want a blank invisible layer. - layer.Visible = anyTextureAttempted && layerDatum.Visible; - layer.Scale = layerDatum.Scale; - - layers.Add(layer); - - if (layerDatum.MapKeys != null) - { - var index = layers.Count - 1; - foreach (var keyString in layerDatum.MapKeys) - { - object key; - if (reflectionManager.TryParseEnumReference(keyString, out var @enum)) - { - key = @enum; - } - else - { - key = keyString; - } - - if (layerMap.ContainsKey(key)) - { - Logger.ErrorS(LogCategory, "Duplicate layer map key definition: {0}", key); - continue; - } - - layerMap.Add(key, index); - } - } - } - - Layers = layers; - LayerMap = layerMap; - _layerMapShared = true; - serializer.SetCacheData(LayerSerializationCache, CloneLayers(Layers)); - serializer.SetCacheData(LayerMapSerializationCache, layerMap); - UpdateIsInert(); - } - public override void OnRemove() { base.OnRemove(); @@ -1663,7 +1655,7 @@ namespace Robust.Client.GameObjects Flip = 3, } - private class Layer : ISpriteLayer + public class Layer : ISpriteLayer { [ViewVariables] private readonly SpriteComponent _parent; @@ -1730,6 +1722,22 @@ namespace Robust.Client.GameObjects RSI.StateId ISpriteLayer.RsiState { get => State; set => SetState(value); } Texture? ISpriteLayer.Texture { get => Texture; set => SetTexture(value); } + public PrototypeLayerData ToPrototypeData() + { + return new PrototypeLayerData + { + Color = Color, + Rotation = Rotation, + Scale = Scale, + //todo Shader = Shader, + State = State.Name, + Visible = Visible, + RsiPath = RSI?.Path?.ToString(), + //todo TexturePath = Textur + //todo MapKeys + }; + } + bool ISpriteLayer.Visible { get => Visible; @@ -2000,7 +2008,6 @@ namespace Robust.Client.GameObjects } return state; - } } @@ -2051,7 +2058,6 @@ namespace Robust.Client.GameObjects if (!anyTexture) yield return resourceCache.GetFallback().Texture; - } public static IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache) @@ -2098,6 +2104,7 @@ namespace Robust.Client.GameObjects public T AddComponent() where T : Component, new() { var typeFactory = IoCManager.Resolve(); + var serializationManager = IoCManager.Resolve(); var comp = (T) typeFactory.CreateInstanceUnchecked(typeof(T)); _components[typeof(T)] = comp; comp.Owner = this; @@ -2107,9 +2114,9 @@ namespace Robust.Client.GameObjects _components[typeof(ISpriteComponent)] = comp; } - if (Prototype != null && Prototype.Components.TryGetValue(comp.Name, out var node)) + if (Prototype != null && Prototype.TryGetComponent(comp.Name, out var node)) { - comp.ExposeData(YamlObjectSerializer.NewReader(node)); + comp = serializationManager.Copy(node, comp)!; } return comp; diff --git a/Robust.Client/GameObjects/Components/UserInterface/ClientUserInterfaceComponent.cs b/Robust.Client/GameObjects/Components/UserInterface/ClientUserInterfaceComponent.cs index d1250eea8..d11da311d 100644 --- a/Robust.Client/GameObjects/Components/UserInterface/ClientUserInterfaceComponent.cs +++ b/Robust.Client/GameObjects/Components/UserInterface/ClientUserInterfaceComponent.cs @@ -6,41 +6,31 @@ using Robust.Shared.Network; using Robust.Shared.Players; using Robust.Shared.Reflection; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; namespace Robust.Client.GameObjects { - public class ClientUserInterfaceComponent : SharedUserInterfaceComponent + public class ClientUserInterfaceComponent : SharedUserInterfaceComponent, ISerializationHooks { + [Dependency] private readonly IReflectionManager _reflectionManager = default!; + [Dependency] private readonly IDynamicTypeFactory _dynamicTypeFactory = default!; + private readonly Dictionary _openInterfaces = new(); - private Dictionary _interfaceData = default!; -#pragma warning disable 649 - [Dependency] private readonly IReflectionManager _reflectionManager = default!; - [Dependency] private readonly IDynamicTypeFactory _dynamicTypeFactory = default!; -#pragma warning restore 649 + private readonly Dictionary _interfaces = new(); - public override void ExposeData(ObjectSerializer serializer) + [DataField("interfaces", readOnly: true)] + private List _interfaceData = new(); + + void ISerializationHooks.AfterDeserialization() { - base.ExposeData(serializer); + _interfaces.Clear(); - const string cache = "ui_cache"; - - if (serializer.TryGetCacheData>(cache, out var interfaceData)) + foreach (var data in _interfaceData) { - _interfaceData = interfaceData; - return; + _interfaces[data.UiKey] = data; } - - var data = serializer.ReadDataFieldCached("interfaces", new List()); - interfaceData = new Dictionary(); - foreach (var prototypeData in data) - { - interfaceData[prototypeData.UiKey] = prototypeData; - } - - serializer.SetCacheData(cache, interfaceData); - _interfaceData = interfaceData; } public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, @@ -81,7 +71,7 @@ namespace Robust.Client.GameObjects private void OpenInterface(BoundInterfaceMessageWrapMessage wrapped) { - var data = _interfaceData[wrapped.UiKey]; + var data = _interfaces[wrapped.UiKey]; // TODO: This type should be cached, but I'm too lazy. var type = _reflectionManager.LooseGetType(data.ClientType); var boundInterface = (BoundUserInterface) _dynamicTypeFactory.CreateInstance(type, new[]{this, wrapped.UiKey}); diff --git a/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs b/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs index e23c892ef..65ef1bb63 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Buffers; using OpenToolkit.Graphics.OpenGL4; using Robust.Client.GameObjects; -using Robust.Client.ResourceManagement.ResourceTypes; +using Robust.Client.ResourceManagement; using Robust.Shared.GameObjects; using Robust.Shared.Log; using Robust.Shared.Map; diff --git a/Robust.Client/Graphics/Clyde/Clyde.Shaders.cs b/Robust.Client/Graphics/Clyde/Clyde.Shaders.cs index 2f2a82c82..592ad452d 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.Shaders.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.Shaders.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Text; using OpenToolkit.Graphics.OpenGL4; -using Robust.Client.ResourceManagement.ResourceTypes; +using Robust.Client.ResourceManagement; using Robust.Shared.Maths; using Robust.Shared.Utility; using StencilOp = Robust.Client.Graphics.StencilOp; diff --git a/Robust.Client/Graphics/Shaders/ShaderPrototype.cs b/Robust.Client/Graphics/Shaders/ShaderPrototype.cs index 09ba9f2fa..8e498aac0 100644 --- a/Robust.Client/Graphics/Shaders/ShaderPrototype.cs +++ b/Robust.Client/Graphics/Shaders/ShaderPrototype.cs @@ -1,23 +1,30 @@ using System; using System.Collections.Generic; using Robust.Client.ResourceManagement; -using Robust.Client.ResourceManagement.ResourceTypes; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Utility; +using Robust.Shared.ViewVariables; using YamlDotNet.RepresentationModel; namespace Robust.Client.Graphics { [Prototype("shader")] - public sealed class ShaderPrototype : IPrototype + public sealed class ShaderPrototype : IPrototype, ISerializationHooks { - [Dependency] private readonly IClydeInternal _clyde = default!; [Dependency] private readonly IResourceCache _resourceCache = default!; - public string ID { get; private set; } = default!; + [ViewVariables] + [field: DataField("id", required: true)] + public string ID { get; } = default!; + + [ViewVariables] + [field: DataField("parent")] + public string? Parent { get; } private ShaderKind Kind; @@ -31,11 +38,14 @@ namespace Robust.Client.Graphics private ShaderInstance? _cachedInstance; private bool _stencilEnabled; - private int _stencilRef; - private int _stencilReadMask = unchecked((int) uint.MaxValue); - private int _stencilWriteMask = unchecked((int) uint.MaxValue); - private StencilFunc _stencilFunc = StencilFunc.Always; - private StencilOp _stencilOp = StencilOp.Keep; + private int _stencilRef => StencilDataHolder?.StencilRef ?? 0; + private int _stencilReadMask => StencilDataHolder?.ReadMask ?? unchecked((int) uint.MaxValue); + private int _stencilWriteMask => StencilDataHolder?.WriteMask ?? unchecked((int) uint.MaxValue); + private StencilFunc _stencilFunc => StencilDataHolder?.StencilFunc ?? StencilFunc.Always; + private StencilOp _stencilOp => StencilDataHolder?.StencilOp ?? StencilOp.Keep; + + [DataField("stencil")] + private StencilData? StencilDataHolder; /// /// Retrieves a ready-to-use instance of this shader. @@ -64,12 +74,12 @@ namespace Robust.Client.Graphics switch (Kind) { case ShaderKind.Source: - instance = _clyde.InstanceShader(Source!.ClydeHandle); + instance = IoCManager.Resolve().InstanceShader(Source!.ClydeHandle); _applyDefaultParameters(instance); break; case ShaderKind.Canvas: - instance = _clyde.InstanceShader(CompiledCanvasShader); + instance = IoCManager.Resolve().InstanceShader(CompiledCanvasShader); break; default: @@ -95,135 +105,108 @@ namespace Robust.Client.Graphics return Instance().Duplicate(); } - public void LoadFrom(YamlMappingNode mapping) - { - ID = mapping.GetNode("id").ToString(); + [DataField("kind", readOnly: true, required: true)] private string _rawKind = default!; + [DataField("path", readOnly: true)] private ResourcePath? path; + [DataField("params", readOnly: true)] private Dictionary? paramMapping; + [DataField("light_mode", readOnly: true)] private string? rawMode; + [DataField("blend_mode", readOnly: true)] private string? rawBlendMode; - var kind = mapping.GetNode("kind").AsString(); - switch (kind) + void ISerializationHooks.AfterDeserialization() + { + switch (_rawKind) { case "source": Kind = ShaderKind.Source; - ReadSourceKind(mapping); + if (path == null) throw new InvalidOperationException(); + Source = IoCManager.Resolve().GetResource(path); + + if (paramMapping != null) + { + ShaderParams = new Dictionary(); + foreach (var item in paramMapping!) + { + var name = item.Key; + if (!Source.ParsedShader.Uniforms.TryGetValue(name, out var uniformDefinition)) + { + Logger.ErrorS("shader", "Shader param '{0}' does not exist on shader '{1}'", name, path); + continue; + } + + var value = _parseUniformValue(item.Value, uniformDefinition.Type.Type); + ShaderParams.Add(name, value); + } + } break; case "canvas": Kind = ShaderKind.Canvas; - ReadCanvasKind(mapping); + var source = ""; + + if(rawMode != null) + { + switch (rawMode) + { + case "normal": + break; + + case "unshaded": + source += "light_mode unshaded;\n"; + break; + + default: + throw new InvalidOperationException($"Invalid light mode: '{rawMode}'"); + } + } + + if(rawBlendMode != null){ + switch (rawBlendMode) + { + case "mix": + source += "blend_mode mix;\n"; + break; + + case "add": + source += "blend_mode add;\n"; + break; + + case "subtract": + source += "blend_mode subtract;\n"; + break; + + case "multiply": + source += "blend_mode multiply;\n"; + break; + + default: + throw new InvalidOperationException($"Invalid blend mode: '{rawBlendMode}'"); + } + } + + source += "void fragment() {\n COLOR = zTexture(UV);\n}"; + + var preset = ShaderParser.Parse(source, _resourceCache); + CompiledCanvasShader = IoCManager.Resolve().LoadShader(preset, $"canvas_preset_{ID}"); break; default: - throw new InvalidOperationException($"Invalid shader kind: '{kind}'"); + throw new InvalidOperationException($"Invalid shader kind: '{_rawKind}'"); } - // Load stencil data. - if (mapping.TryGetNode("stencil", out YamlMappingNode? stencilData)) - { - ReadStencilData(stencilData); - } + if (StencilDataHolder != null) _stencilEnabled = true; } - private void ReadStencilData(YamlMappingNode stencilData) + [DataDefinition] + public class StencilData { - _stencilEnabled = true; + [DataField("ref")] public int StencilRef; - if (stencilData.TryGetNode("ref", out var dataNode)) - { - _stencilRef = dataNode.AsInt(); - } + [DataField("op")] public StencilOp StencilOp; - if (stencilData.TryGetNode("op", out dataNode)) - { - _stencilOp = dataNode.AsEnum(); - } + [DataField("func")] public StencilFunc StencilFunc; - if (stencilData.TryGetNode("func", out dataNode)) - { - _stencilFunc = dataNode.AsEnum(); - } + [DataField("readMask")] public int ReadMask = unchecked((int) uint.MaxValue); - if (stencilData.TryGetNode("readMask", out dataNode)) - { - _stencilReadMask = dataNode.AsInt(); - } - - if (stencilData.TryGetNode("writeMask", out dataNode)) - { - _stencilWriteMask = dataNode.AsInt(); - } - } - - private void ReadSourceKind(YamlMappingNode mapping) - { - var path = mapping.GetNode("path").AsResourcePath(); - Source = _resourceCache.GetResource(path); - if (mapping.TryGetNode("params", out var paramMapping)) - { - ShaderParams = new Dictionary(); - foreach (var item in paramMapping) - { - var name = item.Key.AsString(); - if (!Source.ParsedShader.Uniforms.TryGetValue(name, out var uniformDefinition)) - { - Logger.ErrorS("shader", "Shader param '{0}' does not exist on shader '{1}'", name, path); - continue; - } - - var value = _parseUniformValue(item.Value, uniformDefinition.Type.Type); - ShaderParams.Add(name, value); - } - } - } - - private void ReadCanvasKind(YamlMappingNode mapping) - { - var source = ""; - - if (mapping.TryGetNode("light_mode", out var node)) - { - switch (node.AsString()) - { - case "normal": - break; - - case "unshaded": - source += "light_mode unshaded;\n"; - break; - - default: - throw new InvalidOperationException($"Invalid light mode: '{node.AsString()}'"); - } - } - - if (mapping.TryGetNode("blend_mode", out node)) - { - switch (node.AsString()) - { - case "mix": - source += "blend_mode mix;\n"; - break; - - case "add": - source += "blend_mode add;\n"; - break; - - case "subtract": - source += "blend_mode subtract;\n"; - break; - - case "multiply": - source += "blend_mode multiply;\n"; - break; - - default: - throw new InvalidOperationException($"Invalid blend mode: '{node.AsString()}'"); - } - } - - source += "void fragment() {\n COLOR = zTexture(UV);\n}"; - - var preset = ShaderParser.Parse(source, _resourceCache); - CompiledCanvasShader = _clyde.LoadShader(preset, $"canvas_preset_{ID}"); + [DataField("writeMask")] public int WriteMask = unchecked((int) uint.MaxValue); } private static object _parseUniformValue(YamlNode node, ShaderDataType dataType) diff --git a/Robust.Client/Input/InputManager.cs b/Robust.Client/Input/InputManager.cs index c11c12c4b..77eb53ecd 100644 --- a/Robust.Client/Input/InputManager.cs +++ b/Robust.Client/Input/InputManager.cs @@ -19,6 +19,8 @@ using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Reflection; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; using YamlDotNet.Core; @@ -117,8 +119,8 @@ namespace Robust.Client.Input public void SaveToUserData() { - var mapping = new YamlMappingNode(); - var ser = YamlObjectSerializer.NewWriter(mapping); + var mapping = new MappingDataNode(); + var serializationManager = IoCManager.Resolve(); var modifiedBindings = _modifiedKeyFunctions .Select(p => _bindingsByFunction[p]) @@ -141,15 +143,13 @@ namespace Robust.Client.Input .Where(p => _bindingsByFunction[p].Count == 0) .ToArray(); - var version = 1; - - ser.DataField(ref version, "version", 1); - ser.DataField(ref modifiedBindings, "binds", Array.Empty()); - ser.DataField(ref leaveEmpty, "leaveEmpty", Array.Empty()); + mapping.AddNode("version", new ValueDataNode("1")); + mapping.AddNode("binds", serializationManager.WriteValue(modifiedBindings)); + mapping.AddNode("leaveEmpty", serializationManager.WriteValue(leaveEmpty)); var path = new ResourcePath(KeybindsPath); using var writer = new StreamWriter(_resourceMan.UserData.Create(path)); - var stream = new YamlStream {new(mapping)}; + var stream = new YamlStream {new(mapping.ToMappingNode())}; stream.Save(new YamlMappingFix(new Emitter(writer)), false); } @@ -416,12 +416,14 @@ namespace Robust.Client.Input var mapping = (YamlMappingNode) yamlStream.Documents[0].RootNode; - var baseSerializer = YamlObjectSerializer.NewReader(mapping); + var serializationManager = IoCManager.Resolve(); + var robustMapping = mapping.ToDataNode() as MappingDataNode; + if (robustMapping == null) throw new InvalidOperationException(); - var foundBinds = baseSerializer.TryReadDataField("binds", out var baseKeyRegs); - - if (foundBinds && baseKeyRegs != null && baseKeyRegs.Length > 0) + if (robustMapping.TryGetNode("binds", out var BaseKeyRegsNode)) { + var baseKeyRegs = serializationManager.ReadValueOrThrow(BaseKeyRegsNode); + foreach (var reg in baseKeyRegs) { if (!NetworkBindMap.FunctionExists(reg.Function.FunctionName)) @@ -447,11 +449,11 @@ namespace Robust.Client.Input } } - if (userData) + if (userData && robustMapping.TryGetNode("leaveEmpty", out var node)) { - var foundLeaveEmpty = baseSerializer.TryReadDataField("leaveEmpty", out var leaveEmpty); + var leaveEmpty = serializationManager.ReadValueOrThrow(node); - if (foundLeaveEmpty && leaveEmpty != null && leaveEmpty.Length > 0) + if (leaveEmpty.Length > 0) { // Adding to _modifiedKeyFunctions means that these keybinds won't be loaded from the base file. // Because they've been explicitly cleared. diff --git a/Robust.Client/Input/KeyBindingRegistration.cs b/Robust.Client/Input/KeyBindingRegistration.cs index bbe1c1721..ce0e115b6 100644 --- a/Robust.Client/Input/KeyBindingRegistration.cs +++ b/Robust.Client/Input/KeyBindingRegistration.cs @@ -1,33 +1,30 @@ -using Robust.Shared.Input; -using Robust.Shared.Serialization; +using Robust.Shared.Input; +using Robust.Shared.Serialization.Manager.Attributes; namespace Robust.Client.Input { - public struct KeyBindingRegistration : IExposeData + [DataDefinition] + public class KeyBindingRegistration { + [DataField("function")] public BoundKeyFunction Function; - public KeyBindingType Type; + [DataField("type")] + public KeyBindingType Type = KeyBindingType.State; + [DataField("key")] public Keyboard.Key BaseKey; + [DataField("mod1")] public Keyboard.Key Mod1; + [DataField("mod2")] public Keyboard.Key Mod2; + [DataField("mod3")] public Keyboard.Key Mod3; + [DataField("priority")] public int Priority; + [DataField("canFocus")] public bool CanFocus; + [DataField("canRepeat")] public bool CanRepeat; + [DataField("allowSubCombs")] public bool AllowSubCombs; - - void IExposeData.ExposeData(ObjectSerializer serializer) - { - serializer.DataField(ref Function, "function", default); - serializer.DataField(ref Type, "type", KeyBindingType.State); - serializer.DataField(ref BaseKey, "key", default); - serializer.DataField(ref Mod1, "mod1", default); - serializer.DataField(ref Mod2, "mod2", default); - serializer.DataField(ref Mod3, "mod3", default); - serializer.DataField(ref Priority, "priority", 0); - serializer.DataField(ref CanFocus, "canFocus", false); - serializer.DataField(ref CanRepeat, "canRepeat", false); - serializer.DataField(ref AllowSubCombs, "allowSubCombs", false); - } } } diff --git a/Robust.Client/Placement/PlacementManager.cs b/Robust.Client/Placement/PlacementManager.cs index 597d9435e..6b42db447 100644 --- a/Robust.Client/Placement/PlacementManager.cs +++ b/Robust.Client/Placement/PlacementManager.cs @@ -1,27 +1,25 @@ 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.IoC; -using Robust.Shared.Prototypes; -using Robust.Shared.Maths; -using Robust.Shared.Map; -using Robust.Shared.Network.Messages; -using Robust.Client.Graphics; -using Robust.Client.GameObjects; -using Robust.Client.Input; -using Robust.Client.Player; 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.Utility; -using Robust.Shared.Serialization; using Robust.Shared.Timing; -using Robust.Shared.Log; +using Robust.Shared.Utility; namespace Robust.Client.Placement { @@ -126,14 +124,6 @@ namespace Robust.Client.Placement if (value != null) { PlacementOffset = value.PlacementOffset; - - if (value.Components.ContainsKey("BoundingBox") && value.Components.ContainsKey("Physics")) - { - var map = value.Components["BoundingBox"]; - var serializer = YamlObjectSerializer.NewReader(map); - serializer.DataField(ref _colliderAABB, "aabb", new Box2(0f, 0f, 0f, 0f)); - return; - } } _colliderAABB = new Box2(0f, 0f, 0f, 0f); diff --git a/Robust.Client/ResourceManagement/ResourceTypes/ShaderSourceResource.cs b/Robust.Client/ResourceManagement/ResourceTypes/ShaderSourceResource.cs index b800d389e..86d77ee8d 100644 --- a/Robust.Client/ResourceManagement/ResourceTypes/ShaderSourceResource.cs +++ b/Robust.Client/ResourceManagement/ResourceTypes/ShaderSourceResource.cs @@ -5,7 +5,7 @@ using Robust.Shared.ContentPack; using Robust.Shared.IoC; using Robust.Shared.Utility; -namespace Robust.Client.ResourceManagement.ResourceTypes +namespace Robust.Client.ResourceManagement { /// /// Loads the **source code** of a shader. diff --git a/Robust.Client/Serialization/AppearanceVisualizerSerializer.cs b/Robust.Client/Serialization/AppearanceVisualizerSerializer.cs new file mode 100644 index 000000000..30da946a4 --- /dev/null +++ b/Robust.Client/Serialization/AppearanceVisualizerSerializer.cs @@ -0,0 +1,73 @@ +using Robust.Client.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Reflection; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Client.Serialization +{ + [TypeSerializer] + public class AppearanceVisualizerSerializer : ITypeSerializer + { + public DeserializationResult Read(ISerializationManager serializationManager, MappingDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context = null) + { + if (!node.TryGetNode("type", out var typeNode)) + throw new InvalidMappingException("No type specified for AppearanceVisualizer!"); + + if (typeNode is not ValueDataNode typeValueDataNode) + throw new InvalidMappingException("Type node not a value node for AppearanceVisualizer!"); + + var type = IoCManager.Resolve() + .YamlTypeTagLookup(typeof(AppearanceVisualizer), typeValueDataNode.Value); + if (type == null) + throw new InvalidMappingException( + $"Invalid type {typeValueDataNode.Value} specified for AppearanceVisualizer!"); + + var newNode = (MappingDataNode)node.Copy(); + newNode.RemoveNode("type"); + return serializationManager.Read(type, newNode, context, skipHook); + } + + public ValidationNode Validate(ISerializationManager serializationManager, MappingDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context) + { + if (!node.TryGetNode("type", out var typeNode) || typeNode is not ValueDataNode valueNode) + { + return new ErrorNode(node, "Missing/Invalid type", true); + } + + var reflectionManager = IoCManager.Resolve(); + var type = reflectionManager.YamlTypeTagLookup(typeof(AppearanceVisualizer), valueNode.Value); + + if (type == null) + { + return new ErrorNode(node, $"Failed to resolve type: {valueNode.Value}", true); + } + + return serializationManager.ValidateNode(type, node.CopyCast().RemoveNode("type")); + } + + public DataNode Write(ISerializationManager serializationManager, AppearanceVisualizer value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + var mapping = serializationManager.WriteValueAs(value.GetType(), value, alwaysWrite, context); + mapping.AddNode("type", new ValueDataNode(value.GetType().Name)); + return mapping; + } + + public AppearanceVisualizer Copy(ISerializationManager serializationManager, AppearanceVisualizer source, + AppearanceVisualizer target, bool skipHook, ISerializationContext? context = null) + { + return serializationManager.Copy(source, target, context)!; + } + } +} diff --git a/Robust.Client/Utility/SpriteSpecifierExt.cs b/Robust.Client/Utility/SpriteSpecifierExt.cs index 906434544..6a1e8d088 100644 --- a/Robust.Client/Utility/SpriteSpecifierExt.cs +++ b/Robust.Client/Utility/SpriteSpecifierExt.cs @@ -26,12 +26,10 @@ namespace Robust.Client.Utility { if (cache.TryGetResource( SharedSpriteComponent.TextureRoot / rsiSpecifier.RsiPath, - out var theRsi)) + out var theRsi) && + theRsi.RSI.TryGetState(rsiSpecifier.RsiState, out var state)) { - if (theRsi.RSI.TryGetState(rsiSpecifier.RsiState, out var state)) - { - return state; - } + return state; } Logger.Error("Failed to load RSI {0}", rsiSpecifier.RsiPath); diff --git a/Robust.Generators.UnitTesting/AnalyzerTest.cs b/Robust.Generators.UnitTesting/AnalyzerTest.cs new file mode 100644 index 000000000..82960e230 --- /dev/null +++ b/Robust.Generators.UnitTesting/AnalyzerTest.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using NUnit.Framework; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Robust.Generators.UnitTesting +{ + [Parallelizable] + public abstract class AnalyzerTest + { + protected static Assembly GetAssemblyFromCompilation(Compilation newComp) + { + using var stream = new MemoryStream(); + newComp.Emit(stream); + var assembly = Assembly.Load(stream.ToArray()); + return assembly; + } + + protected static Compilation CreateCompilation(string source) + { + var dd = typeof(Enumerable).GetTypeInfo().Assembly.Location; + var coreDir = Directory.GetParent(dd) ?? throw new Exception("Couldn't find location of coredir"); + + var references = new[] + { + MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(typeof(Enumerable).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(typeof(Dictionary<,>).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(typeof(DataFieldAttribute).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(coreDir.FullName + Path.DirectorySeparatorChar + "mscorlib.dll"), + MetadataReference.CreateFromFile(coreDir.FullName + Path.DirectorySeparatorChar + + "System.Runtime.dll"), + MetadataReference.CreateFromFile(coreDir.FullName + Path.DirectorySeparatorChar + + "System.Collections.dll"), + }; + + var syntaxTree = CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(LanguageVersion.Preview)); + return CSharpCompilation.Create( + "comp", + new[] {syntaxTree}, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + } + + protected static (Compilation, ImmutableArray diagnostics) RunGenerators(Compilation c, + params ISourceGenerator[] gens) + { + var driver = CSharpGeneratorDriver.Create( + ImmutableArray.Create(gens), + ImmutableArray.Empty, + (CSharpParseOptions) c.SyntaxTrees.First().Options); + driver.RunGeneratorsAndUpdateCompilation(c, out var d, out var diagnostics); + return (d, diagnostics); + } + } +} diff --git a/Robust.Generators.UnitTesting/DataClassTests.cs b/Robust.Generators.UnitTesting/DataClassTests.cs new file mode 100644 index 000000000..9f887c759 --- /dev/null +++ b/Robust.Generators.UnitTesting/DataClassTests.cs @@ -0,0 +1,82 @@ +using System.Linq; +using NUnit.Framework; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Robust.Generators.UnitTesting +{ + public class DataClassTests : AnalyzerTest + { + [Test] + public void DCTest() + { + const string source = @" +using System.Collections.Generic; +using Robust.Shared.Prototypes; +//using Robust.Shared.Serialization; +using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Test{ + [DataClass] + public class TestClass{ + [DataFieldWithConstant(""drawdepth"", typeof(DrawDepthTag))] + private int _drawDepth = DrawDepthTag.Default; + [DataField(""myList"")] + public List testList; + [DataField(""myList"")] + public string abc = ""testing""; + [DataField(""drawdepth"", constType: typeof(TestClass))] + public int test; + } +} +"; + var comp = CreateCompilation(source); + + //Assert.IsEmpty(comp.GetDiagnostics()); + + var (newcomp, generatorDiags) = RunGenerators(comp, new DataClassGenerator()); + + Assert.IsEmpty(generatorDiags); + + var type = newcomp.GetTypeByMetadataName("Test.TestClass_AUTODATA"); + Assert.NotNull(type); + + var memberNames = type.MemberNames.ToArray(); + + // 3 properties + Assert.That(memberNames, Has.Length.EqualTo(3)); + Assert.That(memberNames, Contains.Item("testList")); + Assert.That(memberNames, Contains.Item("abc")); + Assert.That(memberNames, Contains.Item("test")); + + var members = type.GetMembers(); + + // 3 properties + constructor + Assert.That(members, Has.Length.EqualTo(4)); + + var memberDictionary = members.ToDictionary(m => m.Name, m => m); + var yamlFieldNamespace = typeof(DataFieldAttribute).FullName; + + Assert.NotNull(yamlFieldNamespace); + + var yamlFieldAttribute = comp.GetTypeByMetadataName(yamlFieldNamespace); + + var testListYamlAttribute = memberDictionary["testList"].GetAttribute(yamlFieldAttribute); + Assert.NotNull(testListYamlAttribute); + Assert.That(testListYamlAttribute.ConstructorArguments[0].Value, Is.EqualTo("myList")); + + var abcYamlAttribute = memberDictionary["abc"].GetAttribute(yamlFieldAttribute); + Assert.NotNull(abcYamlAttribute); + Assert.That(abcYamlAttribute.ConstructorArguments[0].Value, Is.EqualTo("myList")); + + var testYamlAttribute = memberDictionary["test"].GetAttribute(yamlFieldAttribute); + Assert.NotNull(testYamlAttribute); + Assert.That(testYamlAttribute.ConstructorArguments[0].Value, Is.EqualTo("drawdepth")); + Assert.NotNull(testYamlAttribute.ConstructorArguments[3].Value); + Assert.That(testYamlAttribute.ConstructorArguments[3].Value.ToString(), Is.EqualTo("Test.TestClass")); + + //TODO Check for dataclass & if its correct + } + } +} diff --git a/Robust.Generators.UnitTesting/Robust.Generators.UnitTesting.csproj b/Robust.Generators.UnitTesting/Robust.Generators.UnitTesting.csproj new file mode 100644 index 000000000..248614ee1 --- /dev/null +++ b/Robust.Generators.UnitTesting/Robust.Generators.UnitTesting.csproj @@ -0,0 +1,21 @@ + + + + net5.0 + + + + + + + + + + + + + + + + + diff --git a/Robust.Generators/Robust.Generators.csproj b/Robust.Generators/Robust.Generators.csproj new file mode 100644 index 000000000..53ebd2fcd --- /dev/null +++ b/Robust.Generators/Robust.Generators.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/Robust.Server/BaseServer.cs b/Robust.Server/BaseServer.cs index e6f9e1be4..01c181df3 100644 --- a/Robust.Server/BaseServer.cs +++ b/Robust.Server/BaseServer.cs @@ -1,23 +1,10 @@ -using System; +using System; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Threading; using Prometheus; using Robust.Server.Console; -using Robust.Shared.Configuration; -using Robust.Shared.ContentPack; -using Robust.Shared.IoC; -using Robust.Shared.Log; -using Robust.Shared.Prototypes; -using Robust.Server.ViewVariables; -using Robust.Shared.Asynchronous; -using Robust.Shared.Timing; -using Robust.Shared.Utility; -using Robust.Shared.Exceptions; -using Robust.Server.Scripting; -using Robust.Server.ServerStatus; -using Robust.Shared; using Robust.Server.DataMetrics; using Robust.Server.Debugging; using Robust.Server.GameObjects; @@ -25,12 +12,26 @@ using Robust.Server.GameStates; using Robust.Server.Log; using Robust.Server.Placement; using Robust.Server.Player; +using Robust.Server.Scripting; +using Robust.Server.ServerStatus; using Robust.Server.Utility; +using Robust.Server.ViewVariables; +using Robust.Shared; +using Robust.Shared.Asynchronous; +using Robust.Shared.Configuration; +using Robust.Shared.ContentPack; +using Robust.Shared.Exceptions; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; using Robust.Shared.Localization; +using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Network; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Timing; +using Robust.Shared.Utility; using Serilog.Debugging; using Serilog.Sinks.Loki; using Stopwatch = Robust.Shared.Timing.Stopwatch; @@ -298,6 +299,8 @@ namespace Robust.Server _entities.Initialize(); + IoCManager.Resolve().Initialize(); + // because of 'reasons' this has to be called after the last assembly is loaded // otherwise the prototypes will be cleared var prototypeManager = IoCManager.Resolve(); diff --git a/Robust.Server/GameObjects/Components/Eye/EyeComponent.cs b/Robust.Server/GameObjects/Components/Eye/EyeComponent.cs index 487b0c931..27640ee48 100644 --- a/Robust.Server/GameObjects/Components/Eye/EyeComponent.cs +++ b/Robust.Server/GameObjects/Components/Eye/EyeComponent.cs @@ -1,14 +1,19 @@ using Robust.Shared.GameObjects; using Robust.Shared.Maths; +using Robust.Shared.Prototypes; using Robust.Shared.Players; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.ViewVariables; namespace Robust.Server.GameObjects { public class EyeComponent : SharedEyeComponent { - private bool _drawFov; - private Vector2 _zoom; + [DataField("drawFov")] + private bool _drawFov = true; + [DataField("zoom")] + private Vector2 _zoom = Vector2.One/2f; private Vector2 _offset; private Angle _rotation; @@ -68,13 +73,5 @@ namespace Robust.Server.GameObjects { return new EyeComponentState(DrawFov, Zoom, Offset, Rotation); } - - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - - serializer.DataField(ref _zoom, "zoom", Vector2.One/2f); - serializer.DataFieldCached(ref _drawFov, "drawFov", true); - } } } diff --git a/Robust.Server/GameObjects/Components/Light/PointLightComponent.cs b/Robust.Server/GameObjects/Components/Light/PointLightComponent.cs index 799c7f9f8..64bc90840 100644 --- a/Robust.Server/GameObjects/Components/Light/PointLightComponent.cs +++ b/Robust.Server/GameObjects/Components/Light/PointLightComponent.cs @@ -1,7 +1,9 @@ using Robust.Shared.GameObjects; using Robust.Shared.Maths; +using Robust.Shared.Prototypes; using Robust.Shared.Players; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Robust.Server.GameObjects @@ -10,10 +12,14 @@ namespace Robust.Server.GameObjects [ComponentReference(typeof(IPointLightComponent))] public class PointLightComponent : Component, IPointLightComponent { - private Color _color; - private bool _enabled; - private float _radius; - private Vector2 _offset; + [DataField("color")] + private Color _color = new(200, 200, 200); + [DataField("enabled")] + private bool _enabled = true; + [DataField("radius")] + private float _radius = 10; + [DataField("offset")] + private Vector2 _offset = Vector2.Zero; public override string Name => "PointLight"; public override uint? NetID => NetIDs.POINT_LIGHT; @@ -73,17 +79,6 @@ namespace Robust.Server.GameObjects } } - /// - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - - serializer.DataField(ref _enabled, "enabled", true); - serializer.DataField(ref _color, "color", new Color(200, 200, 200)); - serializer.DataField(ref _radius, "radius", 10); - serializer.DataField(ref _offset, "offset", Vector2.Zero); - } - public override ComponentState GetComponentState(ICommonSession player) { return new PointLightComponentState(Enabled, Color, Radius, Offset); diff --git a/Robust.Server/GameObjects/Components/Renderable/SpriteComponent.cs b/Robust.Server/GameObjects/Components/Renderable/SpriteComponent.cs index 27c11c144..0a60b88bd 100644 --- a/Robust.Server/GameObjects/Components/Renderable/SpriteComponent.cs +++ b/Robust.Server/GameObjects/Components/Renderable/SpriteComponent.cs @@ -4,27 +4,49 @@ using Robust.Shared.GameObjects; using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth; using Robust.Shared.Log; using Robust.Shared.Maths; +using Robust.Shared.Prototypes; using Robust.Shared.Players; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; namespace Robust.Server.GameObjects { - public class SpriteComponent : SharedSpriteComponent, ISpriteRenderableComponent + public class SpriteComponent : SharedSpriteComponent, ISpriteRenderableComponent, ISerializationHooks { const string LayerSerializationCache = "spritelayersrv"; + [ViewVariables] + [DataField("layers", priority: 2, readOnly: true)] private List Layers = new(); - private bool _visible; + [DataField("visible")] + private bool _visible = true; + + [DataFieldWithConstant("drawdepth", typeof(DrawDepthTag))] private int _drawDepth = DrawDepthTag.Default; - private Vector2 _scale; - private Vector2 _offset; - private Color _color; - private bool _directional; + + [DataField("scale")] + private Vector2 _scale = Vector2.One; + + [DataField("offset")] + private Vector2 _offset = Vector2.Zero; + + [DataField("color")] + private Color _color = Color.White; + + [DataField("directional")] + private bool _directional = true; + + [DataField("sprite")] private string? _baseRSIPath; - private Angle _rotation; + + [DataField("rotation")] + private Angle _rotation = Angle.Zero; + + [DataField("state")] private string? state; + [DataField("texture")] private string? texture; [ViewVariables(VVAccess.ReadWrite)] public int DrawDepth @@ -129,6 +151,31 @@ namespace Robust.Server.GameObjects [ViewVariables] public int LayerCount => Layers.Count; + void ISerializationHooks.AfterDeserialization() + { + if (Layers.Count == 0) + { + if (state != null || texture != null) + { + var layerZeroData = SharedSpriteComponent.PrototypeLayerData.New(); + if (!string.IsNullOrWhiteSpace(state)) + { + layerZeroData.State = state; + } + + if (!string.IsNullOrWhiteSpace(texture)) + { + layerZeroData.TexturePath = texture; + } + + Layers.Insert(0, layerZeroData); + + state = null; + texture = null; + } + } + } + public int AddLayerWithSprite(SpriteSpecifier specifier) { var layer = PrototypeLayerData.New(); @@ -272,7 +319,7 @@ namespace Robust.Server.GameObjects { if (Layers.Count <= layer) { - Logger.ErrorS("go.comp.sprite", "Layer with index '{0}' does not exist, cannot set set! Trace:\n{1}", + Logger.ErrorS("go.comp.sprite", "Layer with index '{0}' does not exist, cannot set state! Trace:\n{1}", layer, Environment.StackTrace); return; } @@ -389,59 +436,6 @@ namespace Robust.Server.GameObjects Dirty(); } - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - - serializer.DataFieldCached(ref _visible, "visible", true); - serializer.DataFieldCached(ref _drawDepth, "drawdepth", DrawDepthTag.Default, WithFormat.Constants()); - serializer.DataFieldCached(ref _offset, "offset", Vector2.Zero); - serializer.DataFieldCached(ref _scale, "scale", Vector2.One); - serializer.DataFieldCached(ref _color, "color", Color.White); - serializer.DataFieldCached(ref _directional, "directional", true); - serializer.DataFieldCached(ref _baseRSIPath, "sprite", null); - serializer.DataFieldCached(ref _rotation, "rotation", Angle.Zero); - - // TODO: Writing? - if (!serializer.Reading) - { - return; - } - - if (serializer.TryGetCacheData>(LayerSerializationCache, out var layers)) - { - Layers = layers.ShallowClone(); - return; - } - - var layerData = - serializer.ReadDataField>("layers", new List()); - - if(layerData.Count == 0){ - var baseState = serializer.ReadDataField("state", null); - var texturePath = serializer.ReadDataField("texture", null); - - if (baseState != null || texturePath != null) - { - var layerZeroData = PrototypeLayerData.New(); - if (!string.IsNullOrWhiteSpace(baseState)) - { - layerZeroData.State = baseState; - } - - if (!string.IsNullOrWhiteSpace(texturePath)) - { - layerZeroData.TexturePath = texturePath; - } - - layerData.Insert(0, layerZeroData); - } - } - - serializer.SetCacheData(LayerSerializationCache, layerData.ShallowClone()); - Layers = layerData; - } - public override ComponentState GetComponentState(ICommonSession player) { return new SpriteComponentState(Visible, DrawDepth, Scale, Rotation, Offset, Color, diff --git a/Robust.Server/GameObjects/Components/UserInterface/ServerUserInterfaceComponent.cs b/Robust.Server/GameObjects/Components/UserInterface/ServerUserInterfaceComponent.cs index 7daf54019..57ce6e8d2 100644 --- a/Robust.Server/GameObjects/Components/UserInterface/ServerUserInterfaceComponent.cs +++ b/Robust.Server/GameObjects/Components/UserInterface/ServerUserInterfaceComponent.cs @@ -10,6 +10,7 @@ using Robust.Shared.Log; using Robust.Shared.Network; using Robust.Shared.Players; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; namespace Robust.Server.GameObjects { @@ -19,27 +20,24 @@ namespace Robust.Server.GameObjects /// /// [PublicAPI] - public sealed class ServerUserInterfaceComponent : SharedUserInterfaceComponent + public sealed class ServerUserInterfaceComponent : SharedUserInterfaceComponent, ISerializationHooks { private readonly Dictionary _interfaces = new(); + [DataField("interfaces", readOnly: true)] + private List _interfaceData = new(); + /// /// Enumeration of all the interfaces this component provides. /// public IEnumerable Interfaces => _interfaces.Values; - public override void ExposeData(ObjectSerializer serializer) + void ISerializationHooks.AfterDeserialization() { - base.ExposeData(serializer); + _interfaces.Clear(); - if (!serializer.Reading) - { - return; - } - - var data = serializer.ReadDataFieldCached("interfaces", new List()); - foreach (var prototypeData in data) + foreach (var prototypeData in _interfaceData) { _interfaces[prototypeData.UiKey] = new BoundUserInterface(prototypeData.UiKey, this); } diff --git a/Robust.Server/GameObjects/Components/VisibilityComponent.cs b/Robust.Server/GameObjects/Components/VisibilityComponent.cs index 092051ba4..ab78407e2 100644 --- a/Robust.Server/GameObjects/Components/VisibilityComponent.cs +++ b/Robust.Server/GameObjects/Components/VisibilityComponent.cs @@ -1,5 +1,7 @@ using Robust.Shared.GameObjects; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Robust.Server.GameObjects @@ -7,6 +9,7 @@ namespace Robust.Server.GameObjects [RegisterComponent] public class VisibilityComponent : Component { + [DataField("layer")] private int _layer = 1; public override string Name => "Visibility"; @@ -20,12 +23,5 @@ namespace Robust.Server.GameObjects get => _layer; set => _layer = value; } - - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - - serializer.DataField(ref _layer, "layer", 1); - } } } diff --git a/Robust.Server/GameObjects/ServerComponentFactory.cs b/Robust.Server/GameObjects/ServerComponentFactory.cs index 12e082900..eef70b36e 100644 --- a/Robust.Server/GameObjects/ServerComponentFactory.cs +++ b/Robust.Server/GameObjects/ServerComponentFactory.cs @@ -50,7 +50,6 @@ namespace Robust.Server.GameObjects #if DEBUG Register(); - Register(); Register(); Register(); #endif diff --git a/Robust.Server/Maps/MapLoader.cs b/Robust.Server/Maps/MapLoader.cs index 0026fecb9..cd894d2b8 100644 --- a/Robust.Server/Maps/MapLoader.cs +++ b/Robust.Server/Maps/MapLoader.cs @@ -1,21 +1,26 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.IO; +using System.Linq; +using JetBrains.Annotations; +using Robust.Server.GameObjects; +using Robust.Shared.ContentPack; +using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map; -using YamlDotNet.RepresentationModel; -using Robust.Shared.Utility; -using Robust.Shared.Serialization; -using Robust.Shared.GameObjects; -using System.Globalization; -using System.Linq; -using Robust.Server.GameObjects; -using Robust.Shared.ContentPack; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; using Robust.Shared.Timing; +using Robust.Shared.Utility; using YamlDotNet.Core; +using YamlDotNet.RepresentationModel; namespace Robust.Server.Maps { @@ -223,7 +228,10 @@ namespace Robust.Server.Maps /// /// Handles the primary bulk of state during the map serialization process. /// - private class MapContext : YamlObjectSerializer.Context, IEntityLoadContext + private class MapContext : ISerializationContext, IEntityLoadContext, + ITypeSerializer, + ITypeSerializer, + ITypeReaderWriter { private readonly IMapManagerInternal _mapManager; private readonly ITileDefinitionManager _tileDefinitionManager; @@ -255,6 +263,10 @@ namespace Robust.Server.Maps private Dictionary? _tileMap; + public Dictionary<(Type, Type), object> TypeReaders { get; } + public Dictionary TypeWriters { get; } + public Dictionary TypeCopiers => TypeWriters; + public bool MapIsPostInit { get; private set; } public MapContext(IMapManagerInternal maps, ITileDefinitionManager tileDefs, @@ -269,6 +281,18 @@ namespace Robust.Server.Maps _prototypeManager = prototypeManager; RootNode = new YamlMappingNode(); + TypeWriters = new Dictionary() + { + {typeof(IEntity), this}, + {typeof(GridId), this}, + {typeof(EntityUid), this} + }; + TypeReaders = new Dictionary<(Type, Type), object>() + { + {(typeof(IEntity), typeof(ValueDataNode)), this}, + {(typeof(GridId), typeof(ValueDataNode)), this}, + {(typeof(EntityUid), typeof(ValueDataNode)), this} + }; } public MapContext(IMapManagerInternal maps, ITileDefinitionManager tileDefs, @@ -286,6 +310,18 @@ namespace Robust.Server.Maps RootNode = node; TargetMap = targetMapId; _prototypeManager = prototypeManager; + TypeWriters = new Dictionary() + { + {typeof(IEntity), this}, + {typeof(GridId), this}, + {typeof(EntityUid), this} + }; + TypeReaders = new Dictionary<(Type, Type), object>() + { + {(typeof(IEntity), typeof(ValueDataNode)), this}, + {(typeof(GridId), typeof(ValueDataNode)), this}, + {(typeof(EntityUid), typeof(ValueDataNode)), this} + }; } // Deserialization @@ -530,7 +566,10 @@ namespace Robust.Server.Maps { foreach (var compData in componentList) { - CurrentReadingEntityComponents[compData["type"].AsString()] = (YamlMappingNode) compData; + var copy = new YamlMappingNode(((YamlMappingNode)compData).AsEnumerable()); + copy.Children.Remove(new YamlScalarNode("type")); + //TODO Paul: maybe replace mapping with datanode + CurrentReadingEntityComponents[compData["type"].AsString()] = copy; } } @@ -687,9 +726,11 @@ namespace Robust.Server.Maps private void WriteEntitySection() { + var serializationManager = IoCManager.Resolve(); var entities = new YamlSequenceNode(); RootNode.Add("entities", entities); + var prototypeCompCache = new Dictionary>(); foreach (var entity in Entities.OrderBy(e => EntityUidMap[e.Uid])) { CurrentWritingEntity = entity; @@ -701,6 +742,14 @@ namespace Robust.Server.Maps if (entity.Prototype != null) { mapping.Add("type", entity.Prototype.ID); + if (!prototypeCompCache.ContainsKey(entity.Prototype.ID)) + { + prototypeCompCache[entity.Prototype.ID] = new Dictionary(); + foreach (var (compType, comp) in entity.Prototype.Components) + { + prototypeCompCache[entity.Prototype.ID].Add(compType, serializationManager.WriteValueAs(comp.GetType(), comp)); + } + } } var components = new YamlSequenceNode(); @@ -710,18 +759,21 @@ namespace Robust.Server.Maps if (component is MapSaveIdComponent) continue; - var compMapping = new YamlMappingNode(); CurrentWritingComponent = component.Name; - var compSerializer = YamlObjectSerializer.NewWriter(compMapping, this); + var compMapping = serializationManager.WriteValueAs(component.GetType(), component, context: this); - component.ExposeData(compSerializer); + if (entity.Prototype != null && prototypeCompCache[entity.Prototype.ID].TryGetValue(component.Name, out var protMapping)) + { + compMapping = compMapping.Except(protMapping); + if(compMapping == null) continue; + } // Don't need to write it if nothing was written! if (compMapping.Children.Count != 0) { + compMapping.AddNode("type", new ValueDataNode(component.Name)); // Something actually got written! - compMapping.Add("type", component.Name); - components.Add(compMapping); + components.Add(compMapping.ToYamlNode()); } } @@ -734,140 +786,32 @@ namespace Robust.Server.Maps } } - public override bool TryNodeToType(YamlNode node, Type type, [NotNullWhen(true)] out object? obj) - { - if (type == typeof(GridId)) - { - if (node.AsString() == "null") - { - obj = GridId.Invalid; - return true; - } - - var val = node.AsInt(); - if (val >= Grids.Count) - { - Logger.ErrorS("map", "Error in map file: found local grid ID '{0}' which does not exist.", val); - } - else - { - obj = Grids[val].Index; - return true; - } - } - - if (type == typeof(EntityUid)) - { - if (node.AsString() == "null") - { - obj = EntityUid.Invalid; - return true; - } - - var val = node.AsInt(); - if (val >= Entities.Count) - { - Logger.ErrorS("map", "Error in map file: found local entity UID '{0}' which does not exist.", - val); - } - else - { - obj = UidEntityMap[val]; - return true; - } - } - - if (typeof(IEntity).IsAssignableFrom(type)) - { - var val = node.AsInt(); - if (val >= Entities.Count) - { - Logger.ErrorS("map", "Error in map file: found local entity UID '{0}' which does not exist.", - val); - } - else - { - obj = Entities[val]; - return true; - } - } - - obj = null; - return false; - } - - public override bool TryTypeToNode(object obj, [NotNullWhen(true)] out YamlNode? node) - { - switch (obj) - { - case GridId gridId: - if (!GridIDMap.TryGetValue(gridId, out var gridMapped)) - { - Logger.WarningS("map", "Cannot write grid ID '{0}', falling back to nullspace.", gridId); - break; - } - else - { - node = new YamlScalarNode(gridMapped.ToString(CultureInfo.InvariantCulture)); - return true; - } - - case EntityUid entityUid: - if (!EntityUidMap.TryGetValue(entityUid, out var entityUidMapped)) - { - // Terrible hack to mute this warning on the grids themselves when serializing blueprints. - if (!IsBlueprintMode || !CurrentWritingEntity!.HasComponent() || - CurrentWritingComponent != "Transform") - { - Logger.WarningS("map", "Cannot write entity UID '{0}'.", entityUid); - } - - node = new YamlScalarNode("null"); - return true; - } - else - { - node = new YamlScalarNode(entityUidMapped.ToString(CultureInfo.InvariantCulture)); - return true; - } - - case IEntity entity: - if (!EntityUidMap.TryGetValue(entity.Uid, out var entityMapped)) - { - Logger.WarningS("map", "Cannot write entity UID '{0}'.", entity.Uid); - break; - } - else - { - node = new YamlScalarNode(entityMapped.ToString(CultureInfo.InvariantCulture)); - return true; - } - } - - node = null; - return false; - } - // Create custom object serializers that will correctly allow data to be overriden by the map file. - ObjectSerializer IEntityLoadContext.GetComponentSerializer(string componentName, YamlMappingNode? protoData) + IComponent IEntityLoadContext.GetComponentData(string componentName, + IComponent? protoData) { if (CurrentReadingEntityComponents == null) { throw new InvalidOperationException(); } - var list = new List(); + var serializationManager = IoCManager.Resolve(); + var factory = IoCManager.Resolve(); + + IComponent data = protoData != null + ? serializationManager.CreateCopy(protoData, this)! + : (IComponent) Activator.CreateInstance(factory.GetRegistration(componentName).Type)!; + if (CurrentReadingEntityComponents.TryGetValue(componentName, out var mapping)) { - list.Add(mapping); + var mapData = (IDeserializedDefinition) serializationManager.Read( + factory.GetRegistration(componentName).Type, + mapping.ToDataNode(), this); + var newData = serializationManager.PopulateDataDefinition(data, mapData); + data = (IComponent) newData.RawValue!; } - if (protoData != null) - { - list.Add(protoData); - } - - return YamlObjectSerializer.NewReader(list, this); + return data; } public IEnumerable GetExtraComponentTypes() @@ -875,34 +819,6 @@ namespace Robust.Server.Maps return CurrentReadingEntityComponents!.Keys; } - public override bool IsValueDefault(string field, T value, WithFormat format) - { - if (CurrentWritingEntity!.Prototype == null) - { - // No prototype, can't be default. - return false; - } - - if (!CurrentWritingEntity.Prototype.Components.TryGetValue(CurrentWritingComponent!, out var compData)) - { - // This component was added mid-game. - return false; - } - - var testSer = YamlObjectSerializer.NewReader(compData); - if (testSer.TryReadDataFieldCached(field, format, out var prototypeVal)) - { - if (value == null) - { - return prototypeVal == null; - } - - return YamlObjectSerializer.IsSerializedEqual(value, prototypeVal); - } - - return false; - } - private bool IsMapSavable(IEntity entity) { if (entity.Prototype?.MapSavable == false || !GridIDMap.ContainsKey(entity.Transform.GridID)) @@ -925,6 +841,173 @@ namespace Robust.Server.Maps return true; } + + public DeserializationResult Read(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context = null) + { + if (node.Value == "null") return new DeserializedValue(GridId.Invalid); + + var val = int.Parse(node.Value); + if (val >= Grids.Count) + { + Logger.ErrorS("map", "Error in map file: found local grid ID '{0}' which does not exist.", val); + } + else + { + return new DeserializedValue(Grids[val].Index); + } + + return new DeserializedValue(GridId.Invalid); + } + + ValidationNode ITypeReader.Validate(ISerializationManager serializationManager, + ValueDataNode node, IDependencyCollection dependencies, ISerializationContext? context) + { + if (!int.TryParse(node.Value, out var val) || val >= Entities.Count) + { + return new ErrorNode(node, "Invalid EntityUid", true); + } + + return new ValidatedValueNode(node); + } + + ValidationNode ITypeReader.Validate(ISerializationManager serializationManager, + ValueDataNode node, IDependencyCollection dependencies, ISerializationContext? context) + { + if (node.Value == "null") + { + return new ValidatedValueNode(node); + } + + if (!int.TryParse(node.Value, out var val) || val >= Entities.Count) + { + return new ErrorNode(node, "Invalid EntityUid", true); + } + + return new ValidatedValueNode(node); + } + + ValidationNode ITypeReader.Validate(ISerializationManager serializationManager, + ValueDataNode node, IDependencyCollection dependencies, ISerializationContext? context) + { + if (node.Value == "null") return new ValidatedValueNode(node); + + if (!int.TryParse(node.Value, out var val) || val >= Grids.Count) + { + return new ErrorNode(node, "Invalid GridId", true); + } + + return new ValidatedValueNode(node); + } + + public DataNode Write(ISerializationManager serializationManager, IEntity value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + if (!EntityUidMap.TryGetValue(value.Uid, out var entityMapped)) + { + Logger.WarningS("map", "Cannot write entity UID '{0}'.", value.Uid); + return new ValueDataNode(""); + } + else + { + return new ValueDataNode(entityMapped.ToString(CultureInfo.InvariantCulture)); + } + } + + public DataNode Write(ISerializationManager serializationManager, EntityUid value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + if (!EntityUidMap.TryGetValue(value, out var entityUidMapped)) + { + // Terrible hack to mute this warning on the grids themselves when serializing blueprints. + if (!IsBlueprintMode || !CurrentWritingEntity!.HasComponent() || + CurrentWritingComponent != "Transform") + { + Logger.WarningS("map", "Cannot write entity UID '{0}'.", value); + } + + return new ValueDataNode("null"); + } + else + { + return new ValueDataNode(entityUidMapped.ToString(CultureInfo.InvariantCulture)); + } + } + + public DataNode Write(ISerializationManager serializationManager, GridId value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + if (!GridIDMap.TryGetValue(value, out var gridMapped)) + { + Logger.WarningS("map", "Cannot write grid ID '{0}', falling back to nullspace.", gridMapped); + return new ValueDataNode(""); + } + else + { + return new ValueDataNode(gridMapped.ToString(CultureInfo.InvariantCulture)); + } + } + + DeserializationResult ITypeReader.Read(ISerializationManager serializationManager, + ValueDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context) + { + if (node.Value == "null") + { + return new DeserializedValue(EntityUid.Invalid); + } + + var val = int.Parse(node.Value); + if (val >= Entities.Count) + { + Logger.ErrorS("map", "Error in map file: found local entity UID '{0}' which does not exist.", val); + } + else + { + return new DeserializedValue(UidEntityMap[val]); + } + + return new DeserializedValue(EntityUid.Invalid); + } + + DeserializationResult ITypeReader.Read(ISerializationManager serializationManager, + ValueDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context) + { + var val = int.Parse(node.Value); + + if (val >= Entities.Count) + { + Logger.ErrorS("map", "Error in map file: found local entity UID '{0}' which does not exist.", val); + return null!; + } + else + { + return new DeserializedValue(Entities[val]); + } + } + + [MustUseReturnValue] + public GridId Copy(ISerializationManager serializationManager, GridId source, GridId target, + bool skipHook, + ISerializationContext? context = null) + { + return new(source.Value); + } + + [MustUseReturnValue] + public EntityUid Copy(ISerializationManager serializationManager, EntityUid source, EntityUid target, + bool skipHook, + ISerializationContext? context = null) + { + return new((int) source); + } } /// diff --git a/Robust.Shared/Audio/AudioParams.cs b/Robust.Shared/Audio/AudioParams.cs index 136ede6ec..1b35a6447 100644 --- a/Robust.Shared/Audio/AudioParams.cs +++ b/Robust.Shared/Audio/AudioParams.cs @@ -1,6 +1,8 @@ using Robust.Shared.Serialization; using System; using System.Diagnostics.Contracts; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; namespace Robust.Shared.Audio { @@ -8,43 +10,52 @@ namespace Robust.Shared.Audio /// Contains common audio parameters for audio playback on the client. /// [Serializable, NetSerializable] - public struct AudioParams : IExposeData + [DataDefinition] + public struct AudioParams : IPopulateDefaultValues { /// /// Base volume to play the audio at, in dB. /// + [DataField("volume")] public float Volume { get; set; } /// /// Scale for the audio pitch. /// + [DataField("pitchscale")] public float PitchScale { get; set; } /// /// Audio bus to play on. /// + [DataField("busname")] public string BusName { get; set; } /// /// Only applies to positional audio. /// The maximum distance from which the audio is hearable. /// + [DataField("maxdistance")] public float MaxDistance { get; set; } /// /// Only applies to positional audio. /// Positional audio is dampened over distance with this as exponent. /// + [DataField("attenuation")] public float Attenuation { get; set; } /// /// Only applies to global (non-positional) audio. /// Target channels if the audio configuration has more than 2 speakers. /// + [DataField("mixtarget")] public AudioMixTarget MixTarget { get; set; } + [DataField("loop")] public bool Loop { get; set; } + [DataField("playoffset")] public float PlayOffsetSeconds { get; set; } // For the max distance value: it's 2000 in Godot, but I assume that's PIXELS due to the 2D positioning, @@ -54,18 +65,6 @@ namespace Robust.Shared.Audio /// public static readonly AudioParams Default = new(0, 1, "Master", 62.5f, 1, AudioMixTarget.Stereo, false, 0f); - void IExposeData.ExposeData(ObjectSerializer serializer) - { - Volume = serializer.ReadDataField("volume", 0f); - PitchScale = serializer.ReadDataField("pitchscale", 1f); - BusName = serializer.ReadDataField("busname", "Master"); - MaxDistance = serializer.ReadDataField("maxdistance", 62.5f); - Attenuation = serializer.ReadDataField("attenuation", 1f); - MixTarget = serializer.ReadDataField("mixtarget", AudioMixTarget.Stereo); - Loop = serializer.ReadDataField("loop", false); - PlayOffsetSeconds = serializer.ReadDataField("playoffset", 0f); - } - public AudioParams(float volume, float pitchScale, string busName, float maxDistance, float attenuation, AudioMixTarget mixTarget, bool loop, float playOffsetSeconds) : this() { @@ -169,6 +168,15 @@ namespace Robust.Shared.Audio me.PlayOffsetSeconds = offset; return me; } + + public void PopulateDefaultValues() + { + PitchScale = 1f; + BusName = "Master"; + MaxDistance = 62.5f; + Attenuation = 1f; + MixTarget = AudioMixTarget.Stereo; + } } /// diff --git a/Robust.Shared/Containers/BaseContainer.cs b/Robust.Shared/Containers/BaseContainer.cs index 127d63eed..6305e9242 100644 --- a/Robust.Shared/Containers/BaseContainer.cs +++ b/Robust.Shared/Containers/BaseContainer.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Robust.Shared.GameObjects; using Robust.Shared.Maths; -using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; @@ -32,6 +32,7 @@ namespace Robust.Shared.Containers /// [ViewVariables(VVAccess.ReadWrite)] + [field: DataField("occludes")] public bool OccludesLight { get; set; } = true; /// @@ -40,6 +41,7 @@ namespace Robust.Shared.Containers /// [ViewVariables(VVAccess.ReadWrite)] + [field: DataField("showEnts")] public bool ShowContents { get; set; } /// @@ -165,13 +167,5 @@ namespace Robust.Shared.Containers Manager.Owner.SendMessage(Manager, new ContainerContentsModifiedMessage(this, toremove, true)); Manager.Dirty(); } - - /// - public virtual void ExposeData(ObjectSerializer serializer) - { - // ID and Manager are filled in Initialize - serializer.DataReadWriteFunction("showEnts", false, value => ShowContents = value, () => ShowContents); - serializer.DataReadWriteFunction("occludes", true, value => OccludesLight = value, () => OccludesLight); - } } } diff --git a/Robust.Shared/Containers/Container.cs b/Robust.Shared/Containers/Container.cs index 2e4b9872a..854ba621a 100644 --- a/Robust.Shared/Containers/Container.cs +++ b/Robust.Shared/Containers/Container.cs @@ -1,9 +1,8 @@ using System.Collections.Generic; -using System.Linq; using JetBrains.Annotations; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; namespace Robust.Shared.Containers { @@ -22,37 +21,14 @@ namespace Robust.Shared.Containers /// /// The generic container class uses a list of entities /// - private List _containerList = new(); + [DataField("ents")] + private readonly List _containerList = new(); /// public override IReadOnlyList ContainedEntities => _containerList; /// public override string ContainerType => ClassName; - - /// - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - -#if SERV3 - // ONLY PAUL CAN MAKE ME WHOLE - serializer.DataField(ref _containerList, "ents", new List()); -#else - if (serializer.Writing) - { - serializer.DataWriteFunction("ents", new List(), - () => _containerList.Select(e => e.Uid).ToList()); - } - else - { - var entMan = IoCManager.Resolve(); - - serializer.DataReadFunction("ents", new List(), - value => _containerList = value.Select((uid => entMan.GetEntity(uid))).ToList()); - } -#endif - } /// protected override void InternalInsert(IEntity toinsert) diff --git a/Robust.Shared/Containers/ContainerManagerComponent.cs b/Robust.Shared/Containers/ContainerManagerComponent.cs index 3ca9b76fb..e256f6ce5 100644 --- a/Robust.Shared/Containers/ContainerManagerComponent.cs +++ b/Robust.Shared/Containers/ContainerManagerComponent.cs @@ -7,6 +7,8 @@ using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Players; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Robust.Shared.Containers @@ -20,8 +22,10 @@ namespace Robust.Shared.Containers { [Dependency] private readonly IRobustSerializer _serializer = default!; [Dependency] private readonly IDynamicTypeFactoryInternal _dynFactory = default!; - - [ViewVariables] private Dictionary _containers = new(); + + [ViewVariables] + [DataField("containers")] + private Dictionary _containers = new(); /// public sealed override string Name => "ContainerContainer"; @@ -81,7 +85,7 @@ namespace Robust.Shared.Containers _containers.Remove(dead); } } - + // Add new containers and update existing contents. foreach (var (containerType, id, showEnts, occludesLight, entityUids) in cast.ContainerSet) @@ -135,13 +139,6 @@ namespace Robust.Shared.Containers newContainer.Manager = this; return newContainer; } - /// - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - - serializer.DataField(ref _containers, "containers", new Dictionary()); - } /// public override ComponentState GetComponentState(ICommonSession player) @@ -319,9 +316,13 @@ namespace Robust.Shared.Containers } } - private struct ContainerPrototypeData : IExposeData + [DataDefinition] + private struct ContainerPrototypeData : IPopulateDefaultValues { + [DataField("entities")] public List Entities; + + [DataField("type")] public string? Type; public ContainerPrototypeData(List entities, string type) @@ -330,10 +331,9 @@ namespace Robust.Shared.Containers Type = type; } - void IExposeData.ExposeData(ObjectSerializer serializer) + public void PopulateDefaultValues() { - serializer.DataField(ref Entities, "entities", new List()); - serializer.DataField(ref Type, "type", null); + Entities = new List(); } } diff --git a/Robust.Shared/Containers/ContainerSlot.cs b/Robust.Shared/Containers/ContainerSlot.cs index a6fe4e6e9..8b04d2f07 100644 --- a/Robust.Shared/Containers/ContainerSlot.cs +++ b/Robust.Shared/Containers/ContainerSlot.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; -using System.Linq; using JetBrains.Annotations; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Robust.Shared.Containers @@ -15,8 +14,6 @@ namespace Robust.Shared.Containers { private const string ClassName = "ContainerSlot"; - private IEntity? _containedEntity; - /// public override IReadOnlyList ContainedEntities { @@ -29,38 +26,11 @@ namespace Robust.Shared.Containers } [ViewVariables] - public IEntity? ContainedEntity - { - get => _containedEntity; - private set => _containedEntity = value; - } + [field: DataField("ent")] + public IEntity? ContainedEntity { get; private set; } /// public override string ContainerType => ClassName; - - /// - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - -#if SERV3 - // ONLY PAUL CAN MAKE ME WHOLE - serializer.DataField(ref _containedEntity, "ent", default); -#else - if (serializer.Writing) - { - serializer.DataWriteFunction("ents", EntityUid.Invalid, - () => _containedEntity?.Uid ?? EntityUid.Invalid); - } - else - { - var entMan = IoCManager.Resolve(); - - serializer.DataReadFunction("ent", EntityUid.Invalid, - value => _containedEntity = value != EntityUid.Invalid ? entMan.GetEntity(value) : null); - } -#endif - } /// public override bool CanInsert(IEntity toinsert) diff --git a/Robust.Shared/Containers/IContainer.cs b/Robust.Shared/Containers/IContainer.cs index f507f2cb9..a1ab42e2b 100644 --- a/Robust.Shared/Containers/IContainer.cs +++ b/Robust.Shared/Containers/IContainer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using JetBrains.Annotations; using Robust.Shared.GameObjects; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; namespace Robust.Shared.Containers { @@ -25,7 +26,8 @@ namespace Robust.Shared.Containers /// /// [PublicAPI] - public interface IContainer : IExposeData + [ImplicitDataDefinitionForInheritors] + public interface IContainer { /// /// Readonly collection of all the entities contained within this specific container diff --git a/Robust.Shared/GameObjects/Component.cs b/Robust.Shared/GameObjects/Component.cs index 296167046..bfde46729 100644 --- a/Robust.Shared/GameObjects/Component.cs +++ b/Robust.Shared/GameObjects/Component.cs @@ -1,8 +1,10 @@ using System; using Robust.Shared.Network; using Robust.Shared.Players; +using Robust.Shared.Prototypes; using Robust.Shared.Reflection; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Timing; using Robust.Shared.ViewVariables; @@ -10,6 +12,7 @@ namespace Robust.Shared.GameObjects { /// [Reflect(false)] + [ImplicitDataDefinitionForInheritorsAttribute] public abstract class Component : IComponent { /// @@ -24,6 +27,7 @@ namespace Robust.Shared.GameObjects [ViewVariables] public virtual bool NetworkSynchronizeExistence => false; + [DataField("netsync")] private bool _netSyncEnabled = true; /// [ViewVariables] @@ -159,12 +163,6 @@ namespace Robust.Shared.GameObjects _running = false; } - /// - public virtual void ExposeData(ObjectSerializer serializer) - { - serializer.DataField(ref _netSyncEnabled, "netsync", true); - } - /// public void Dirty() { diff --git a/Robust.Shared/GameObjects/ComponentDependencies/ComponentDependencyAttribute.cs b/Robust.Shared/GameObjects/ComponentDependencies/ComponentDependencyAttribute.cs index ff035db83..17b813181 100644 --- a/Robust.Shared/GameObjects/ComponentDependencies/ComponentDependencyAttribute.cs +++ b/Robust.Shared/GameObjects/ComponentDependencies/ComponentDependencyAttribute.cs @@ -3,6 +3,7 @@ namespace Robust.Shared.GameObjects { [AttributeUsage(AttributeTargets.Field)] + [MeansImplicitAssignment] public class ComponentDependencyAttribute : Attribute { public readonly string? OnAddMethodName; diff --git a/Robust.Shared/GameObjects/ComponentManager.cs b/Robust.Shared/GameObjects/ComponentManager.cs index 12eb89b06..a3312357b 100644 --- a/Robust.Shared/GameObjects/ComponentManager.cs +++ b/Robust.Shared/GameObjects/ComponentManager.cs @@ -129,13 +129,6 @@ namespace Robust.Shared.GameObjects ComponentAdded?.Invoke(this, new AddedComponentEventArgs(component)); } - if (entity.Initialized || entity.Initializing) - { - var defaultSerializer = DefaultValueSerializer.Reader(); - defaultSerializer.CurrentType = component.GetType(); - component.ExposeData(defaultSerializer); - } - _componentDependencyManager.OnComponentAdd(entity, component); component.OnAdd(); diff --git a/Robust.Shared/GameObjects/Components/Collidable/PhysicsComponent.Collision.cs b/Robust.Shared/GameObjects/Components/Collidable/PhysicsComponent.Collision.cs index b3516dedd..1ffefc39f 100644 --- a/Robust.Shared/GameObjects/Components/Collidable/PhysicsComponent.Collision.cs +++ b/Robust.Shared/GameObjects/Components/Collidable/PhysicsComponent.Collision.cs @@ -5,8 +5,10 @@ using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Physics; +using Robust.Shared.Prototypes; using Robust.Shared.Players; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Robust.Shared.GameObjects @@ -43,10 +45,15 @@ namespace Robust.Shared.GameObjects { [Dependency] private readonly IPhysicsManager _physicsManager = default!; - private bool _canCollide; - private bool _isHard; - private BodyStatus _status; - private BodyType _bodyType; + [DataField("on")] + private bool _canCollide = true; + [DataField("hard")] + private bool _isHard = true; + [DataField("status")] + private BodyStatus _status = BodyStatus.OnGround; + [DataField("bodyType")] + private BodyType _bodyType = BodyType.Static; + [DataField("shapes")] private List _physShapes = new(); /// @@ -119,20 +126,6 @@ namespace Robust.Shared.GameObjects PhysicsShapes = new PhysShapeList(this); } - /// - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - - serializer.DataField(ref _canCollide, "on", true); - serializer.DataField(ref _isHard, "hard", true); - serializer.DataField(ref _status, "status", BodyStatus.OnGround); - serializer.DataField(ref _bodyType, "bodyType", BodyType.Static); - serializer.DataField(ref _physShapes, "shapes", new List {new PhysShapeAabb()}); - serializer.DataField(ref _anchored, "anchored", true); - serializer.DataField(ref _mass, "mass", 1.0f); - } - /// /// public override ComponentState GetComponentState(ICommonSession player) diff --git a/Robust.Shared/GameObjects/Components/Collidable/PhysicsComponent.Physics.cs b/Robust.Shared/GameObjects/Components/Collidable/PhysicsComponent.Physics.cs index 39b1a549c..ba49e3ab4 100644 --- a/Robust.Shared/GameObjects/Components/Collidable/PhysicsComponent.Physics.cs +++ b/Robust.Shared/GameObjects/Components/Collidable/PhysicsComponent.Physics.cs @@ -4,6 +4,8 @@ using System.Diagnostics.CodeAnalysis; using Robust.Shared.IoC; using Robust.Shared.Maths; using Robust.Shared.Physics; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Robust.Shared.GameObjects @@ -168,11 +170,13 @@ namespace Robust.Shared.GameObjects { [Dependency] private readonly IDynamicTypeFactory _dynamicTypeFactory = default!; - private float _mass = 1; + [DataField("mass")] + private float _mass = 1.0f; private float _angularMass = 1; private Vector2 _linVelocity; private float _angVelocity; private Dictionary _controllers = new(); + [DataField("anchored")] private bool _anchored = true; private float _friction = 1; diff --git a/Robust.Shared/GameObjects/Components/DebugExceptionComponents.cs b/Robust.Shared/GameObjects/Components/DebugExceptionComponents.cs index e8db4fc32..14cda7eed 100644 --- a/Robust.Shared/GameObjects/Components/DebugExceptionComponents.cs +++ b/Robust.Shared/GameObjects/Components/DebugExceptionComponents.cs @@ -20,16 +20,6 @@ namespace Robust.Shared.GameObjects public override void OnAdd() => throw new NotSupportedException(); } - /// - /// Throws an exception in . - /// - public sealed class DebugExceptionExposeDataComponent : Component - { - public override string Name => "DebugExceptionExposeData"; - - public override void ExposeData(ObjectSerializer serializer) => throw new NotSupportedException(); - } - /// /// Throws an exception in . /// diff --git a/Robust.Shared/GameObjects/Components/Light/OccluderComponent.cs b/Robust.Shared/GameObjects/Components/Light/OccluderComponent.cs index 2fedeab6c..5a8cdc97a 100644 --- a/Robust.Shared/GameObjects/Components/Light/OccluderComponent.cs +++ b/Robust.Shared/GameObjects/Components/Light/OccluderComponent.cs @@ -1,8 +1,10 @@ using System; using Robust.Shared.Map; using Robust.Shared.Maths; +using Robust.Shared.Prototypes; using Robust.Shared.Players; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Robust.Shared.GameObjects @@ -12,7 +14,9 @@ namespace Robust.Shared.GameObjects public sealed override string Name => "Occluder"; public sealed override uint? NetID => NetIDs.OCCLUDER; + [DataField("enabled")] private bool _enabled = true; + [DataField("boundingBox")] private Box2 _boundingBox = new(-0.5f, -0.5f, 0.5f, 0.5f); [ViewVariables(VVAccess.ReadWrite)] @@ -53,14 +57,6 @@ namespace Robust.Shared.GameObjects EntitySystem.Get().AddOrUpdateEntity(Owner, Owner.Transform.Coordinates); } - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - - serializer.DataField(ref _enabled, "enabled", true); - serializer.DataField(ref _boundingBox, "boundingBox", new Box2(-0.5f, -0.5f, 0.5f, 0.5f)); - } - public override void OnRemove() { base.OnRemove(); diff --git a/Robust.Shared/GameObjects/Components/Localization/GrammarComponent.cs b/Robust.Shared/GameObjects/Components/Localization/GrammarComponent.cs index 1d5aeec2d..d2f8284cd 100644 --- a/Robust.Shared/GameObjects/Components/Localization/GrammarComponent.cs +++ b/Robust.Shared/GameObjects/Components/Localization/GrammarComponent.cs @@ -1,8 +1,5 @@ - using Robust.Shared.Enums; -using Robust.Shared.IoC; -using Robust.Shared.Reflection; -using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Robust.Shared.GameObjects.Components.Localization @@ -14,27 +11,15 @@ namespace Robust.Shared.GameObjects.Components.Localization public override uint? NetID => NetIDs.GRAMMAR; [ViewVariables] + [DataField("localizationId")] public string LocalizationId = ""; [ViewVariables] + [DataField("gender")] public Gender? Gender = null; [ViewVariables] + [DataField("proper")] public bool? ProperNoun = null; - - public override void ExposeData(ObjectSerializer serializer) - { - serializer.DataField(ref LocalizationId, "localizationId", ""); - - if (serializer.TryReadDataFieldCached("gender", out string? gender0)) - { - var refl = IoCManager.Resolve(); - if (refl.TryParseEnumReference(gender0!, out var gender)) - { - Gender = (Gender)gender; - } - } - serializer.DataField(ref ProperNoun, "proper", null); - } } } diff --git a/Robust.Shared/GameObjects/Components/Map/MapComponent.cs b/Robust.Shared/GameObjects/Components/Map/MapComponent.cs index b13bf1ed7..e2767e5f4 100644 --- a/Robust.Shared/GameObjects/Components/Map/MapComponent.cs +++ b/Robust.Shared/GameObjects/Components/Map/MapComponent.cs @@ -1,7 +1,9 @@ using System; using Robust.Shared.Map; +using Robust.Shared.Prototypes; using Robust.Shared.Players; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Robust.Shared.GameObjects @@ -19,7 +21,8 @@ namespace Robust.Shared.GameObjects public class MapComponent : Component, IMapComponent { [ViewVariables(VVAccess.ReadOnly)] - private MapId _mapIndex; + [DataField("index")] + private MapId _mapIndex = MapId.Nullspace; /// public override string Name => "Map"; @@ -59,14 +62,6 @@ namespace Robust.Shared.GameObjects ((TransformComponent) Owner.Transform).ChangeMapId(_mapIndex); } - - /// - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - - serializer.DataField(ref _mapIndex, "index", MapId.Nullspace); - } } /// diff --git a/Robust.Shared/GameObjects/Components/Map/MapGridComponent.cs b/Robust.Shared/GameObjects/Components/Map/MapGridComponent.cs index 9fe5faa19..ffb994ef7 100644 --- a/Robust.Shared/GameObjects/Components/Map/MapGridComponent.cs +++ b/Robust.Shared/GameObjects/Components/Map/MapGridComponent.cs @@ -2,8 +2,10 @@ using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map; +using Robust.Shared.Prototypes; using Robust.Shared.Players; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Robust.Shared.GameObjects @@ -24,7 +26,8 @@ namespace Robust.Shared.GameObjects [Dependency] private readonly IMapManager _mapManager = default!; [ViewVariables(VVAccess.ReadOnly)] - private GridId _gridIndex; + [DataField("index")] + private GridId _gridIndex = GridId.Invalid; /// public override string Name => "MapGrid"; @@ -80,14 +83,6 @@ namespace Robust.Shared.GameObjects _gridIndex = state.GridIndex; Grid.HasGravity = state.HasGravity; } - - /// - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - - serializer.DataField(ref _gridIndex, "index", GridId.Invalid); - } } /// diff --git a/Robust.Shared/GameObjects/Components/MetaDataComponent.cs b/Robust.Shared/GameObjects/Components/MetaDataComponent.cs index 2aa79dde6..06d1bb4e1 100644 --- a/Robust.Shared/GameObjects/Components/MetaDataComponent.cs +++ b/Robust.Shared/GameObjects/Components/MetaDataComponent.cs @@ -3,6 +3,7 @@ using Robust.Shared.IoC; using Robust.Shared.Players; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Robust.Shared.GameObjects @@ -68,7 +69,9 @@ namespace Robust.Shared.GameObjects { [Dependency] private readonly IPrototypeManager _prototypes = default!; + [DataField("name")] private string? _entityName; + [DataField("desc")] private string? _entityDescription; private EntityPrototype? _entityPrototype; @@ -160,18 +163,6 @@ namespace Robust.Shared.GameObjects _entityPrototype = _prototypes.Index(state.PrototypeId); } - /// - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - - serializer.DataField(ref _entityName, "name", null); - serializer.DataField(ref _entityDescription, "desc", null); - //serializer.DataField(ref _entityPrototype, "proto", null, - // s => _prototypes.Index(s), - // p => p.ID); - } - internal override void ClearTicks() { // Do not clear modified ticks. diff --git a/Robust.Shared/GameObjects/Components/Renderable/SharedSpriteComponent.cs b/Robust.Shared/GameObjects/Components/Renderable/SharedSpriteComponent.cs index 2f5bcf3a2..fa73faa4b 100644 --- a/Robust.Shared/GameObjects/Components/Renderable/SharedSpriteComponent.cs +++ b/Robust.Shared/GameObjects/Components/Renderable/SharedSpriteComponent.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Robust.Shared.Maths; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Utility; namespace Robust.Shared.GameObjects @@ -54,16 +55,26 @@ namespace Robust.Shared.GameObjects } [Serializable, NetSerializable] - protected struct PrototypeLayerData : IExposeData + [DataDefinition] + public class PrototypeLayerData { + [DataField("shader")] public string? Shader; + [DataField("texture")] public string? TexturePath; + [DataField("sprite")] public string? RsiPath; + [DataField("state")] public string? State; - public Vector2 Scale; - public Angle Rotation; - public bool Visible; - public Color Color; + [DataField("scale")] + public Vector2 Scale = Vector2.One; + [DataField("rotation")] + public Angle Rotation = Angle.Zero; + [DataField("visible")] + public bool Visible = true; + [DataField("color")] + public Color Color = Color.White; + [DataField("map")] public List? MapKeys; public static PrototypeLayerData New() @@ -75,19 +86,6 @@ namespace Robust.Shared.GameObjects Visible = true, }; } - - void IExposeData.ExposeData(ObjectSerializer serializer) - { - serializer.DataField(ref Shader, "shader", null); - serializer.DataField(ref TexturePath, "texture", null); - serializer.DataField(ref RsiPath, "sprite", null); - serializer.DataField(ref State, "state", null); - serializer.DataField(ref Scale, "scale", Vector2.One); - serializer.DataField(ref Rotation, "rotation", Angle.Zero); - serializer.DataField(ref Visible, "visible", true); - serializer.DataField(ref Color, "color", Color.White); - serializer.DataField(ref MapKeys, "map", null); - } } } } diff --git a/Robust.Shared/GameObjects/Components/Transform/SnapGridComponent.cs b/Robust.Shared/GameObjects/Components/Transform/SnapGridComponent.cs index ee7c91a48..92b97d6f6 100644 --- a/Robust.Shared/GameObjects/Components/Transform/SnapGridComponent.cs +++ b/Robust.Shared/GameObjects/Components/Transform/SnapGridComponent.cs @@ -6,7 +6,9 @@ using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Maths; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; namespace Robust.Shared.GameObjects { @@ -19,6 +21,7 @@ namespace Robust.Shared.GameObjects public sealed override string Name => "SnapGrid"; private bool IsSet; + [DataField("offset")] private SnapGridOffset _offset = SnapGridOffset.Center; [Dependency] private readonly IMapManager _mapManager = default!; @@ -53,13 +56,6 @@ namespace Robust.Shared.GameObjects } } - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - - serializer.DataFieldCached(ref _offset, "offset", SnapGridOffset.Center); - } - /// /// Returns an enumerable over all the entities which are one tile over in a certain direction. /// diff --git a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs index c70386580..8baa594d8 100644 --- a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs +++ b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs @@ -1,13 +1,16 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using Robust.Shared.Animations; using Robust.Shared.Containers; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; +using Robust.Shared.Prototypes; using Robust.Shared.Players; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; @@ -15,9 +18,13 @@ namespace Robust.Shared.GameObjects { internal class TransformComponent : Component, ITransformComponent, IComponentDebug { + [DataField("parent")] private EntityUid _parent; - private Vector2 _localPosition; // holds offset from grid, or offset from parent + [DataField("pos")] + private Vector2 _localPosition = Vector2.Zero; // holds offset from grid, or offset from parent + [DataField("rot")] private Angle _localRotation; // local rotation + [DataField("noRot")] private bool _noLocalRotation; private Matrix3 _localMatrix = Matrix3.Identity; @@ -689,16 +696,6 @@ namespace Robust.Shared.GameObjects } } - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - - serializer.DataField(ref _parent, "parent", new EntityUid()); - serializer.DataField(ref _localPosition, "pos", Vector2.Zero); - serializer.DataField(ref _localRotation, "rot", new Angle()); - serializer.DataField(ref _noLocalRotation, "noRot", false); - } - /// /// public override ComponentState GetComponentState(ICommonSession player) diff --git a/Robust.Shared/GameObjects/Components/UserInterface/SharedUserInterfaceComponent.cs b/Robust.Shared/GameObjects/Components/UserInterface/SharedUserInterfaceComponent.cs index 99ad095aa..18e1b0dc4 100644 --- a/Robust.Shared/GameObjects/Components/UserInterface/SharedUserInterfaceComponent.cs +++ b/Robust.Shared/GameObjects/Components/UserInterface/SharedUserInterfaceComponent.cs @@ -1,5 +1,8 @@ using System; +using Robust.Shared.IoC; +using Robust.Shared.Reflection; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; namespace Robust.Shared.GameObjects { @@ -8,15 +11,28 @@ namespace Robust.Shared.GameObjects public sealed override string Name => "UserInterface"; public sealed override uint? NetID => NetIDs.USERINTERFACE; - protected sealed class PrototypeData : IExposeData + [DataDefinition] + public sealed class PrototypeData : ISerializationHooks { - public object UiKey { get; private set; } = default!; - public string ClientType { get; private set; } = default!; + public object UiKey { get; set; } = default!; - void IExposeData.ExposeData(ObjectSerializer serializer) + [DataField("key", readOnly: true, required: true)] + private string _uiKeyRaw = default!; + + [DataField("type", readOnly: true, required: true)] + public string ClientType { get; set; } = default!; + + void ISerializationHooks.AfterDeserialization() { - UiKey = serializer.ReadStringEnumKey("key"); - ClientType = serializer.ReadDataField("type"); + var reflectionManager = IoCManager.Resolve(); + + if (reflectionManager.TryParseEnumReference(_uiKeyRaw, out var @enum)) + { + UiKey = @enum; + return; + } + + UiKey = _uiKeyRaw; } } diff --git a/Robust.Shared/GameObjects/IComponent.cs b/Robust.Shared/GameObjects/IComponent.cs index ae97f75e1..9a499c11d 100644 --- a/Robust.Shared/GameObjects/IComponent.cs +++ b/Robust.Shared/GameObjects/IComponent.cs @@ -100,13 +100,6 @@ namespace Robust.Shared.GameObjects /// void Initialize(); - /// - /// This allows setting of the component's parameters from YAML once it is instantiated. - /// This should basically be overridden by every inheriting component, as parameters will be different - /// across the board. - /// - void ExposeData(ObjectSerializer serializer); - /// /// Handles a local incoming component message. /// diff --git a/Robust.Shared/GameObjects/IEntity.cs b/Robust.Shared/GameObjects/IEntity.cs index 00a787cc3..a813afeec 100644 --- a/Robust.Shared/GameObjects/IEntity.cs +++ b/Robust.Shared/GameObjects/IEntity.cs @@ -3,10 +3,12 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Robust.Shared.Network; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Timing; namespace Robust.Shared.GameObjects { + [CopyByRef] public interface IEntity { GameTick LastModifiedTick { get; } @@ -40,7 +42,7 @@ namespace Robust.Shared.GameObjects /// True if the entity has been deleted. /// bool Deleted { get; } - + bool Paused { get; set; } /// diff --git a/Robust.Shared/GameObjects/IEntityLoadContext.cs b/Robust.Shared/GameObjects/IEntityLoadContext.cs index bd1042170..bd482ac09 100644 --- a/Robust.Shared/GameObjects/IEntityLoadContext.cs +++ b/Robust.Shared/GameObjects/IEntityLoadContext.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager; using YamlDotNet.RepresentationModel; namespace Robust.Shared.GameObjects @@ -12,11 +14,11 @@ namespace Robust.Shared.GameObjects /// /// Gets the serializer used to ExposeData a specific component. /// - ObjectSerializer GetComponentSerializer(string componentName, YamlMappingNode? protoData); + IComponent GetComponentData(string componentName, IComponent? protoData); /// /// Gets extra component names that must also be instantiated on top of the ones defined in the prototype, - /// (and then deserialized with ) + /// (and then deserialized with ) /// IEnumerable GetExtraComponentTypes(); } diff --git a/Robust.Shared/Map/PhysShapeGrid.cs b/Robust.Shared/Map/PhysShapeGrid.cs index ee5ceea98..520e9ccfd 100644 --- a/Robust.Shared/Map/PhysShapeGrid.cs +++ b/Robust.Shared/Map/PhysShapeGrid.cs @@ -3,6 +3,7 @@ using Robust.Shared.IoC; using Robust.Shared.Maths; using Robust.Shared.Physics; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Robust.Shared.Map @@ -11,9 +12,11 @@ namespace Robust.Shared.Map /// A physics shape that represents a . /// [Serializable, NetSerializable] - public class PhysShapeGrid : IPhysShape + [DataDefinition] + public class PhysShapeGrid : IPhysShape, ISerializationHooks { - private GridId _gridId; + [DataField("grid")] + private GridId _gridId = GridId.Invalid; [NonSerialized] private IMapGridInternal _mapGrid = default!; @@ -40,6 +43,12 @@ namespace Robust.Shared.Map set { } } + void ISerializationHooks.AfterDeserialization() + { + var mapMan = IoCManager.Resolve(); + _mapGrid = (IMapGridInternal)mapMan.GetGrid(_gridId); + } + /// public void ApplyState() { @@ -81,18 +90,6 @@ namespace Robust.Shared.Map _gridId = _mapGrid.Index; } - /// - void IExposeData.ExposeData(ObjectSerializer serializer) - { - serializer.DataField(ref _gridId, "grid", GridId.Invalid); - - if (serializer.Reading) // There is no Initialize function - { - var mapMan = IoCManager.Resolve(); - _mapGrid = (IMapGridInternal)mapMan.GetGrid(_gridId); - } - } - public event Action? OnDataChanged { add { } remove { } } /// diff --git a/Robust.Shared/MeansImplicitAssignmentAttribute.cs b/Robust.Shared/MeansImplicitAssignmentAttribute.cs new file mode 100644 index 000000000..ec47d6ee8 --- /dev/null +++ b/Robust.Shared/MeansImplicitAssignmentAttribute.cs @@ -0,0 +1,6 @@ +using System; + +namespace Robust.Shared +{ + public class MeansImplicitAssignmentAttribute : Attribute { } +} diff --git a/Robust.Shared/Network/Messages/Handshake/MsgEncryptionRequest.cs b/Robust.Shared/Network/Messages/Handshake/MsgEncryptionRequest.cs index 99dba17db..adcedcb0a 100644 --- a/Robust.Shared/Network/Messages/Handshake/MsgEncryptionRequest.cs +++ b/Robust.Shared/Network/Messages/Handshake/MsgEncryptionRequest.cs @@ -2,7 +2,7 @@ #nullable disable -namespace Robust.Shared.Network.Messages +namespace Robust.Shared.Network.Messages.Handshake { internal sealed class MsgEncryptionRequest : NetMessage { diff --git a/Robust.Shared/Network/Messages/Handshake/MsgEncryptionResponse.cs b/Robust.Shared/Network/Messages/Handshake/MsgEncryptionResponse.cs index a132a509c..d1e4467c7 100644 --- a/Robust.Shared/Network/Messages/Handshake/MsgEncryptionResponse.cs +++ b/Robust.Shared/Network/Messages/Handshake/MsgEncryptionResponse.cs @@ -3,7 +3,7 @@ using Lidgren.Network; #nullable disable -namespace Robust.Shared.Network.Messages +namespace Robust.Shared.Network.Messages.Handshake { internal sealed class MsgEncryptionResponse : NetMessage { diff --git a/Robust.Shared/Network/Messages/Handshake/MsgLoginStart.cs b/Robust.Shared/Network/Messages/Handshake/MsgLoginStart.cs index d68743825..c5b0d709e 100644 --- a/Robust.Shared/Network/Messages/Handshake/MsgLoginStart.cs +++ b/Robust.Shared/Network/Messages/Handshake/MsgLoginStart.cs @@ -2,7 +2,7 @@ #nullable disable -namespace Robust.Shared.Network.Messages +namespace Robust.Shared.Network.Messages.Handshake { internal sealed class MsgLoginStart : NetMessage { diff --git a/Robust.Shared/Network/Messages/Handshake/MsgLoginSuccess.cs b/Robust.Shared/Network/Messages/Handshake/MsgLoginSuccess.cs index 313be642a..dc5addfcc 100644 --- a/Robust.Shared/Network/Messages/Handshake/MsgLoginSuccess.cs +++ b/Robust.Shared/Network/Messages/Handshake/MsgLoginSuccess.cs @@ -2,7 +2,7 @@ #nullable disable -namespace Robust.Shared.Network.Messages +namespace Robust.Shared.Network.Messages.Handshake { internal sealed class MsgLoginSuccess : NetMessage { diff --git a/Robust.Shared/Network/NetManager.ClientConnect.cs b/Robust.Shared/Network/NetManager.ClientConnect.cs index ed0f9fa70..7fc4678df 100644 --- a/Robust.Shared/Network/NetManager.ClientConnect.cs +++ b/Robust.Shared/Network/NetManager.ClientConnect.cs @@ -13,6 +13,7 @@ using Lidgren.Network; using Newtonsoft.Json; using Robust.Shared.Log; using Robust.Shared.Network.Messages; +using Robust.Shared.Network.Messages.Handshake; using Robust.Shared.Utility; namespace Robust.Shared.Network diff --git a/Robust.Shared/Network/NetManager.ServerAuth.cs b/Robust.Shared/Network/NetManager.ServerAuth.cs index a9a011895..39bb62b89 100644 --- a/Robust.Shared/Network/NetManager.ServerAuth.cs +++ b/Robust.Shared/Network/NetManager.ServerAuth.cs @@ -8,6 +8,7 @@ using Lidgren.Network; using Newtonsoft.Json; using Robust.Shared.Log; using Robust.Shared.Network.Messages; +using Robust.Shared.Network.Messages.Handshake; using Robust.Shared.Utility; using UsernameHelpers = Robust.Shared.AuthLib.UsernameHelpers; diff --git a/Robust.Shared/Physics/CollisionRay.cs b/Robust.Shared/Physics/CollisionRay.cs index 66e87b123..b804c7901 100644 --- a/Robust.Shared/Physics/CollisionRay.cs +++ b/Robust.Shared/Physics/CollisionRay.cs @@ -1,6 +1,7 @@ using System; +using Robust.Shared.Maths; -namespace Robust.Shared.Maths +namespace Robust.Shared.Physics { /// /// A representation of a 2D ray. diff --git a/Robust.Shared/Physics/IPhysShape.cs b/Robust.Shared/Physics/IPhysShape.cs index 45c5e4100..2378827b6 100644 --- a/Robust.Shared/Physics/IPhysShape.cs +++ b/Robust.Shared/Physics/IPhysShape.cs @@ -7,7 +7,7 @@ namespace Robust.Shared.Physics /// /// A primitive physical shape that is used by a . /// - public interface IPhysShape : IExposeData + public interface IPhysShape { /// /// Raised when any of the parameters on this physics shape change. diff --git a/Robust.Shared/Physics/PhysShapeAabb.cs b/Robust.Shared/Physics/PhysShapeAabb.cs index ba8cbcd0f..93ad78bd9 100644 --- a/Robust.Shared/Physics/PhysShapeAabb.cs +++ b/Robust.Shared/Physics/PhysShapeAabb.cs @@ -1,6 +1,7 @@ using System; using Robust.Shared.Maths; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Robust.Shared.Physics @@ -11,10 +12,14 @@ namespace Robust.Shared.Physics /// entity origin in world space. /// [Serializable, NetSerializable] + [DataDefinition] public class PhysShapeAabb : IPhysShape { + [DataFieldWithFlag("layer", typeof(CollisionLayer))] private int _collisionLayer; + [DataFieldWithFlag("mask", typeof(CollisionMask))] private int _collisionMask; + [DataField("bounds")] private Box2 _localBounds = Box2.UnitCentered; /// @@ -78,13 +83,5 @@ namespace Robust.Shared.Physics { return _localBounds; } - - /// - void IExposeData.ExposeData(ObjectSerializer serializer) - { - serializer.DataField(ref _collisionLayer, "layer", 0, WithFormat.Flags()); - serializer.DataField(ref _collisionMask, "mask", 0, WithFormat.Flags()); - serializer.DataField(ref _localBounds, "bounds", Box2.UnitCentered); - } } } diff --git a/Robust.Shared/Physics/PhysShapeCircle.cs b/Robust.Shared/Physics/PhysShapeCircle.cs index 1a845fff7..d40c1eb42 100644 --- a/Robust.Shared/Physics/PhysShapeCircle.cs +++ b/Robust.Shared/Physics/PhysShapeCircle.cs @@ -1,6 +1,7 @@ using System; using Robust.Shared.Maths; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Robust.Shared.Physics @@ -10,12 +11,16 @@ namespace Robust.Shared.Physics /// and it's origin is always the same as the entity position. /// [Serializable, NetSerializable] + [DataDefinition] public class PhysShapeCircle : IPhysShape { private const float DefaultRadius = 0.5f; + [DataFieldWithFlag("layer", typeof(CollisionLayer))] private int _collisionLayer; + [DataFieldWithFlag("mask", typeof(CollisionMask))] private int _collisionMask; + [DataField("radius")] private float _radius = DefaultRadius; /// @@ -60,14 +65,6 @@ namespace Robust.Shared.Physics } } - /// - void IExposeData.ExposeData(ObjectSerializer serializer) - { - serializer.DataField(ref _collisionLayer, "layer", 0, WithFormat.Flags()); - serializer.DataField(ref _collisionMask, "mask", 0, WithFormat.Flags()); - serializer.DataField(ref _radius, "radius", DefaultRadius); - } - /// public Box2 CalculateLocalBounds(Angle rotation) { diff --git a/Robust.Shared/Physics/PhysShapeRect.cs b/Robust.Shared/Physics/PhysShapeRect.cs index c8c7c9222..edca107f4 100644 --- a/Robust.Shared/Physics/PhysShapeRect.cs +++ b/Robust.Shared/Physics/PhysShapeRect.cs @@ -1,6 +1,7 @@ using System; using Robust.Shared.Maths; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Robust.Shared.Physics @@ -11,11 +12,15 @@ namespace Robust.Shared.Physics /// entity origin in world space. /// [Serializable, NetSerializable] + [DataDefinition] public class PhysShapeRect : IPhysShape { + [DataFieldWithFlag("layer", typeof(CollisionLayer))] private int _collisionLayer; + [DataFieldWithFlag("mask", typeof(CollisionMask))] private int _collisionMask; + [DataField("bounds")] private Box2 _rectangle = Box2.UnitCentered; [ViewVariables(VVAccess.ReadWrite)] public Box2 Rectangle @@ -64,13 +69,6 @@ namespace Robust.Shared.Physics handle.SetTransform(Matrix3.Identity); } - void IExposeData.ExposeData(ObjectSerializer serializer) - { - serializer.DataField(ref _collisionLayer, "layer", 0, WithFormat.Flags()); - serializer.DataField(ref _collisionMask, "mask", 0, WithFormat.Flags()); - serializer.DataField(ref _rectangle, "bounds", Box2.UnitCentered); - } - [field: NonSerialized] public event Action? OnDataChanged; diff --git a/Robust.Shared/Physics/Ray.cs b/Robust.Shared/Physics/Ray.cs index 9f492edde..796e81049 100644 --- a/Robust.Shared/Physics/Ray.cs +++ b/Robust.Shared/Physics/Ray.cs @@ -1,7 +1,8 @@ -using Robust.Shared.Utility; -using System; +using System; +using Robust.Shared.Maths; +using Robust.Shared.Utility; -namespace Robust.Shared.Maths +namespace Robust.Shared.Physics { /// /// A representation of a 2D ray. diff --git a/Robust.Shared/Prototypes/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index ae7a29104..ce82f04ae 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -8,36 +8,45 @@ using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Log; using Robust.Shared.Maths; -using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; -using YamlDotNet.RepresentationModel; namespace Robust.Shared.Prototypes { /// /// Prototype that represents game entities. /// - [Prototype("entity")] - public class EntityPrototype : IPrototype, ISyncingPrototype + [Prototype("entity", -1)] + public class EntityPrototype : IPrototype { - [Dependency] private readonly IComponentFactory _componentFactory = default!; - /// /// The "in code name" of the object. Must be unique. /// [ViewVariables] + [DataField("id")] public string ID { get; private set; } = default!; /// /// The "in game name" of the object. What is displayed to most players. /// [ViewVariables, CanBeNull] - public string Name { get; private set; } = ""; + [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; /// @@ -46,18 +55,8 @@ namespace Robust.Shared.Prototypes [ViewVariables, CanBeNull] public string? LocalizationID { - get - { - if(_localizationId == null) - { - _localizationId = $"ent-{CaseConversion.PascalToKebab(ID)}"; - } - return _localizationId; - } - private set - { - _localizationId = value; - } + get => _localizationId ??= $"ent-{CaseConversion.PascalToKebab(ID)}"; + private set => _localizationId = value; } /// @@ -65,13 +64,31 @@ namespace Robust.Shared.Prototypes /// to provide additional info without ruining the Name property itself. /// [ViewVariables] - public string? EditorSuffix { get; private set; } + [DataField("suffix")] + public string? EditorSuffix + { + get => _editorSuffix; + private set => _editorSuffix = value != null ? Loc.GetString(value) : null; + } + + private string? _editorSuffix; /// /// The description of the object that shows upon using examine /// [ViewVariables] - public string Description { get; private set; } = ""; + [DataField("description")] + public string Description + { + get => _description; + private set + { + _descriptionModified = true; + _description = Loc.GetString(value); + } + + } + private string _description = ""; private bool _descriptionModified; @@ -79,54 +96,61 @@ namespace Robust.Shared.Prototypes /// If true, this object should not show up in the entity spawn panel. /// [ViewVariables] + [NeverPushInheritance] + [DataField("abstract")] public bool Abstract { get; private set; } + [DataField("placement")] + private EntityPlacementProperties PlacementProperties = new(); + /// /// The different mounting points on walls. (If any). /// [ViewVariables] - public List? MountingPoints { get; private set; } + public List? MountingPoints => PlacementProperties.MountingPoints; /// /// 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. /// [ViewVariables] - public string PlacementMode { get; protected set; } = "PlaceFree"; + public string PlacementMode => PlacementProperties.PlacementMode; /// /// 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. /// [ViewVariables] - public int PlacementRange { get; protected set; } = DEFAULT_RANGE; + public int PlacementRange => PlacementProperties.PlacementRange; private const int DEFAULT_RANGE = 200; /// /// Set to hold snapping categories that this object has applied to it such as pipe/wire/wallmount /// - private readonly HashSet _snapFlags = new(); + private HashSet _snapFlags => PlacementProperties.SnapFlags; - private bool _snapOverriden = false; + private bool _snapOverriden => PlacementProperties.SnapOverriden; /// /// Offset that is added to the position when placing. (if any). Client only. /// [ViewVariables] - public Vector2i PlacementOffset { get; protected set; } + public Vector2i PlacementOffset => PlacementProperties.PlacementOffset; - private bool _placementOverriden = false; + private bool _placementOverriden => PlacementProperties.PlacementOverriden; /// /// True if this entity will be saved by the map loader. /// [ViewVariables] + [DataField("save")] public bool MapSavable { get; protected set; } = true; /// /// The prototype we inherit from. /// [ViewVariables] - public EntityPrototype? Parent { get; private set; } + [DataField("parent")] + public string? Parent { get; private set; } /// /// A list of children inheriting from this prototype. @@ -136,20 +160,12 @@ namespace Robust.Shared.Prototypes public bool IsRoot => Parent == null; - /// - /// Used to store the parent id until we sync when all templates are done loading. - /// - private string? parentTemp; - /// /// A dictionary mapping the component type list to the YAML mapping containing their settings. /// - public Dictionary Components { get; } = new(); - - /// - /// The mapping node inside the data field of the prototype. Null if no data field exists. - /// - public YamlMappingNode? DataNode { get; set; } + [field: DataField("components")] + [field: AlwaysPushInheritance] + public ComponentRegistry Components { get; } = new(); private readonly HashSet ReferenceTypes = new(); @@ -163,322 +179,27 @@ namespace Robust.Shared.Prototypes public EntityPrototype() { // Everybody gets a transform component! - Components.Add("Transform", new YamlMappingNode()); + Components.Add("Transform", new TransformComponent()); // And a metadata component too! - Components.Add("MetaData", new YamlMappingNode()); + Components.Add("MetaData", new MetaDataComponent()); } - public void LoadFrom(YamlMappingNode mapping) + public bool TryGetComponent(string name, [NotNullWhen(true)] out T? component) where T : IComponent { - var loc = IoCManager.Resolve(); - ID = mapping.GetNode("id").AsString(); - - if (mapping.TryGetNode("localizationId", out var locId)) { - _localizationId = locId.ToString(); - } - if (mapping.TryGetNode("name", out var node)) + if (!Components.TryGetValue(name, out var componentUnCast)) { - _nameModified = true; - Name = loc.GetString(node.AsString()); - } - else if (loc.TryGetString($"ent-{CaseConversion.PascalToKebab(ID)}", out var name)) - { - _nameModified = true; - Name = name; + component = default; + return false; } - if (mapping.TryGetNode("parent", out node)) - { - parentTemp = node.AsString(); - } - - // DESCRIPTION - if (mapping.TryGetNode("description", out node)) - { - _descriptionModified = true; - Description = loc.GetString(node.AsString()); - } - else if (loc.TryGetString($"ent-{CaseConversion.PascalToKebab(ID)}.desc", out var name)) - { - _descriptionModified = true; - Description = loc.GetString(name); - } - - if (mapping.TryGetNode("suffix", out node)) - { - EditorSuffix = loc.GetString(node.AsString()); - } - - // COMPONENTS - if (mapping.TryGetNode("components", out var componentsequence)) - { - foreach (var componentMapping in componentsequence.Cast()) - { - ReadComponent(componentMapping, _componentFactory); - } - - // Assert that there are no conflicting component references. - foreach (var componentName in Components.Keys) - { - var registration = _componentFactory.GetRegistration(componentName); - foreach (var type in registration.References) - { - if (ReferenceTypes.Contains(type)) - { - throw new InvalidOperationException( - $"Duplicate component reference in prototype: '{type}'"); - } - - ReferenceTypes.Add(type); - } - } - } - - // DATA FIELD - if (mapping.TryGetNode("data", out var dataMapping)) - { - DataNode = dataMapping; - } - - // PLACEMENT - // TODO: move to a component or something. Shouldn't be a root part of prototypes IMO. - if (mapping.TryGetNode("placement", out var placementMapping)) - { - ReadPlacementProperties(placementMapping); - } - - // SAVING - if (mapping.TryGetNode("save", out node)) - { - MapSavable = node.AsBool(); - } - - if (mapping.TryGetNode("abstract", out node)) - { - Abstract = node.AsBool(); - } - } - - private void ReadPlacementProperties(YamlMappingNode mapping) - { - if (mapping.TryGetNode("mode", out var node)) - { - PlacementMode = node.AsString(); - _placementOverriden = true; - } - - if (mapping.TryGetNode("offset", out node)) - { - PlacementOffset = node.AsVector2i(); - _placementOverriden = true; - } - - if (mapping.TryGetNode("nodes", out var sequence)) - { - MountingPoints = sequence.Select(p => p.AsInt()).ToList(); - } - - if (mapping.TryGetNode("range", out node)) - { - PlacementRange = node.AsInt(); - } - - // Reads snapping flags that this object holds that describe its properties to such as wire/pipe/wallmount, used to prevent certain stacked placement - if (mapping.TryGetNode("snap", out var snapSequence)) - { - var flagsList = snapSequence.Select(p => p.AsString()); - foreach (var flag in flagsList) - { - _snapFlags.Add(flag); - } - - _snapOverriden = true; - } - } - - public void Reset() - { - Children.Clear(); - } - - // Resolve inheritance. - public bool Sync(IPrototypeManager manager, int stage) - { - switch (stage) - { - case 0: - if (parentTemp == null) - { - return true; - } - - Parent = manager.Index(parentTemp); - if (Parent.Children == null) - { - Parent.Children = new List(); - } - - Parent.Children.Add(this); - return false; - - case 1: - // We are a root-level prototype. - // As such we're getting the duty of pushing inheritance into everybody's face. - // Can't do a "puller" system where each queries the parent because it requires n stages - // (n being the depth of each inheritance tree) - - if (Children == null) - { - break; - } - - PushInheritanceAll(); - break; - } - - return false; - } - - /// - /// Iteratively pushes inheritance down to all children, children's children, etc. breadth-first. - /// - private void PushInheritanceAll() - { - if (Children == null) - { - return; - } - - var sourceTargets = new List<(EntityPrototype, List)> {(this, Children)}; - var newSources = new List(); - while (true) - { - foreach (var (source, targetList) in sourceTargets) - { - if (targetList == null) - { - continue; - } - - foreach (var target in targetList) - { - PushInheritance(_componentFactory, source, target); - } - - newSources.AddRange(targetList); - } - - if (newSources.Count == 0) - { - break; - } - - sourceTargets.Clear(); - foreach (var newSource in newSources) - { - sourceTargets.Add((newSource, newSource.Children)); - } - - newSources.Clear(); - } - } - - private static void PushInheritance(IComponentFactory factory, EntityPrototype source, EntityPrototype target) - { - // Copy component data over. - foreach (KeyValuePair component in source.Components) - { - if (target.Components.TryGetValue(component.Key, out var targetComponent)) - { - // Copy over values the target component does not have. - foreach (YamlNode key in component.Value.Children.Keys) - { - if (!targetComponent.Children.ContainsKey(key)) - { - targetComponent.Children[key] = component.Value[key]; - } - } - } - else - { - // Copy component into the target, since it doesn't have it yet. - // Unless it'd cause a conflict. - foreach (var refType in factory.GetRegistration(component.Key).References) - { - if (target.ReferenceTypes.Contains(refType)) - { - // yeah nope the child's got it!! NEXT!! - goto next; - } - } - - target.Components[component.Key] = new YamlMappingNode(component.Value.AsEnumerable()); - } - - next: ; - } - - // Copy all simple data over. - if (!target._placementOverriden) - { - target.PlacementMode = source.PlacementMode; - } - - if (!target._placementOverriden) - { - target.PlacementOffset = source.PlacementOffset; - } - - if (target.MountingPoints == null && source.MountingPoints != null) - { - target.MountingPoints = new List(source.MountingPoints); - } - - if (target.PlacementRange == DEFAULT_RANGE) - { - target.PlacementRange = source.PlacementRange; - } - - if (!target._descriptionModified) - { - target.Description = source.Description; - } - - if (target.EditorSuffix == null) - { - target.EditorSuffix = source.EditorSuffix; - } - - if (!target._snapOverriden) - { - foreach (var flag in source._snapFlags) - { - target._snapFlags.Add(flag); - } - } - - if (!target._nameModified) - { - target.Name = source.Name; - } - - if (target.Children == null) - { - return; - } + // 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) { - bool HasBeenModified(string name, YamlMappingNode data, EntityPrototype prototype, IComponent currentComponent, IComponentFactory factory) - { - var component = factory.GetComponent(name); - prototype.CurrentDeserializingComponent = name; - ObjectSerializer ser = YamlObjectSerializer.NewReader(data, new PrototypeSerializationContext(prototype)); - component.ExposeData(ser); - return component == (Component) currentComponent; - } - 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}'"); @@ -503,8 +224,7 @@ namespace Robust.Shared.Prototypes // Find components to be removed, and remove them foreach (var (name, type) in oldPrototypeComponents.Except(newPrototypeComponents)) { - if (!HasBeenModified(name, oldPrototype.Components[name], oldPrototype, entity.GetComponent(type), - factory) && Components.Keys.Contains(name)) + if (Components.Keys.Contains(name)) { ignoredComponents.Add(name); continue; @@ -515,15 +235,16 @@ namespace Robust.Shared.Prototypes componentManager.CullRemovedComponents(); + var componentDependencyManager = IoCManager.Resolve(); + // 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); - ObjectSerializer ser = YamlObjectSerializer.NewReader(data, new PrototypeSerializationContext(this)); CurrentDeserializingComponent = name; component.Owner = entity; - component.ExposeData(ser); + componentDependencyManager.OnComponentAdd(entity, component); entity.AddComponent(component); } @@ -531,30 +252,25 @@ namespace Robust.Shared.Prototypes entity.MetaData.EntityPrototype = this; } - internal static void LoadEntity(EntityPrototype? prototype, Entity entity, IComponentFactory factory, IEntityLoadContext? context) + internal static void LoadEntity(EntityPrototype? prototype, Entity entity, IComponentFactory factory, IEntityLoadContext? context) //yeah officer this method right here { - YamlObjectSerializer.Context? defaultContext = null; + /*YamlObjectSerializer.Context? defaultContext = null; if (context == null) { defaultContext = new PrototypeSerializationContext(prototype); - } + }*/ if (prototype != null) { foreach (var (name, data) in prototype.Components) { - ObjectSerializer ser; + var fullData = data; if (context != null) { - ser = context.GetComponentSerializer(name, data); - } - else - { - prototype.CurrentDeserializingComponent = name; - ser = YamlObjectSerializer.NewReader(data, defaultContext); + fullData = context.GetComponentData(name, data); } - EnsureCompExistsAndDeserialize(entity, factory, name, ser); + EnsureCompExistsAndDeserialize(entity, factory, name, fullData, context as ISerializationContext); } } @@ -570,17 +286,16 @@ namespace Robust.Shared.Prototypes continue; } - var ser = context.GetComponentSerializer(name, null); + var ser = context.GetComponentData(name, null); - EnsureCompExistsAndDeserialize(entity, factory, name, ser); + EnsureCompExistsAndDeserialize(entity, factory, name, ser, context as ISerializationContext); } } } - private static void EnsureCompExistsAndDeserialize(Entity entity, IComponentFactory factory, string compName, ObjectSerializer ser) + private static void EnsureCompExistsAndDeserialize(Entity entity, IComponentFactory factory, string compName, IComponent data, ISerializationContext? context) { var compType = factory.GetRegistration(compName).Type; - ser.CurrentType = compType; if (!entity.TryGetComponent(compType, out var component)) { @@ -590,39 +305,8 @@ namespace Robust.Shared.Prototypes component = newComponent; } - component.ExposeData(ser); - } - - private void ReadComponent(YamlMappingNode mapping, IComponentFactory factory) - { - string type = mapping.GetNode("type").AsString(); - // See if type exists to detect errors. - switch (factory.GetComponentAvailability(type)) - { - case ComponentAvailability.Available: - break; - - case ComponentAvailability.Ignore: - return; - - case ComponentAvailability.Unknown: - Log.Logger.Error($"Unknown component '{type}' in prototype {ID}!"); - return; - } - - // Has this type already been added? - if (Components.Keys.Contains(type)) - { - Log.Logger.Error($"Component of type '{type}' defined twice in prototype {ID}!"); - return; - } - - var copy = new YamlMappingNode(mapping.AsEnumerable()); - // TODO: figure out a better way to exclude the type node. - // Also maybe deep copy this? Right now it's pretty error prone. - copy.Children.Remove(new YamlScalarNode("type")); - - Components[type] = copy; + // TODO use this value to support struct components + _ = IoCManager.Resolve().Copy(data, component, context); } public override string ToString() @@ -630,7 +314,64 @@ namespace Robust.Shared.Prototypes return $"EntityPrototype({ID})"; } - private class PrototypeSerializationContext : YamlObjectSerializer.Context + public class ComponentRegistry : Dictionary + { + public ComponentRegistry() + { + } + + public ComponentRegistry(Dictionary 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? MountingPoints; + + [DataField("range")] public int PlacementRange = DEFAULT_RANGE; + private HashSet _snapFlags = new (); + + [DataField("snap")] + public HashSet SnapFlags + { + get => _snapFlags; + set + { + SnapOverriden = true; + _snapFlags = value; + } + } + } + /*private class PrototypeSerializationContext : YamlObjectSerializer.Context { readonly EntityPrototype? prototype; @@ -696,6 +437,6 @@ namespace Robust.Shared.Prototypes return prototype.DataCache.TryGetValue(field, out value); } - } + }*/ } } diff --git a/Robust.Shared/Prototypes/IPrototype.cs b/Robust.Shared/Prototypes/IPrototype.cs index f7ee74463..020da7207 100644 --- a/Robust.Shared/Prototypes/IPrototype.cs +++ b/Robust.Shared/Prototypes/IPrototype.cs @@ -17,31 +17,6 @@ namespace Robust.Shared.Prototypes /// string ID { get; } - /// - /// Load data from the YAML mappings in the prototype files. - /// - void LoadFrom(YamlMappingNode mapping); - } - - /// - /// Extension on that allows "syncing" between prototypes after all prototypes have done initial loading. - /// To resolve reference like the entity prototype parenting. - /// - public interface ISyncingPrototype - { - void Reset(); - - /// - /// Sync and update cross-referencing data. - /// Syncing works in stages, each time it will be called with the stage it's currently on. - /// Each prototype will be called in a stage, then the stage count goes up. - /// - /// - /// The order of syncing is in no way guaranteed to be consistent across stages. - /// This means that on stage 1 prototype A might sync first, but on stage 2 prototype B might. - /// - /// The current sync stage. - /// Whether or not the prototype will be included in the next sync stage - bool Sync(IPrototypeManager manager, int stage); + string? Parent { get; } } } diff --git a/Robust.Shared/Prototypes/PrototypeInheritanceTree.cs b/Robust.Shared/Prototypes/PrototypeInheritanceTree.cs new file mode 100644 index 000000000..44a53e8c9 --- /dev/null +++ b/Robust.Shared/Prototypes/PrototypeInheritanceTree.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; + +namespace Robust.Shared.Prototypes +{ + public class PrototypeInheritanceTree + { + private Dictionary> _nodes = new(); + + private Dictionary> _pendingParent = new(); + + private HashSet _baseNodes = new(); + + private Dictionary _parents = new(); + + public IReadOnlySet BaseNodes => _baseNodes; + + public IReadOnlySet Children(string id) + { + if (!_nodes.ContainsKey(id)) + throw new ArgumentException($"ID {id} not present in InheritanceTree", nameof(id)); + return _nodes[id]; + } + + public string GetBaseNode(string id) + { + if (!_nodes.ContainsKey(id)) + throw new ArgumentException($"ID {id} not present in InheritanceTree", nameof(id)); + + var parent = id; + while (_parents.TryGetValue(parent, out var nextParent)) + { + parent = nextParent; + } + + return parent; + } + + public string? GetParent(string id) + { + return _parents.GetValueOrDefault(id); + } + + public void AddId(string id, string? parent, bool overwrite = false) + { + if (overwrite && HasId(id)) + { + RemoveId(id); + } + + if (_nodes.ContainsKey(id)) + throw new ArgumentException($"ID {id} already present in InheritanceTree", nameof(id)); + + if (parent != null) + { + _parents.Add(id, parent); + + if (_nodes.TryGetValue(parent, out var parentsChildren)) + { + parentsChildren.Add(id); + } + else + { + if (!_pendingParent.TryGetValue(parent, out _)) + { + _pendingParent[parent] = new HashSet(); + } + + _pendingParent[parent].Add(id); + } + + //cycle detection + var currentParent = parent; + while (currentParent != null) + { + if (currentParent == id) + throw new InvalidOperationException( + $"Cycle detected when trying to add id {id} with parent {parent}"); + _parents.TryGetValue(currentParent, out currentParent); + } + } + else + { + _baseNodes.Add(id); + } + + if (!_pendingParent.TryGetValue(id, out var ourChildren)) + ourChildren = new HashSet(); + + _nodes.Add(id, ourChildren); + } + + public bool HasId(string id) + { + return _nodes.ContainsKey(id); + } + + public void RemoveId(string id) + { + if (!_nodes.ContainsKey(id)) + throw new ArgumentException($"ID {id} not present in InheritanceTree", nameof(id)); + + _nodes.Remove(id); + foreach (var (_, children) in _pendingParent) + { + children.Remove(id); + } + + _baseNodes.Remove(id); + _parents.Remove(id); + } + } +} diff --git a/Robust.Shared/Prototypes/PrototypeManager.cs b/Robust.Shared/Prototypes/PrototypeManager.cs index 94a35c4e3..80a74bbb0 100644 --- a/Robust.Shared/Prototypes/PrototypeManager.cs +++ b/Robust.Shared/Prototypes/PrototypeManager.cs @@ -5,7 +5,6 @@ using System.IO; using System.Linq; using System.Runtime.Serialization; using System.Threading; -using System.Threading.Tasks; using JetBrains.Annotations; using Robust.Shared.Asynchronous; using Robust.Shared.ContentPack; @@ -15,6 +14,11 @@ using Robust.Shared.IoC.Exceptions; using Robust.Shared.Log; using Robust.Shared.Network; using Robust.Shared.Reflection; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; using Robust.Shared.Utility; using YamlDotNet.Core; using YamlDotNet.RepresentationModel; @@ -65,7 +69,9 @@ namespace Robust.Shared.Prototypes /// /// Load prototypes from files in a directory, recursively. /// - Task> LoadDirectory(ResourcePath path); + List LoadDirectory(ResourcePath path); + + Dictionary> ValidateDirectory(ResourcePath path); List LoadFromStream(TextReader stream); @@ -108,13 +114,16 @@ namespace Robust.Shared.Prototypes [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [BaseTypeRequired(typeof(IPrototype))] [MeansImplicitUse] + [MeansDataDefinition] public class PrototypeAttribute : Attribute { private readonly string type; public string Type => type; - public PrototypeAttribute(string type) + public readonly int LoadPriority = 1; + public PrototypeAttribute(string type, int loadPriority = 1) { this.type = type; + LoadPriority = loadPriority; } } @@ -126,8 +135,10 @@ namespace Robust.Shared.Prototypes [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] public readonly ITaskManager TaskManager = default!; [Dependency] public readonly INetManager NetManager = default!; + [Dependency] private readonly ISerializationManager _serializationManager = default!; private readonly Dictionary prototypeTypes = new(); + private readonly Dictionary prototypePriorities = new(); private bool _initialized; private bool _hasEverBeenReloaded; @@ -135,6 +146,8 @@ namespace Robust.Shared.Prototypes #region IPrototypeManager members private readonly Dictionary> prototypes = new(); + private readonly Dictionary> _prototypeResults = new(); + private readonly Dictionary _inheritanceTrees = new(); private readonly HashSet IgnoredPrototypeTypes = new(); @@ -198,24 +211,68 @@ namespace Robust.Shared.Prototypes { prototypeTypes.Clear(); prototypes.Clear(); + _prototypeResults.Clear(); + _inheritanceTrees.Clear(); } - public virtual async void ReloadPrototypes(ResourcePath file) + private int SortPrototypesByPriority(Type a, Type b) + { + return prototypePriorities[b].CompareTo(prototypePriorities[a]); + } + + public virtual void ReloadPrototypes(ResourcePath file) { #if !FULL_RELEASE - var changed = await LoadFile(file.ToRootedPath(), true); - Resync(); + var changed = LoadFile(file.ToRootedPath(), true).ToList(); + changed.Sort((prototype, prototype1) => SortPrototypesByPriority(prototype.GetType(), prototype1.GetType())); + var pushed = new Dictionary>(); foreach (var prototype in changed) { - if (prototype is not EntityPrototype entityPrototype) + var type = prototype.GetType(); + if (!pushed.ContainsKey(type)) pushed[type] = new HashSet(); + var baseNode = prototype.ID; + + if (pushed[type].Contains(baseNode)) { continue; } - foreach (var entity in _entityManager.GetEntities(new PredicateEntityQuery(e => e.Prototype != null && e.Prototype.ID == entityPrototype.ID))) + var tree = _inheritanceTrees[type]; + var currentNode = prototype.Parent; + + if (currentNode == null) { - entityPrototype.UpdateEntity((Entity) entity); + PushInheritance(type, baseNode, null, pushed[type]); + continue; + } + + while (true) + { + var parent = tree.GetParent(currentNode); + + if (parent == null) + { + break; + } + + baseNode = currentNode; + currentNode = parent; + } + + PushInheritance(type, currentNode, baseNode, null, pushed[type]); + } + + // TODO filter by entity prototypes changed + if (!pushed.ContainsKey(typeof(EntityPrototype))) return; + + var entityPrototypes = prototypes[typeof(EntityPrototype)]; + + foreach (var prototype in pushed[typeof(EntityPrototype)]) + { + foreach (var entity in _entityManager.GetEntities(new PredicateEntityQuery(e => e.Prototype != null && e.Prototype.ID == prototype))) + { + ((EntityPrototype) entityPrototypes[prototype]).UpdateEntity((Entity) entity); } } #endif @@ -223,66 +280,51 @@ namespace Robust.Shared.Prototypes public void Resync() { - // TODO Make this smarter and only resync changed prototypes - if (_hasEverResynced) + var trees = _inheritanceTrees.Keys.ToList(); + trees.Sort(SortPrototypesByPriority); + foreach (var type in trees) { - foreach (var prototypeList in prototypes.Values) + var tree = _inheritanceTrees[type]; + foreach (var baseNode in tree.BaseNodes) { - foreach (var prototype in prototypeList.Values) - { - if (prototype is ISyncingPrototype syncing) - { - syncing.Reset(); - } - } + PushInheritance(type, baseNode, null, new HashSet()); } } + } - foreach (Type type in prototypeTypes.Values.Where(t => typeof(ISyncingPrototype).IsAssignableFrom(t))) + public void PushInheritance(Type type, string id, string child, DeserializationResult? baseResult, HashSet changed) + { + changed.Add(id); + + var myRes = _prototypeResults[type][id]; + var newResult = baseResult != null ? myRes.PushInheritanceFrom(baseResult) : myRes; + + PushInheritance(type, child, newResult, changed); + + newResult.CallAfterDeserializationHook(); + var populatedRes = _serializationManager.PopulateDataDefinition(prototypes[type][id], (IDeserializedDefinition)newResult); + prototypes[type][id] = (IPrototype) populatedRes.RawValue!; + } + + public void PushInheritance(Type type, string id, DeserializationResult? baseResult, HashSet changed) + { + changed.Add(id); + + var myRes = _prototypeResults[type][id]; + var newResult = baseResult != null ? myRes.PushInheritanceFrom(baseResult) : myRes; + + foreach (var childID in _inheritanceTrees[type].Children(id)) { - // This list is the list of prototypes we're syncing. - // Iterate using indices. - // IF the prototype wants to NOT by synced again, - // Swap remove it with the one at the end of the list, - // and do the whole thing again with the one formerly at the end of the list - // otherwise keep it and move up an index - // When we get to the end, do the whole thing again! - // Yes this is ridiculously overengineered BUT IT PERFORMS WELL. - // I hope. - List currentRun = prototypes[type].Values.Select(p => (ISyncingPrototype) p).ToList(); - - var stage = 0; - // Outer loop to iterate stages. - while (currentRun.Count > 0) - { - // Increase positions to iterate over list. - // If we need to stick, i gets reduced down below. - for (var i = 0; i < currentRun.Count; i++) - { - ISyncingPrototype prototype = currentRun[i]; - var result = prototype.Sync(this, stage); - // Keep prototype and move on to next one if it returns true. - // Thus it stays in the list for next stage. - if (result) - { - continue; - } - - // Move the last element in the list to where we are currently. - // Since we don't break we'll do this one next, as i stays the same. - // (for loop cancels out decrement here) - currentRun.RemoveSwap(i); - i--; - } - stage++; - } + PushInheritance(type, childID, newResult, changed); } - _hasEverResynced = true; + newResult.CallAfterDeserializationHook(); + var populatedRes = _serializationManager.PopulateDataDefinition(prototypes[type][id], (IDeserializedDefinition)newResult); + prototypes[type][id] = (IPrototype) populatedRes.RawValue!; } /// - public async Task> LoadDirectory(ResourcePath path) + public List LoadDirectory(ResourcePath path) { var changedPrototypes = new List(); @@ -292,14 +334,64 @@ namespace Robust.Shared.Prototypes foreach (var resourcePath in streams) { - var filePrototypes = await LoadFile(resourcePath); + var filePrototypes = LoadFile(resourcePath); changedPrototypes.AddRange(filePrototypes); } return changedPrototypes; } - private Task ReadFile(ResourcePath file, bool @throw = true) + public Dictionary> ValidateDirectory(ResourcePath path) + { + var streams = Resources.ContentFindFiles(path).ToList().AsParallel() + .Where(filePath => filePath.Extension == "yml" && !filePath.Filename.StartsWith(".")); + + var dict = new Dictionary>(); + foreach (var resourcePath in streams) + { + using var reader = ReadFile(resourcePath); + + if (reader == null) + { + continue; + } + + var yamlStream = new YamlStream(); + yamlStream.Load(reader); + + for (var i = 0; i < yamlStream.Documents.Count; i++) + { + var rootNode = (YamlSequenceNode) yamlStream.Documents[i].RootNode; + foreach (YamlMappingNode node in rootNode.Cast()) + { + var type = node.GetNode("type").AsString(); + if (!prototypeTypes.ContainsKey(type)) + { + if (IgnoredPrototypeTypes.Contains(type)) + { + continue; + } + + throw new PrototypeLoadException($"Unknown prototype type: '{type}'"); + } + + var mapping = node.ToDataNodeCast(); + mapping.RemoveNode("type"); + var errorNodes = _serializationManager.ValidateNode(prototypeTypes[type], mapping).GetErrors().ToHashSet(); + if (errorNodes.Count != 0) + { + if (!dict.TryGetValue(resourcePath.ToString(), out var hashSet)) + dict[resourcePath.ToString()] = new HashSet(); + dict[resourcePath.ToString()].UnionWith(errorNodes); + } + } + } + } + + return dict; + } + + private StreamReader? ReadFile(ResourcePath file, bool @throw = true) { var retries = 0; @@ -309,8 +401,7 @@ namespace Robust.Shared.Prototypes try { var reader = new StreamReader(Resources.ContentFileRead(file), EncodingHelpers.UTF8); - - return Task.FromResult(reader); + return reader; } catch (IOException e) { @@ -322,7 +413,7 @@ namespace Robust.Shared.Prototypes } Logger.Error($"Error reloading prototypes in file {file}.", e); - return Task.FromResult(null); + return null; } retries++; @@ -331,13 +422,13 @@ namespace Robust.Shared.Prototypes } } - public async Task> LoadFile(ResourcePath file, bool overwrite = false) + public HashSet LoadFile(ResourcePath file, bool overwrite = false) { - var changedPrototypes = new List(); + var changedPrototypes = new HashSet(); try { - using var reader = await ReadFile(file, !overwrite); + using var reader = ReadFile(file, !overwrite); if (reader == null) { @@ -354,7 +445,7 @@ namespace Robust.Shared.Prototypes try { var documentPrototypes = LoadFromDocument(yamlStream.Documents[i], overwrite); - changedPrototypes.AddRange(documentPrototypes); + changedPrototypes.UnionWith(documentPrototypes); } catch (Exception e) { @@ -418,9 +509,9 @@ namespace Robust.Shared.Prototypes } } - private List LoadFromDocument(YamlDocument document, bool overwrite = false) + private HashSet LoadFromDocument(YamlDocument document, bool overwrite = false) { - var changedPrototypes = new List(); + var changedPrototypes = new HashSet(); var rootNode = (YamlSequenceNode) document.RootNode; foreach (YamlMappingNode node in rootNode.Cast()) @@ -437,19 +528,19 @@ namespace Robust.Shared.Prototypes } var prototypeType = prototypeTypes[type]; - var prototype = _dynamicTypeFactory.CreateInstanceUnchecked(prototypeType); + var res = _serializationManager.Read(prototypeType, node.ToDataNode(), skipHook: true); + var prototype = (IPrototype) res.RawValue!; - prototype.LoadFrom(node); - changedPrototypes.Add(prototype); - - var id = prototype.ID; - - if (!overwrite && prototypes[prototypeType].ContainsKey(id)) + if (!overwrite && prototypes[prototypeType].ContainsKey(prototype.ID)) { - throw new PrototypeLoadException($"Duplicate ID: '{id}'"); + throw new PrototypeLoadException($"Duplicate ID: '{prototype.ID}'"); } - prototypes[prototypeType][id] = prototype; + _prototypeResults[prototypeType][prototype.ID] = res; + _inheritanceTrees[prototypeType].AddId(prototype.ID, prototype.Parent, true); + + prototypes[prototypeType][prototype.ID] = prototype; + changedPrototypes.Add(prototype); } return changedPrototypes; @@ -503,10 +594,13 @@ namespace Robust.Shared.Prototypes } prototypeTypes[attribute.Type] = type; + prototypePriorities[type] = attribute.LoadPriority; if (typeof(IPrototype).IsAssignableFrom(type)) { prototypes[type] = new Dictionary(); + _prototypeResults[type] = new Dictionary(); + _inheritanceTrees[type] = new PrototypeInheritanceTree(); } } diff --git a/Robust.Shared/Reflection/IReflectionManager.cs b/Robust.Shared/Reflection/IReflectionManager.cs index 2d9da0296..6e6df4803 100644 --- a/Robust.Shared/Reflection/IReflectionManager.cs +++ b/Robust.Shared/Reflection/IReflectionManager.cs @@ -71,6 +71,13 @@ namespace Robust.Shared.Reflection /// Enumeration of all types with the specified attribute. IEnumerable FindTypesWithAttribute() where T : Attribute; + /// + /// Finds all Types in all Assemblies that have a specific Attribute. + /// + /// Attribute to search for. + /// Enumeration of all types with the specified attribute. + IEnumerable FindTypesWithAttribute(Type attributeType); + /// /// Loads assemblies into the manager and get all the types. /// diff --git a/Robust.Shared/Reflection/ReflectionManager.cs b/Robust.Shared/Reflection/ReflectionManager.cs index 8e55c28fa..acf5d5782 100644 --- a/Robust.Shared/Reflection/ReflectionManager.cs +++ b/Robust.Shared/Reflection/ReflectionManager.cs @@ -160,12 +160,18 @@ namespace Robust.Shared.Reflection /// public IEnumerable FindTypesWithAttribute() where T : Attribute + { + return FindTypesWithAttribute(typeof(T)); + } + + /// + public IEnumerable FindTypesWithAttribute(Type attributeType) { var types = new List(); foreach (var assembly in Assemblies) { - types.AddRange(assembly.GetTypes().Where(type => Attribute.IsDefined(type, typeof(T)))); + types.AddRange(assembly.GetTypes().Where(type => Attribute.IsDefined(type, attributeType))); } return types; diff --git a/Robust.Shared/Serialization/ConstantsForAttribute.cs b/Robust.Shared/Serialization/ConstantsForAttribute.cs new file mode 100644 index 000000000..f3c8e8df4 --- /dev/null +++ b/Robust.Shared/Serialization/ConstantsForAttribute.cs @@ -0,0 +1,34 @@ +using System; + +namespace Robust.Shared.Serialization +{ + /// + /// Attribute for marking an enum type as being the constant representation for a field. + /// + /// Some fields are arbitrary ints, but it's helpful for readability to have them be + /// named constants instead. This allows for that. + /// + /// NB: AllowMultiple is true - don't assume the same representation cannot + /// be reused between multiple fields. + /// + [AttributeUsage(AttributeTargets.Enum, AllowMultiple = true, Inherited = false)] + public class ConstantsForAttribute : Attribute + { + private readonly Type _tag; + public Type Tag => _tag; + + // NB: This is not generic because C# does not allow generic attributes + + /// + /// An attribute with tag type + /// + /// + /// An arbitrary tag type used for coordinating between the data field and the + /// representation. Not actually used for serialization/deserialization. + /// + public ConstantsForAttribute(Type tag) + { + _tag = tag; + } + } +} diff --git a/Robust.Shared/Serialization/CustomFormatManager.cs b/Robust.Shared/Serialization/CustomFormatManager.cs deleted file mode 100644 index 3ca9ba5be..000000000 --- a/Robust.Shared/Serialization/CustomFormatManager.cs +++ /dev/null @@ -1,317 +0,0 @@ -using Robust.Shared.IoC; -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using System.Reflection; -using Robust.Shared.Reflection; - -namespace Robust.Shared.Serialization -{ - /// - public class CustomFormatManager : ICustomFormatManager - { - private Dictionary> _flagFormatters = new(); - private Dictionary> _constantFormatters = new(); - - public WithFormat FlagFormat() - { - if (!_flagFormatters.TryGetValue(typeof(T), out var formatter)) - { - formatter = new WithFlagRepresentation(GetFlag()); - _flagFormatters.Add(typeof(T), formatter); - } - - return formatter; - } - - public WithFormat ConstantFormat() - { - if (!_constantFormatters.TryGetValue(typeof(T), out var formatter)) - { - formatter = new WithConstantRepresentation(GetConstants()); - _constantFormatters.Add(typeof(T), formatter); - } - - return formatter; - } - - /// - /// Get the enum flag type for the given tag . - /// - /// - /// The tag type to use for finding the flag representation. To learn more, - /// see the . - /// - /// - /// Thrown if: - /// - /// - /// The tag type corresponds to no enum flag representation. - /// - /// - /// The tag type corresponds to more than one enum flag representation. - /// - /// - /// The tag type corresponds to a non-enum representation. - /// - /// - /// The tag type corresponds to a non-int enum representation. - /// - /// - /// The tag type corresponds to a non-bitflag int enum representation. - /// - /// - /// - /// - /// The unique int-backed bitflag enum type for the given tag. - /// - private Type GetFlag() - { - var reflectionManager = IoCManager.Resolve(); - - Type? flagType = null; - - foreach (Type bitflagType in reflectionManager.FindTypesWithAttribute()) - { - foreach (var flagsforAttribute in bitflagType.GetCustomAttributes(true)) - { - if (typeof(T) == flagsforAttribute.Tag) - { - if (flagType != null) - { - throw new NotSupportedException($"Multiple bitflag enums declared for the tag {flagsforAttribute.Tag}."); - } - - if (!bitflagType.IsEnum) - { - throw new FlagSerializerException($"Could not create FlagSerializer for non-enum {bitflagType}."); - } - - if (Enum.GetUnderlyingType(bitflagType) != typeof(int)) - { - throw new FlagSerializerException($"Could not create FlagSerializer for non-int enum {bitflagType}."); - } - - if (!bitflagType.GetCustomAttributes(false).Any()) - { - throw new FlagSerializerException($"Could not create FlagSerializer for non-bitflag enum {bitflagType}."); - } - - - flagType = bitflagType; - } - } - } - - if (flagType == null) - { - throw new FlagSerializerException($"Found no type marked with a `FlagsForAttribute(typeof({typeof(T)}))`."); - } - - return flagType; - } - - /// - /// Get the constant type for the given tag . - /// - /// - /// The tag type to use for finding the constant representation. To learn more, - /// see the . - /// - /// - /// Thrown if: - /// - /// - /// The tag type corresponds to no constant representation. - /// - /// - /// The tag type corresponds to more than one constant representation. - /// - /// - /// The tag type corresponds to a non-enum representation. - /// - /// - /// The tag type corresponds to a non-int enum representation. - /// - /// - /// - /// - /// The unique int-backed enum constant type for the given tag. - /// - private Type GetConstants() - { - var reflectionManager = IoCManager.Resolve(); - - Type? constantType = null; - - foreach (Type enumConstantType in reflectionManager.FindTypesWithAttribute()) - { - foreach (var constantsForAttribute in enumConstantType.GetCustomAttributes(true)) - { - if (typeof(T) == constantsForAttribute.Tag) - { - if (constantType != null) - { - throw new NotSupportedException($"Multiple constant enums declared for the tag {constantsForAttribute.Tag}."); - } - - if (!enumConstantType.IsEnum) - { - throw new ConstantSerializerException($"Could not create ConstantSerializer for non-enum {enumConstantType}."); - } - - if (Enum.GetUnderlyingType(enumConstantType) != typeof(int)) - { - throw new ConstantSerializerException($"Could not create ConstantSerializer for non-int enum {enumConstantType}."); - } - - constantType = enumConstantType; - } - } - } - - if (constantType == null) - { - throw new FlagSerializerException($"Found no type marked with a `ConstantsForAttribute(typeof({typeof(T)}))`."); - } - - return constantType; - } - } - - /// - /// int representation in terms of some enum flag type. - /// - public class WithFlagRepresentation : WithFormat - { - private Type _flagType; - public Type FlagType => _flagType; - - private YamlFlagSerializer _serializer; - - public WithFlagRepresentation(Type flagType) - { - _flagType = flagType; - _serializer = new YamlFlagSerializer(_flagType, this); - - } - - public override YamlObjectSerializer.TypeSerializer GetYamlSerializer() - { - return _serializer; - } - - public override Type Format => typeof(List); - - public override int FromCustomFormat(object obj) - { - var flagNames = (List)obj; - var flags = 0; - - foreach (var flagName in flagNames) - { - flags |= (int)Enum.Parse(_flagType, flagName); - } - - return flags; - } - - public override object ToCustomFormat([NotNull] int flags) - { - var flagNames = new List(); - - // Assumption: a bitflag enum has a constructor for every bit value such that - // that bit is set in some other constructor i.e. if a 1 appears somewhere in - // the bits of one of the enum constructors, there is an enum constructor which - // is 1 just in that position. - // - // Otherwise, this code may throw an exception - var maxFlagValue = ((int[])Enum.GetValues(_flagType)).Max(); - - for (var bitIndex = 1; bitIndex <= maxFlagValue; bitIndex = bitIndex << 1) - { - if ((bitIndex & flags) == bitIndex) - { - var flagName = Enum.GetName(_flagType, bitIndex); - - if (flagName == null) - { - throw new FlagSerializerException($"No bitflag corresponding to bit {bitIndex} in {_flagType}, but it was set anyways."); - } - - flagNames.Add(flagName); - } - } - - return flagNames; - } - } - - internal sealed class FlagSerializerException : Exception - { - public FlagSerializerException(string message) : base(message) - { - } - } - - /// - /// int representation in terms of some constant enum constructors. - /// - public class WithConstantRepresentation : WithFormat - { - private Type _constantType; - public Type ConstantType => _constantType; - - private YamlConstantSerializer _serializer; - - public WithConstantRepresentation(Type constantType) - { - _constantType = constantType; - _serializer = new YamlConstantSerializer(_constantType, this); - - } - - public override YamlObjectSerializer.TypeSerializer GetYamlSerializer() - { - return _serializer; - } - - public override Type Format => typeof(string); - - public override int FromCustomFormat(object obj) - { - return (int)Enum.Parse(_constantType, (string)obj); - } - - public int FromCustomFormatText(string text) - { - if (Enum.TryParse(_constantType, text, out var val)) - { - return (int) val!; - } - - return int.Parse(text, CultureInfo.InvariantCulture); - } - - public override object ToCustomFormat([NotNull] int value) - { - var constantName = Enum.GetName(_constantType, value); - - if (constantName == null) - { - throw new ConstantSerializerException($"No constant corresponding to value {value} in {_constantType}."); - } - - return constantName; - } - } - - internal sealed class ConstantSerializerException : Exception - { - public ConstantSerializerException(string message) : base(message) - { - } - } -} diff --git a/Robust.Shared/Serialization/DefaultValueSerializer.cs b/Robust.Shared/Serialization/DefaultValueSerializer.cs deleted file mode 100644 index ec862489f..000000000 --- a/Robust.Shared/Serialization/DefaultValueSerializer.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace Robust.Shared.Serialization -{ - public sealed class DefaultValueSerializer : ObjectSerializer - { - public static DefaultValueSerializer Reader() - { - return new() - { - Reading = true, - }; - } - - private DefaultValueSerializer() {} - - public override void DataField(ref T value, string name, T defaultValue, WithFormat withFormat, bool alwaysWrite = false) - { - if (Reading) - { - if (EqualityComparer.Default.Equals(value, default)) - value = defaultValue; - } - } - - public override T ReadDataField(string name, T defaultValue) - { - return defaultValue; - } - - public override bool TryReadDataField(string name, WithFormat format, [MaybeNullWhen(false)] out T value) - { - value = default; - return false; - } - - public override void DataField( - ref TTarget value, - string name, - TTarget defaultValue, - ReadConvertFunc ReadConvertFunc, - WriteConvertFunc? WriteConvertFunc = null, - bool alwaysWrite = false - ) - { - if (Reading) - { - if (EqualityComparer.Default.Equals(value, default)) - value = defaultValue; - } - } - - public override void DataReadFunction(string name, T defaultValue, ReadFunctionDelegate func) - { - if (Reading) - { - func(defaultValue); - } - } - - public override void DataWriteFunction(string name, T defaultValue, WriteFunctionDelegate func, bool alwaysWrite = false) - { - } - } -} diff --git a/Robust.Shared/Serialization/FlagsForAttribute.cs b/Robust.Shared/Serialization/FlagsForAttribute.cs new file mode 100644 index 000000000..98a5bbbaf --- /dev/null +++ b/Robust.Shared/Serialization/FlagsForAttribute.cs @@ -0,0 +1,36 @@ +using System; + +namespace Robust.Shared.Serialization +{ + /// + /// Attribute for marking an enum type as being the bitflag representation for a field. + /// + /// Some int values in the engine are bitflags, but the actual bitflag definitions + /// are reserved for the content layer. This means that serialization/deserialization + /// of those flags into readable YAML is impossible, unless the engine is notified + /// that a certain representation should be used. That's the role of this attribute. + /// + /// NB: AllowMultiple is true - don't assume the same representation cannot + /// be reused between multiple fields. + /// + [AttributeUsage(AttributeTargets.Enum, AllowMultiple = true, Inherited = false)] + public class FlagsForAttribute : Attribute + { + private readonly Type _tag; + public Type Tag => _tag; + + // NB: This is not generic because C# does not allow generic attributes + + /// + /// An attribute with tag type + /// + /// + /// An arbitrary tag type used for coordinating between the data field and the + /// representation. Not actually used for serialization/deserialization. + /// + public FlagsForAttribute(Type tag) + { + _tag = tag; + } + } +} diff --git a/Robust.Shared/Serialization/ICustomFormatManager.cs b/Robust.Shared/Serialization/ICustomFormatManager.cs deleted file mode 100644 index 600b79348..000000000 --- a/Robust.Shared/Serialization/ICustomFormatManager.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Robust.Shared.Serialization -{ - /// - /// Provides information about custom serialization formats used by certain fields. - /// - public interface ICustomFormatManager - { - /// - /// Get a custom int format in terms of enum flags, chosen by a tag type. - /// - /// - /// The tag type to select the representation with. To understand more about how - /// tag types are used, see the . - /// - /// - /// A custom serialization format for int values, chosen by the tag type. - /// - public WithFormat FlagFormat(); - - /// - /// Get a custom int format in terms of enum constants, chosen by a tag type. - /// - /// - /// The tag type to select the representation with. To understand more about how - /// tag types are used, see the . - /// - /// - /// A custom serialization format for int values, chosen by the tag type. - /// - public WithFormat ConstantFormat(); - } -} \ No newline at end of file diff --git a/Robust.Shared/Serialization/IExposeData.cs b/Robust.Shared/Serialization/IExposeData.cs deleted file mode 100644 index 445164b95..000000000 --- a/Robust.Shared/Serialization/IExposeData.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Robust.Shared.Analyzers; - -namespace Robust.Shared.Serialization -{ - /// - /// Interface for the "expose data" system, which is basically our method of handling data serialization. - /// - [RequiresExplicitImplementation] - public interface IExposeData - { - /// - /// Will get called to either make this object read data from or provide data to write to/from a serialization format. - /// This method should only rely on for reading data. - /// External data (i.e. current time) is unreliable as this method will get called to get a representation of this object from some data, - /// and may not be called again later. - /// - /// - /// A serializer that data can be read/written from using its various methods. - /// Tell it everything you want to preserve, even your dirtiest secrets. - /// - void ExposeData(ObjectSerializer serializer); - } -} diff --git a/Robust.Shared/Serialization/ISelfSerialize.cs b/Robust.Shared/Serialization/ISelfSerialize.cs index b7c74bb14..38d7ffa13 100644 --- a/Robust.Shared/Serialization/ISelfSerialize.cs +++ b/Robust.Shared/Serialization/ISelfSerialize.cs @@ -1,4 +1,4 @@ -namespace Robust.Shared.Serialization +namespace Robust.Shared.Serialization { public interface ISelfSerialize { diff --git a/Robust.Shared/Serialization/ISerializationHooks.cs b/Robust.Shared/Serialization/ISerializationHooks.cs new file mode 100644 index 000000000..7fa685382 --- /dev/null +++ b/Robust.Shared/Serialization/ISerializationHooks.cs @@ -0,0 +1,21 @@ +using Robust.Shared.Analyzers; + +namespace Robust.Shared.Serialization +{ + /// + /// Provides a method that gets executed after deserialization is complete and a method that gets executed before serialization + /// + [RequiresExplicitImplementation] + public interface ISerializationHooks + { + /// + /// Gets executed after deserialization is complete + /// + void AfterDeserialization() {} + + /// + /// Gets executed before serialization + /// + void BeforeSerialization() {} + } +} diff --git a/Robust.Shared/Serialization/InvalidMappingException.cs b/Robust.Shared/Serialization/InvalidMappingException.cs new file mode 100644 index 000000000..c958d9f5a --- /dev/null +++ b/Robust.Shared/Serialization/InvalidMappingException.cs @@ -0,0 +1,13 @@ +using System; + +namespace Robust.Shared.Serialization +{ + public class InvalidMappingException : Exception + { + + public InvalidMappingException(string msg) : base(msg) + { + + } + } +} diff --git a/Robust.Shared/Serialization/Manager/Attributes/AlwaysPushInheritanceAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/AlwaysPushInheritanceAttribute.cs new file mode 100644 index 000000000..f7c444bd5 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Attributes/AlwaysPushInheritanceAttribute.cs @@ -0,0 +1,9 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Robust.Shared.Serialization.Manager.Attributes +{ + //todo paul find a way to constrain this to datafields only & make exclusive w/ neverpush + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public class AlwaysPushInheritanceAttribute : Attribute { } +} diff --git a/Robust.Shared/Serialization/Manager/Attributes/CopyByRefAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/CopyByRefAttribute.cs new file mode 100644 index 000000000..64a4c8ea1 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Attributes/CopyByRefAttribute.cs @@ -0,0 +1,7 @@ +using System; + +namespace Robust.Shared.Serialization.Manager.Attributes +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Interface, Inherited = false)] + public class CopyByRefAttribute : Attribute {} +} diff --git a/Robust.Shared/Serialization/Manager/Attributes/DataDefinition.cs b/Robust.Shared/Serialization/Manager/Attributes/DataDefinition.cs new file mode 100644 index 000000000..532287ba9 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Attributes/DataDefinition.cs @@ -0,0 +1,12 @@ +using System; +using JetBrains.Annotations; + +namespace Robust.Shared.Serialization.Manager.Attributes +{ + [MeansDataDefinition] + [MeansImplicitUse] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)] + public class DataDefinition : Attribute + { + } +} diff --git a/Robust.Shared/Serialization/Manager/Attributes/DataFieldAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/DataFieldAttribute.cs new file mode 100644 index 000000000..69dd4b74b --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Attributes/DataFieldAttribute.cs @@ -0,0 +1,32 @@ +using System; +using JetBrains.Annotations; + +namespace Robust.Shared.Serialization.Manager.Attributes +{ + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + [MeansImplicitAssignment] + public class DataFieldAttribute : Attribute + { + public readonly string Tag; + public readonly int Priority; + public readonly bool ReadOnly; + + /// + /// Whether or not this field being mapped is required for the component to function. + /// This will not guarantee that the field is mapped when the program is run, + /// it is meant to be used as metadata information. + /// + public readonly bool Required; + + public readonly bool ServerOnly; + + public DataFieldAttribute([NotNull] string tag, bool readOnly = false, int priority = 1, bool required = false, bool serverOnly = false) + { + Tag = tag; + Priority = priority; + ReadOnly = readOnly; + Required = required; + ServerOnly = serverOnly; + } + } +} diff --git a/Robust.Shared/Serialization/Manager/Attributes/DataFieldWithConstantAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/DataFieldWithConstantAttribute.cs new file mode 100644 index 000000000..609c3b03f --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Attributes/DataFieldWithConstantAttribute.cs @@ -0,0 +1,16 @@ +using System; +using JetBrains.Annotations; + +namespace Robust.Shared.Serialization.Manager.Attributes +{ + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + [MeansImplicitAssignment] + public class DataFieldWithConstantAttribute : DataFieldAttribute + { + public readonly Type ConstantTag; + public DataFieldWithConstantAttribute([NotNull] string tag, Type constantTag, bool readOnly = false, int priority = 1, bool required = false, bool serverOnly = false) : base(tag, readOnly, priority, required, serverOnly) + { + ConstantTag = constantTag; + } + } +} diff --git a/Robust.Shared/Serialization/Manager/Attributes/DataFieldWithFlagAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/DataFieldWithFlagAttribute.cs new file mode 100644 index 000000000..33591167c --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Attributes/DataFieldWithFlagAttribute.cs @@ -0,0 +1,16 @@ +using System; +using JetBrains.Annotations; + +namespace Robust.Shared.Serialization.Manager.Attributes +{ + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + [MeansImplicitAssignment] + public class DataFieldWithFlagAttribute : DataFieldAttribute + { + public readonly Type FlagTag; + public DataFieldWithFlagAttribute([NotNull] string tag, Type flagTag, bool readOnly = false, int priority = 1, bool required = false, bool serverOnly = false) : base(tag, readOnly, priority, required, serverOnly) + { + FlagTag = flagTag; + } + } +} diff --git a/Robust.Shared/Serialization/Manager/Attributes/ImplicitDataDefinitionForInheritorsAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/ImplicitDataDefinitionForInheritorsAttribute.cs new file mode 100644 index 000000000..caae46356 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Attributes/ImplicitDataDefinitionForInheritorsAttribute.cs @@ -0,0 +1,7 @@ +using System; + +namespace Robust.Shared.Serialization.Manager.Attributes +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)] + public class ImplicitDataDefinitionForInheritorsAttribute : Attribute { } +} diff --git a/Robust.Shared/Serialization/Manager/Attributes/MeansDataDefinition.cs b/Robust.Shared/Serialization/Manager/Attributes/MeansDataDefinition.cs new file mode 100644 index 000000000..debc227d7 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Attributes/MeansDataDefinition.cs @@ -0,0 +1,9 @@ +using System; +using JetBrains.Annotations; + +namespace Robust.Shared.Serialization.Manager.Attributes +{ + [BaseTypeRequired(typeof(Attribute))] + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public class MeansDataDefinition : Attribute { } +} diff --git a/Robust.Shared/Serialization/Manager/Attributes/NeverPushInheritanceAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/NeverPushInheritanceAttribute.cs new file mode 100644 index 000000000..bc87649ee --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Attributes/NeverPushInheritanceAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace Robust.Shared.Serialization.Manager.Attributes +{ + //todo paul find a way to constrain this to datafields only & make exclusive w/ alwayspush + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + + public class NeverPushInheritanceAttribute : Attribute + { + + } +} diff --git a/Robust.Shared/Serialization/Manager/Attributes/TypeSerializerAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/TypeSerializerAttribute.cs new file mode 100644 index 000000000..5be585dbc --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Attributes/TypeSerializerAttribute.cs @@ -0,0 +1,9 @@ +using System; +using JetBrains.Annotations; + +namespace Robust.Shared.Serialization.Manager.Attributes +{ + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + [MeansImplicitUse] + public class TypeSerializerAttribute : Attribute{} +} diff --git a/Robust.Shared/Serialization/Manager/IPopulateDefaultValues.cs b/Robust.Shared/Serialization/Manager/IPopulateDefaultValues.cs new file mode 100644 index 000000000..99fb52eb6 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/IPopulateDefaultValues.cs @@ -0,0 +1,7 @@ +namespace Robust.Shared.Serialization.Manager +{ + public interface IPopulateDefaultValues + { + void PopulateDefaultValues(); + } +} diff --git a/Robust.Shared/Serialization/Manager/ISerializationContext.cs b/Robust.Shared/Serialization/Manager/ISerializationContext.cs new file mode 100644 index 000000000..5008f998c --- /dev/null +++ b/Robust.Shared/Serialization/Manager/ISerializationContext.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace Robust.Shared.Serialization.Manager +{ + //todo paul make this actually not kanser to use holy moly (& allow generics) + public interface ISerializationContext + { + Dictionary<(Type, Type), object> TypeReaders { get; } + Dictionary TypeWriters { get; } + Dictionary TypeCopiers { get; } + } +} diff --git a/Robust.Shared/Serialization/Manager/ISerializationManager.cs b/Robust.Shared/Serialization/Manager/ISerializationManager.cs new file mode 100644 index 000000000..eba8c0ad8 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/ISerializationManager.cs @@ -0,0 +1,265 @@ +using System; +using JetBrains.Annotations; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; + +namespace Robust.Shared.Serialization.Manager +{ + public interface ISerializationManager + { + #region Serialization + + /// + /// Initializes the serialization manager. + /// + void Initialize(); + + /// + /// Checks if a type has a data definition defined for it. + /// + /// The type to check for. + /// True if it does, false otherwise. + bool HasDataDefinition(Type type); + + /// + /// Validates that a node has all the properties required by a certain type with its serializer. + /// + /// The type to check for. + /// The node to check. + /// The context to use, if any. + /// + /// A node with whether or not is valid and which of its fields + /// are invalid, if any. + /// + ValidationNode ValidateNode(Type type, DataNode node, ISerializationContext? context = null); + + /// + /// Validates that a node has all the properties required by a certain type with its serializer. + /// + /// The node to check. + /// The context to use, if any. + /// + /// A node with whether or not is valid and which of its fields + /// are invalid, if any. + /// + ValidationNode ValidateNode(DataNode node, ISerializationContext? context = null); + + /// + /// Creates a deserialization result from a generic type and its fields, + /// populating the object. + /// + /// The fields to use for deserialization. + /// Whether or not to skip running + /// The type to populate. + /// A result with the populated type. + DeserializationResult CreateDataDefinition(DeserializedFieldEntry[] fields, bool skipHook = false) where T : notnull, new(); + + /// + /// Creates a deserialization result from a generic type and its definition, + /// populating the object. + /// + /// The object to populate. + /// The data to use for deserialization. + /// Whether or not to skip running + /// The type of to populate. + /// A result with the populated object. + DeserializationResult PopulateDataDefinition(T obj, DeserializedDefinition definition, bool skipHook = false) where T : notnull, new(); + + /// + /// Creates a deserialization result from an object and its definition, + /// populating the object. + /// + /// The object to populate. + /// The data to use for deserialization. + /// Whether or not to skip running + /// A result with the populated object. + DeserializationResult PopulateDataDefinition(object obj, IDeserializedDefinition definition, bool skipHook = false); + + /// + /// Deserializes a node into an object, populating it. + /// + /// The type of object to populate. + /// The node to deserialize. + /// The context to use, if any. + /// Whether or not to skip running + /// A result with the deserialized object. + DeserializationResult Read(Type type, DataNode node, ISerializationContext? context = null, bool skipHook = false); + + /// + /// Deserializes a node into an object, populating it. + /// + /// The type of object to deserialize into. + /// The node to deserialize. + /// The context to use, if any. + /// Whether or not to skip running + /// The deserialized object or null. + public object? ReadValue(Type type, DataNode node, ISerializationContext? context = null, bool skipHook = false); + + /// + /// Deserializes a node into an object of the given , + /// directly casting it to the given generic type . + /// + /// The type of object to deserialize into. + /// The node to deserialize. + /// The context to use, if any. + /// Whether or not to skip running + /// The generic type to cast the resulting object to. + /// The deserialized casted object, or null. + T? ReadValueCast(Type type, DataNode node, ISerializationContext? context = null, bool skipHook = false); + + /// + /// Deserializes a node into a populated object of the given generic type + /// + /// The node to deserialize. + /// The context to use, if any. + /// Whether or not to skip running + /// The type of object to create and populate. + /// The deserialized object, or null. + T? ReadValue(DataNode node, ISerializationContext? context = null, bool skipHook = false); + + /// + /// Serializes a value into a node. + /// + /// The value to serialize. + /// + /// Whether or not to always write the given values into the resulting node, + /// even if they are the default. + /// + /// The context to use, if any. + /// The type to serialize. + /// A serialized datanode created from the given . + DataNode WriteValue(T value, bool alwaysWrite = false, ISerializationContext? context = null) + where T : notnull; + + /// + /// Serializes a value into a node. + /// + /// The type of the to serialize as. + /// The value to serialize. + /// + /// Whether or not to always write the given values into the resulting node, + /// even if they are the default. + /// + /// The context to use, if any. + /// + /// A serialized datanode created from the given + /// of type . + /// + DataNode WriteValue(Type type, object? value, bool alwaysWrite = false, ISerializationContext? context = null); + + /// + /// Copies the values of one object into another. + /// This does not guarantee that the object passed as + /// is actually mutated. + /// + /// The object to copy values from. + /// The object to copy values into. + /// The context to use, if any. + /// Whether or not to skip running + /// + /// The object with the copied values. + /// This object is not necessarily the same instance as the one passed + /// as . + /// + [MustUseReturnValue] + object? Copy(object? source, object? target, ISerializationContext? context = null, bool skipHook = false); + + /// + /// Copies the values of one object into another. + /// This does not guarantee that the object passed as + /// is actually mutated. + /// + /// The object to copy values from. + /// The object to copy values into. + /// The context to use, if any. + /// Whether or not to skip running + /// The type of the objects to copy from and into. + /// + /// The object with the copied values. + /// This object is not necessarily the same instance as the one passed + /// as . + /// + [MustUseReturnValue] + T? Copy(T? source, T? target, ISerializationContext? context = null, bool skipHook = false); + + /// + /// Creates a copy of the given object. + /// + /// The object to copy. + /// The context to use, if any. + /// Whether or not to skip running + /// A copy of the given object. + object? CreateCopy(object? source, ISerializationContext? context = null, bool skipHook = false); + + /// + /// Creates a copy of the given object. + /// + /// The object to copy. + /// The context to use, if any. + /// Whether or not to skip running + /// The type of the object to copy. + /// A copy of the given object. + T? CreateCopy(T? source, ISerializationContext? context = null, bool skipHook = false); + + #endregion + + #region Flags And Constants + + /// + /// Deserializes a node into an enum flag value of the given type. + /// + /// The type of enum to deserialize into. + /// The node to deserialize. + /// The deserialized enum flags. + int ReadFlag(Type tagType, DataNode node); + + /// + /// Validates that a node can be deserialized into the specified flagtype. + /// + /// The tagtype for used to retrieve the flagtype. + /// The node to check. + /// + /// A node with whether or not is valid and which of its fields + /// are invalid, if any. + /// + ValidationNode ValidateFlag(Type tagType, DataNode node); + + /// + /// Deserializes a node into an enum value of the given type. + /// + /// The type of enum to deserialize into. + /// The node to deserialize. + /// The deserialized enum. + int ReadConstant(Type tagType, DataNode node); + + /// + /// Validates that a node can be deserialized into the specified constanttype. + /// + /// The tagtype for used to retrieve the constanttype. + /// The node to check. + /// + /// A node with whether or not is valid and which of its fields + /// are invalid, if any. + /// + ValidationNode ValidateConstant(Type tagType, DataNode node); + + /// + /// Serializes an enum flag into a node. + /// + /// The type of enum to serialize. + /// The enum flags value to serialize. + /// The serialized node. + DataNode WriteFlag(Type tagType, int flag); + + /// + /// Serializes an enum into a node. + /// + /// The type of enum to serialize. + /// The enum value to serialize. + /// The serialized node. + DataNode WriteConstant(Type tagType, int constant); + + #endregion + } +} diff --git a/Robust.Shared/Serialization/Manager/InvalidNodeTypeException.cs b/Robust.Shared/Serialization/Manager/InvalidNodeTypeException.cs new file mode 100644 index 000000000..183feda7a --- /dev/null +++ b/Robust.Shared/Serialization/Manager/InvalidNodeTypeException.cs @@ -0,0 +1,15 @@ +using System; + +namespace Robust.Shared.Serialization.Manager +{ + public class InvalidNodeTypeException : Exception + { + public InvalidNodeTypeException() + { + } + + public InvalidNodeTypeException(string? message) : base(message) + { + } + } +} diff --git a/Robust.Shared/Serialization/Manager/RequiredDataFieldNotProvidedException.cs b/Robust.Shared/Serialization/Manager/RequiredDataFieldNotProvidedException.cs new file mode 100644 index 000000000..a533ac863 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/RequiredDataFieldNotProvidedException.cs @@ -0,0 +1,6 @@ +using System; + +namespace Robust.Shared.Serialization.Manager +{ + public class RequiredDataFieldNotProvidedException : Exception { } +} diff --git a/Robust.Shared/Serialization/Manager/Result/DeserializationResult.cs b/Robust.Shared/Serialization/Manager/Result/DeserializationResult.cs new file mode 100644 index 000000000..bac6f754a --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Result/DeserializationResult.cs @@ -0,0 +1,44 @@ +using System; +using Robust.Shared.IoC; + +namespace Robust.Shared.Serialization.Manager.Result +{ + public abstract class DeserializationResult + { + public abstract object? RawValue { get; } + + public abstract DeserializationResult PushInheritanceFrom(DeserializationResult source); + + public abstract DeserializationResult Copy(); + + public abstract void CallAfterDeserializationHook(); + + public static DeserializationResult Value(T value) where T : notnull + { + var type = typeof(DeserializedValue<>).MakeGenericType(value.GetType()); + return (DeserializationResult) Activator.CreateInstance(type, value)!; + } + + public static DeserializationResult Definition(object value, DeserializedFieldEntry[] mappings) + { + //if (!IoCManager.Resolve().HasDataDefinition(value.GetType())) + // throw new ArgumentException("Provided value was not a data definition", nameof(value)); + + //todo validate mappings array count + //unless... + var type = typeof(DeserializedDefinition<>).MakeGenericType(value.GetType()); + return (DeserializationResult) Activator.CreateInstance(type, value, mappings)!; + } + + public T Cast() where T : DeserializationResult + { + if (this is T value) return value; + throw new InvalidDeserializedResultTypeException(GetType()); + } + } + + public abstract class DeserializationResult : DeserializationResult + { + public abstract T Value { get; } + } +} diff --git a/Robust.Shared/Serialization/Manager/Result/DeserializedArray.cs b/Robust.Shared/Serialization/Manager/Result/DeserializedArray.cs new file mode 100644 index 000000000..ea443d4ac --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Result/DeserializedArray.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; + +namespace Robust.Shared.Serialization.Manager.Result +{ + public class DeserializedArray : DeserializationResult + { + public DeserializedArray(Array array, IEnumerable mappings) + { + Value = array; + Mappings = mappings; + } + + public Array Value { get; } + + public IEnumerable Mappings { get; } + + public override object? RawValue => Value; + + public override DeserializationResult PushInheritanceFrom(DeserializationResult source) + { + var sourceCollection = source.Cast(); + var valueList = (Array) Activator.CreateInstance(Value.GetType(), Value.Length)!; + var resList = new List(); + + var i = 0; + foreach (var oldRes in sourceCollection.Mappings) + { + var newRes = oldRes.Copy(); + valueList.SetValue(newRes.RawValue, i); + resList.Add(newRes); + i++; + } + + i = 0; + foreach (var oldRes in Mappings) + { + var newRes = oldRes.Copy(); + valueList.SetValue(newRes.RawValue, i); + resList.Add(newRes); + i++; + } + + return new DeserializedArray(valueList, resList); + } + + public override DeserializationResult Copy() + { + var valueList = (Array) Activator.CreateInstance(Value.GetType(), Value.Length)!; + var resList = new List(); + + var i = 0; + foreach (var oldRes in Mappings) + { + var newRes = oldRes.Copy(); + valueList.SetValue(newRes.RawValue, i); + resList.Add(newRes); + i++; + } + + return new DeserializedArray(valueList, resList); + } + + public override void CallAfterDeserializationHook() + { + foreach (var elem in Mappings) + { + elem.CallAfterDeserializationHook(); + } + } + } +} diff --git a/Robust.Shared/Serialization/Manager/Result/DeserializedCollection.cs b/Robust.Shared/Serialization/Manager/Result/DeserializedCollection.cs new file mode 100644 index 000000000..aeb97be9d --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Result/DeserializedCollection.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; + +namespace Robust.Shared.Serialization.Manager.Result +{ + public class DeserializedCollection : DeserializationResult where TCollection : IReadOnlyCollection + { + public delegate TCollection Create(List elements); + + public DeserializedCollection( + TCollection value, + IEnumerable mappings, + Create createDelegate) + { + Value = value; + Mappings = mappings; + CreateDelegate = createDelegate; + } + + public override TCollection Value { get; } + + public IEnumerable Mappings { get; } + + public override object? RawValue => Value; + + private Create CreateDelegate { get; } + + public override DeserializationResult PushInheritanceFrom(DeserializationResult source) + { + var sourceCollection = source.Cast>(); + var valueList = new List(); + var resList = new List(); + + foreach (var oldRes in sourceCollection.Mappings) + { + var newRes = oldRes.Copy().Cast>(); + valueList.Add(newRes.Value!); + resList.Add(newRes); + } + + foreach (var oldRes in Mappings) + { + var newRes = oldRes.Copy().Cast>(); + valueList.Add(newRes.Value!); + resList.Add(newRes); + } + + return new DeserializedCollection(CreateDelegate(valueList), resList, CreateDelegate); + } + + public override DeserializationResult Copy() + { + var valueList = new List(); + var resList = new List(); + + foreach (var oldRes in Mappings) + { + var newRes = oldRes.Copy(); + valueList.Add((TElement) newRes.RawValue!); + resList.Add(newRes); + } + + return new DeserializedCollection(CreateDelegate(valueList), resList, CreateDelegate); + } + + public override void CallAfterDeserializationHook() + { + foreach (var val in Mappings) + { + val.CallAfterDeserializationHook(); + } + } + } +} diff --git a/Robust.Shared/Serialization/Manager/Result/DeserializedComponentRegistry.cs b/Robust.Shared/Serialization/Manager/Result/DeserializedComponentRegistry.cs new file mode 100644 index 000000000..a94550b11 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Result/DeserializedComponentRegistry.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Utility; +using static Robust.Shared.Prototypes.EntityPrototype; + +namespace Robust.Shared.Serialization.Manager.Result +{ + public class DeserializedComponentRegistry : DeserializationResult + { + public DeserializedComponentRegistry( + ComponentRegistry value, + IReadOnlyDictionary mappings) + { + Value = value; + Mappings = mappings; + } + + public override ComponentRegistry Value { get; } + + public IReadOnlyDictionary Mappings { get; } + + public override object? RawValue => Value; + + public override DeserializationResult PushInheritanceFrom(DeserializationResult source) + { + var componentFactory = IoCManager.Resolve(); + var sourceRes = source.Cast(); + var mappingDict = Mappings.ToDictionary(p => p.Key.Copy(), p => p.Value.Copy()); + + foreach (var (keyRes, valRes) in sourceRes.Mappings) + { + var newKeyRes = keyRes.Copy(); + var newValueRes = valRes.Copy(); + + if (mappingDict.Any(p => + { + var k1 = (string) newKeyRes.RawValue!; + var k2 = (string) p.Key.RawValue!; + + if (k1 == k2) + { + return false; + } + + var registration1 = componentFactory.GetRegistration(k1!); + var registration2 = componentFactory.GetRegistration(k2!); + + foreach (var reference in registration1.References) + { + if (registration2.References.Contains(reference)) + { + return true; + } + } + + return false; + })) + { + continue; + } + + var oldEntry = mappingDict.FirstOrNull(p => Equals(p.Key.RawValue, newKeyRes.RawValue)); + + if (oldEntry.HasValue) + { + newKeyRes = oldEntry.Value.Key.PushInheritanceFrom(newKeyRes); + newValueRes = oldEntry.Value.Value.PushInheritanceFrom(newValueRes); + mappingDict.Remove(oldEntry.Value.Key); + } + + mappingDict.Add(newKeyRes, newValueRes); + } + + var valueDict = new ComponentRegistry(); + foreach (var (key, val) in mappingDict) + { + valueDict.Add((string) key.RawValue!, (IComponent) val.RawValue!); + } + + return new DeserializedComponentRegistry(valueDict, mappingDict); + } + + public override DeserializationResult Copy() + { + var registry = new ComponentRegistry(); + var mappingDict = new Dictionary(); + + foreach (var (keyRes, valRes) in Mappings) + { + var newKeyRes = keyRes.Copy(); + var newValueRes = valRes.Copy(); + + registry.Add((string) newKeyRes.RawValue!, (IComponent) newValueRes.RawValue!); + mappingDict.Add(newKeyRes, newValueRes); + } + + return new DeserializedComponentRegistry(registry, mappingDict); + } + + public override void CallAfterDeserializationHook() + { + foreach (var (_, comp) in Mappings) + { + comp.CallAfterDeserializationHook(); + } + } + } +} diff --git a/Robust.Shared/Serialization/Manager/Result/DeserializedDefinition.cs b/Robust.Shared/Serialization/Manager/Result/DeserializedDefinition.cs new file mode 100644 index 000000000..ed611f01b --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Result/DeserializedDefinition.cs @@ -0,0 +1,57 @@ +using System; +using Robust.Shared.IoC; + +namespace Robust.Shared.Serialization.Manager.Result +{ + public class DeserializedDefinition : DeserializationResult, IDeserializedDefinition where T : notnull, new() + { + public DeserializedDefinition(T value, DeserializedFieldEntry[] mapping) + { + Value = value; + Mapping = mapping; + } + + public override T Value { get; } + + public DeserializedFieldEntry[] Mapping { get; } + + public override object RawValue => Value; + + public override DeserializationResult PushInheritanceFrom(DeserializationResult source) + { + var dataDef = source.Cast>(); + if (dataDef.Mapping.Length != Mapping.Length) + throw new ArgumentException($"Mapping length mismatch in PushInheritanceFrom ({typeof(T)})"); + + var newMapping = new DeserializedFieldEntry[Mapping.Length]; + + for (var i = 0; i < dataDef.Mapping.Length; i++) + { + newMapping[i] = Mapping[i].PushInheritanceFrom(dataDef.Mapping[i]); + } + + return IoCManager.Resolve().CreateDataDefinition(newMapping, true); + } + + public override DeserializationResult Copy() + { + var newMapping = new DeserializedFieldEntry[Mapping.Length]; + + for (var i = 0; i < Mapping.Length; i++) + { + newMapping[i] = Mapping[i].Copy(); + } + + return IoCManager.Resolve().CreateDataDefinition(newMapping, true); + } + + public override void CallAfterDeserializationHook() + { + foreach (var fieldEntry in Mapping) + { + fieldEntry.Result?.CallAfterDeserializationHook(); + } + if(Value is ISerializationHooks hooks) hooks.AfterDeserialization(); + } + } +} diff --git a/Robust.Shared/Serialization/Manager/Result/DeserializedDictionary.cs b/Robust.Shared/Serialization/Manager/Result/DeserializedDictionary.cs new file mode 100644 index 000000000..303366981 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Result/DeserializedDictionary.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; + +namespace Robust.Shared.Serialization.Manager.Result +{ + public class DeserializedDictionary : + DeserializationResult + where TKey : notnull + where TDict : IReadOnlyDictionary + { + public delegate TDict Create(Dictionary elements); + + public DeserializedDictionary( + TDict value, + IReadOnlyDictionary mappings, + Create createDelegate) + { + Value = value; + Mappings = mappings; + CreateDelegate = createDelegate; + } + + public override TDict Value { get; } + + public IReadOnlyDictionary Mappings { get; } + + public Create CreateDelegate { get; } + + public override object RawValue => Value; + + public override DeserializationResult PushInheritanceFrom(DeserializationResult source) + { + var sourceRes = source.Cast>(); + var valueDict = new Dictionary(); + var mappingDict = new Dictionary(); + + foreach (var (keyRes, valRes) in sourceRes.Mappings) + { + var newKeyRes = keyRes.Copy(); + var newValueRes = valRes.Copy(); + + valueDict.Add((TKey)newKeyRes.RawValue!, (TValue)newValueRes.RawValue!); + mappingDict.Add(newKeyRes, newValueRes); + } + + foreach (var (keyRes, valRes) in Mappings) + { + var newKeyRes = keyRes.Copy(); + var newValueRes = valRes.Copy(); + + valueDict.Add((TKey) newKeyRes.RawValue!, (TValue)newValueRes.RawValue!); + mappingDict.Add(newKeyRes, newValueRes); + } + + return new DeserializedDictionary(CreateDelegate(valueDict), mappingDict, CreateDelegate); + } + + public override DeserializationResult Copy() + { + var valueDict = new Dictionary(); + var mappingDict = new Dictionary(); + + foreach (var (keyRes, valRes) in Mappings) + { + var newKeyRes = keyRes.Copy(); + var newValueRes = valRes.Copy(); + + valueDict.Add((TKey)newKeyRes.RawValue!, (TValue)newValueRes.RawValue!); + mappingDict.Add(newKeyRes, newValueRes); + } + + return new DeserializedDictionary(CreateDelegate(valueDict), mappingDict, CreateDelegate); + } + + public override void CallAfterDeserializationHook() + { + foreach (var (key, val) in Mappings) + { + key.CallAfterDeserializationHook(); + val.CallAfterDeserializationHook(); + } + } + } +} diff --git a/Robust.Shared/Serialization/Manager/Result/DeserializedFieldEntry.cs b/Robust.Shared/Serialization/Manager/Result/DeserializedFieldEntry.cs new file mode 100644 index 000000000..6537aa8d9 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Result/DeserializedFieldEntry.cs @@ -0,0 +1,48 @@ +using System; + +namespace Robust.Shared.Serialization.Manager.Result +{ + public class DeserializedFieldEntry + { + public DeserializedFieldEntry(bool mapped, SerializationDataDefinition.InheritanceBehaviour inheritanceBehaviour, DeserializationResult? result = null) + { + Mapped = mapped; + Result = result; + InheritanceBehaviour = inheritanceBehaviour; + } + + public bool Mapped { get; } + public SerializationDataDefinition.InheritanceBehaviour InheritanceBehaviour { get; } + + public DeserializationResult? Result { get; } + + public DeserializedFieldEntry PushInheritanceFrom(DeserializedFieldEntry fieldEntry) + { + if(Mapped) + { + if (InheritanceBehaviour == SerializationDataDefinition.InheritanceBehaviour.Always) + { + if (Result != null) + { + return fieldEntry.Result != null + ? new(Mapped, InheritanceBehaviour, Result.PushInheritanceFrom(fieldEntry.Result)) + : Copy(); + } + else + { + return fieldEntry.Copy(); + } + } + + return Copy(); + } + + return InheritanceBehaviour == SerializationDataDefinition.InheritanceBehaviour.Never ? Copy() : fieldEntry.Copy(); + } + + public DeserializedFieldEntry Copy() + { + return new(Mapped, InheritanceBehaviour, Result?.Copy()); + } + } +} diff --git a/Robust.Shared/Serialization/Manager/Result/DeserializedValue.cs b/Robust.Shared/Serialization/Manager/Result/DeserializedValue.cs new file mode 100644 index 000000000..0e628dcae --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Result/DeserializedValue.cs @@ -0,0 +1,29 @@ +namespace Robust.Shared.Serialization.Manager.Result +{ + public class DeserializedValue : DeserializationResult + { + public DeserializedValue(T value) + { + Value = value; + } + + public override T Value { get; } + + public override object? RawValue => Value; + + public override DeserializationResult PushInheritanceFrom(DeserializationResult source) + { + return source.Copy().Cast>(); + } + + public override DeserializationResult Copy() + { + return new DeserializedValue(Value); + } + + public override void CallAfterDeserializationHook() + { + if(Value is ISerializationHooks hooks) hooks.AfterDeserialization(); + } + } +} diff --git a/Robust.Shared/Serialization/Manager/Result/IDeserializedDefinition.cs b/Robust.Shared/Serialization/Manager/Result/IDeserializedDefinition.cs new file mode 100644 index 000000000..e2082ab18 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Result/IDeserializedDefinition.cs @@ -0,0 +1,7 @@ +namespace Robust.Shared.Serialization.Manager.Result +{ + public interface IDeserializedDefinition + { + DeserializedFieldEntry[] Mapping { get; } + } +} diff --git a/Robust.Shared/Serialization/Manager/Result/InvalidDeserializedResultTypeException.cs b/Robust.Shared/Serialization/Manager/Result/InvalidDeserializedResultTypeException.cs new file mode 100644 index 000000000..09226a8e0 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Result/InvalidDeserializedResultTypeException.cs @@ -0,0 +1,33 @@ +using System; +using System.Runtime.Serialization; +using JetBrains.Annotations; + +namespace Robust.Shared.Serialization.Manager.Result +{ + public class InvalidDeserializedResultTypeException : Exception + { + public readonly Type ReceivedType; + + public override string Message => $"Invalid Type {ReceivedType} received. Expected {typeof(TExpected)}"; + + public InvalidDeserializedResultTypeException(Type receivedType) + { + ReceivedType = receivedType; + } + + protected InvalidDeserializedResultTypeException([NotNull] SerializationInfo info, StreamingContext context, Type receivedType) : base(info, context) + { + ReceivedType = receivedType; + } + + public InvalidDeserializedResultTypeException([CanBeNull] string? message, Type receivedType) : base(message) + { + ReceivedType = receivedType; + } + + public InvalidDeserializedResultTypeException([CanBeNull] string? message, [CanBeNull] Exception? innerException, Type receivedType) : base(message, innerException) + { + ReceivedType = receivedType; + } + } +} diff --git a/Robust.Shared/Serialization/Manager/SerializationDataDefinition.cs b/Robust.Shared/Serialization/Manager/SerializationDataDefinition.cs new file mode 100644 index 000000000..3386ac134 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/SerializationDataDefinition.cs @@ -0,0 +1,408 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Network; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Utility; + +namespace Robust.Shared.Serialization.Manager +{ + public class SerializationDataDefinition + { + private delegate DeserializedFieldEntry[] DeserializeDelegate(MappingDataNode mappingDataNode, + ISerializationManager serializationManager, ISerializationContext? context, bool skipHook); + + private delegate DeserializationResult PopulateDelegateSignature(object target, DeserializedFieldEntry[] deserializationResults, object?[] defaultValues); + + private delegate MappingDataNode SerializeDelegateSignature(object obj, ISerializationManager serializationManager, + ISerializationContext? context, bool alwaysWrite, object?[] defaultValues); + + private delegate object CopyDelegateSignature(object source, object target, + ISerializationManager serializationManager, ISerializationContext? context); + + public readonly Type Type; + + private readonly string[] _duplicates; + private readonly FieldDefinition[] _baseFieldDefinitions; + private readonly object?[] _defaultValues; + + private readonly DeserializeDelegate _deserializeDelegate; + + private readonly PopulateDelegateSignature _populateDelegate; + + private readonly SerializeDelegateSignature _serializeDelegate; + + private readonly CopyDelegateSignature _copyDelegate; + + public DeserializationResult InvokePopulateDelegate(object target, DeserializedFieldEntry[] fields) => + _populateDelegate(target, fields, _defaultValues); + + public DeserializationResult InvokePopulateDelegate(object target, MappingDataNode mappingDataNode, ISerializationManager serializationManager, + ISerializationContext? context, bool skipHook) + { + var fields = _deserializeDelegate(mappingDataNode, serializationManager, context, skipHook); + return _populateDelegate(target, fields, _defaultValues); + } + + public MappingDataNode InvokeSerializeDelegate(object obj, ISerializationManager serializationManager, ISerializationContext? context, bool alwaysWrite) => + _serializeDelegate(obj, serializationManager, context, alwaysWrite, _defaultValues); + + public object InvokeCopyDelegate(object source, object target, ISerializationManager serializationManager, ISerializationContext? context) => + _copyDelegate(source, target, serializationManager, context); + + public bool CanCallWith(object obj) => Type.IsInstanceOfType(obj); + + public SerializationDataDefinition(Type type) + { + Type = type; + var dummyObj = Activator.CreateInstance(type)!; + + var fieldDefs = new List(); + + foreach (var abstractFieldInfo in type.GetAllPropertiesAndFields()) + { + var attr = abstractFieldInfo.GetCustomAttribute(); + + if (attr == null) continue; + + if (abstractFieldInfo is SpecificPropertyInfo propertyInfo) + { + // We only want the most overriden instance of a property for the type we are working with + if (!propertyInfo.IsMostOverridden(type)) + { + continue; + } + + if (propertyInfo.PropertyInfo.GetMethod == null) + { + Logger.ErrorS(SerializationManager.LogCategory, $"Property {propertyInfo} is annotated with DataFieldAttribute but has no getter"); + continue; + } + else if (!attr.ReadOnly && propertyInfo.PropertyInfo.SetMethod == null) + { + Logger.ErrorS(SerializationManager.LogCategory, $"Property {propertyInfo} is annotated with DataFieldAttribute as non-readonly but has no setter"); + continue; + } + } + + var inheritanceBehaviour = InheritanceBehaviour.Default; + if (abstractFieldInfo.GetCustomAttribute() != null) + { + inheritanceBehaviour = InheritanceBehaviour.Always; + } + else if (abstractFieldInfo.GetCustomAttribute() != null) + { + inheritanceBehaviour = InheritanceBehaviour.Never; + } + + fieldDefs.Add(new FieldDefinition(attr, abstractFieldInfo.GetValue(dummyObj), abstractFieldInfo, inheritanceBehaviour)); + } + + _duplicates = fieldDefs + .Where(f => + fieldDefs.Count(df => df.Attribute.Tag == f.Attribute.Tag) > 1) + .Select(f => f.Attribute.Tag) + .Distinct() + .ToArray(); + + var fields = fieldDefs; + fields.Sort((a, b) => b.Attribute.Priority.CompareTo(a.Attribute.Priority)); + _baseFieldDefinitions = fields.ToArray(); + _defaultValues = fieldDefs.Select(f => f.DefaultValue).ToArray(); + + _deserializeDelegate = EmitDeserializationDelegate(); + _populateDelegate = EmitPopulateDelegate(); + _serializeDelegate = EmitSerializeDelegate(); + _copyDelegate = EmitCopyDelegate(); + } + + public int DataFieldCount => _baseFieldDefinitions.Length; + + public bool TryGetDuplicates([NotNullWhen(true)] out string[] duplicates) + { + duplicates = _duplicates; + return duplicates.Length > 0; + } + + public ValidationNode Validate(ISerializationManager serializationManager, MappingDataNode node, ISerializationContext? context) + { + var validatedMapping = new Dictionary(); + + foreach (var (key, val) in node.Children) + { + if (key is not ValueDataNode valueDataNode) + { + validatedMapping.Add(new ErrorNode(key, "Key not ValueDataNode."), new InconclusiveNode(val)); + continue; + } + + var field = _baseFieldDefinitions.FirstOrDefault(f => f.Attribute.Tag == valueDataNode.Value); + if (field == null) + { + var error = new ErrorNode( + key, + $"Field \"{valueDataNode.Value}\" not found in \"{Type}\".", + false); + + validatedMapping.Add(error, new InconclusiveNode(val)); + continue; + } + + var keyValidated = serializationManager.ValidateNode(typeof(string), key, context); + var valValidated = field.Attribute switch + { + DataFieldWithFlagAttribute flagAttribute => serializationManager.ValidateFlag(flagAttribute.FlagTag, + val), + DataFieldWithConstantAttribute constantAttribute => serializationManager.ValidateConstant( + constantAttribute.ConstantTag, val), + _ => serializationManager.ValidateNode(field.FieldType, val, context) + }; + + validatedMapping.Add(keyValidated, valValidated); + } + + return new ValidatedMappingNode(validatedMapping); + } + + private DeserializeDelegate EmitDeserializationDelegate() + { + DeserializedFieldEntry[] DeserializationDelegate(MappingDataNode mappingDataNode, + ISerializationManager serializationManager, ISerializationContext? serializationContext, bool skipHook) + { + var mappedInfo = new DeserializedFieldEntry[_baseFieldDefinitions.Length]; + + for (var i = 0; i < _baseFieldDefinitions.Length; i++) + { + var fieldDefinition = _baseFieldDefinitions[i]; + + if (fieldDefinition.Attribute.ServerOnly && !IoCManager.Resolve().IsServer) + { + mappedInfo[i] = new DeserializedFieldEntry(false, fieldDefinition.InheritanceBehaviour); + continue; + } + + var mapped = mappingDataNode.HasNode(fieldDefinition.Attribute.Tag); + + if (!mapped) + { + mappedInfo[i] = new DeserializedFieldEntry(mapped, fieldDefinition.InheritanceBehaviour); + continue; + } + + DeserializationResult? result; + + switch (fieldDefinition.Attribute) + { + case DataFieldWithConstantAttribute constantAttribute: + { + if (fieldDefinition.FieldType.EnsureNotNullableType() != typeof(int)) + throw new InvalidOperationException(); + + var type = constantAttribute.ConstantTag; + var node = mappingDataNode.GetNode(fieldDefinition.Attribute.Tag); + var constant = serializationManager.ReadConstant(type, node); + + result = new DeserializedValue(constant); + break; + } + case DataFieldWithFlagAttribute flagAttribute: + { + if (fieldDefinition.FieldType.EnsureNotNullableType() != typeof(int)) throw new InvalidOperationException(); + + var type = flagAttribute.FlagTag; + var node = mappingDataNode.GetNode(fieldDefinition.Attribute.Tag); + var flag = serializationManager.ReadFlag(type, node); + + result = new DeserializedValue(flag); + break; + } + default: + { + var type = fieldDefinition.FieldType; + var node = mappingDataNode.GetNode(fieldDefinition.Attribute.Tag); + result = serializationManager.Read(type, node, serializationContext, skipHook); + break; + } + } + + var entry = new DeserializedFieldEntry(mapped, fieldDefinition.InheritanceBehaviour, result); + mappedInfo[i] = entry; + } + + return mappedInfo; + } + + return DeserializationDelegate; + } + + // TODO PAUL SERV3: Turn this back into IL once it is fixed + private PopulateDelegateSignature EmitPopulateDelegate() + { + DeserializationResult PopulateDelegate( + object target, + DeserializedFieldEntry[] deserializedFields, + object?[] defaultValues) + { + for (var i = 0; i < _baseFieldDefinitions.Length; i++) + { + var res = deserializedFields[i]; + if (!res.Mapped) continue; + + var fieldDefinition = _baseFieldDefinitions[i]; + + var defValue = defaultValues[i]; + + if (Equals(res.Result?.RawValue, defValue)) + { + continue; + } + + fieldDefinition.FieldInfo.SetValue(target, res.Result?.RawValue); + } + + return DeserializationResult.Definition(target, deserializedFields); + } + + return PopulateDelegate; + } + + // TODO PAUL SERV3: Turn this back into IL once it is fixed + private SerializeDelegateSignature EmitSerializeDelegate() + { + MappingDataNode SerializeDelegate( + object obj, + ISerializationManager manager, + ISerializationContext? context, + bool alwaysWrite, + object?[] defaultValues) + { + var mapping = new MappingDataNode(); + + for (var i = _baseFieldDefinitions.Length - 1; i >= 0; i--) + { + var fieldDefinition = _baseFieldDefinitions[i]; + + if (fieldDefinition.Attribute.ReadOnly) + { + continue; + } + + if (fieldDefinition.Attribute.ServerOnly && + !IoCManager.Resolve().IsServer) + { + continue; + } + + var info = fieldDefinition.FieldInfo; + var value = info.GetValue(obj); + + if (value == null) + { + continue; + } + + if (!fieldDefinition.Attribute.Required && + !alwaysWrite && + Equals(value, defaultValues[i])) + { + continue; + } + + DataNode node; + + switch (fieldDefinition.Attribute) + { + case DataFieldWithConstantAttribute constantAttribute: + { + if (fieldDefinition.FieldType.EnsureNotNullableType() != typeof(int)) throw new InvalidOperationException(); + + var tag = constantAttribute.ConstantTag; + node = manager.WriteConstant(tag, (int) value); + + break; + } + case DataFieldWithFlagAttribute flagAttribute: + { + if (fieldDefinition.FieldType.EnsureNotNullableType() != typeof(int)) throw new InvalidOperationException(); + + var tag = flagAttribute.FlagTag; + node = manager.WriteFlag(tag, (int) value); + + break; + } + default: + { + var type = fieldDefinition.FieldType; + node = manager.WriteValue(type, value, alwaysWrite, context); + + break; + } + } + + mapping[fieldDefinition.Attribute.Tag] = node; + } + + return mapping; + } + + return SerializeDelegate; + } + + // TODO PAUL SERV3: Turn this back into IL once it is fixed + private CopyDelegateSignature EmitCopyDelegate() + { + object PopulateDelegate( + object source, + object target, + ISerializationManager manager, + ISerializationContext? context) + { + foreach (var field in _baseFieldDefinitions) + { + var info = field.FieldInfo; + var sourceValue = info.GetValue(source); + var targetValue = info.GetValue(target); + + var copy = manager.Copy(sourceValue, targetValue, context); + + info.SetValue(target, copy); + } + + return target; + } + + return PopulateDelegate; + } + + public class FieldDefinition + { + public readonly DataFieldAttribute Attribute; + public readonly object? DefaultValue; + public readonly AbstractFieldInfo FieldInfo; + public readonly InheritanceBehaviour InheritanceBehaviour; + + public FieldDefinition(DataFieldAttribute attr, object? defaultValue, AbstractFieldInfo fieldInfo, InheritanceBehaviour inheritanceBehaviour) + { + Attribute = attr; + DefaultValue = defaultValue; + FieldInfo = fieldInfo; + InheritanceBehaviour = inheritanceBehaviour; + } + + public Type FieldType => FieldInfo.FieldType; + } + + public enum InheritanceBehaviour : byte + { + Default, + Always, + Never + } + } +} diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.cs b/Robust.Shared/Serialization/Manager/SerializationManager.cs new file mode 100644 index 000000000..09d1b3b91 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/SerializationManager.cs @@ -0,0 +1,602 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using JetBrains.Annotations; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Reflection; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Utility; + +namespace Robust.Shared.Serialization.Manager +{ + public partial class SerializationManager : ISerializationManager + { + [Dependency] private readonly IReflectionManager _reflectionManager = default!; + + public const string LogCategory = "serialization"; + + private bool _initializing; + private bool _initialized; + + private readonly Dictionary _dataDefinitions = new(); + private readonly List _copyByRefRegistrations = new(); + + public IDependencyCollection DependencyCollection { get; private set; } = default!; + + public void Initialize() + { + if (_initializing) + { + throw new InvalidOperationException($"{nameof(SerializationManager)} is already being initialized."); + } + + if (_initialized) + { + throw new InvalidOperationException($"{nameof(SerializationManager)} has already been initialized."); + } + + _initializing = true; + + DependencyCollection = IoCManager.Instance ?? throw new NullReferenceException(); + + InitializeFlagsAndConstants(); + InitializeTypeSerializers(); + + //var registrations = _reflectionManager.FindTypesWithAttribute().ToHashSet(); + var registrations = new HashSet(); + + foreach (var baseType in _reflectionManager.FindTypesWithAttribute()) + { + if (!baseType.IsAbstract && !baseType.IsInterface && !baseType.IsGenericTypeDefinition) registrations.Add(baseType); + foreach (var child in _reflectionManager.GetAllChildren(baseType)) + { + if (child.IsAbstract || child.IsInterface || child.IsGenericTypeDefinition) continue; + registrations.Add(child); + } + } + + foreach (var meansAttr in _reflectionManager.FindTypesWithAttribute()) + { + foreach (var type in _reflectionManager.FindTypesWithAttribute(meansAttr)) + { + registrations.Add(type); + } + } + + foreach (var type in registrations) + { + if (type.IsAbstract || type.IsInterface || type.IsGenericTypeDefinition) + { + Logger.Debug(LogCategory, $"Skipping registering data definition for type {type} since it is abstract or an interface"); + continue; + } + + if (!type.IsValueType && type.GetConstructors(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(m => m.GetParameters().Length == 0) == null) + { + Logger.Debug(LogCategory, $"Skipping registering data definition for type {type} since it has no parameterless ctor"); + continue; + } + + _dataDefinitions.Add(type, new SerializationDataDefinition(type)); + } + + var error = new StringBuilder(); + + foreach (var (type, definition) in _dataDefinitions) + { + if (definition.TryGetDuplicates(out var definitionDuplicates)) + { + error.Append($"{type}: [{string.Join(", ", definitionDuplicates)}]\n"); + } + } + + if (error.Length > 0) + { + throw new ArgumentException($"Duplicate data field tags found in:\n{error}"); + } + + foreach (var type in _reflectionManager.FindTypesWithAttribute()) + { + _copyByRefRegistrations.Add(type); + } + + _initialized = true; + _initializing = false; + } + + public bool HasDataDefinition(Type type) + { + if (type.IsGenericTypeDefinition) throw new NotImplementedException($"Cannot yet check data definitions for generic types. ({type})"); + return _dataDefinitions.ContainsKey(type); + } + + public ValidationNode ValidateNode(Type type, DataNode node, ISerializationContext? context = null) + { + var underlyingType = type.EnsureNotNullableType(); + + if (underlyingType.IsPrimitive || underlyingType == typeof(decimal)) + return node is ValueDataNode valueDataNode ? new ValidatedValueNode(valueDataNode) : new ErrorNode(node, "Invalid nodetype for primitive/decimal.", true); + + if (underlyingType.IsArray) + { + if (node is not SequenceDataNode sequenceDataNode) return new ErrorNode(node, "Invalid nodetype for array.", true); + var elementType = underlyingType.GetElementType(); + if (elementType == null) + throw new ArgumentException($"Failed to get elementtype of arraytype {underlyingType}", nameof(underlyingType)); + var validatedList = new List(); + foreach (var dataNode in sequenceDataNode.Sequence) + { + validatedList.Add(ValidateNode(elementType, dataNode, context)); + } + + return new ValidatedSequenceNode(validatedList); + } + + if (underlyingType.IsEnum) + { + var enumName = node switch + { + ValueDataNode valueNode => valueNode.Value, + SequenceDataNode sequenceNode => string.Join(", ", sequenceNode.Sequence), + _ => null + }; + + if (enumName == null) + { + return new ErrorNode(node, $"Invalid node type {node.GetType().Name} for enum {underlyingType}."); + } + + if (!Enum.TryParse(underlyingType, enumName, true, out var enumValue)) + { + return new ErrorNode(node, $"{enumValue} is not a valid enum value of type {underlyingType}", false); + } + + return new ValidatedValueNode(node); + } + + if (node.Tag?.StartsWith("!type:") == true) + { + var typeString = node.Tag.Substring(6); + try + { + underlyingType = ResolveConcreteType(underlyingType, typeString); + } + catch (InvalidOperationException) + { + return new ErrorNode(node, $"Failed to resolve !type tag: {typeString}", false); + } + } + + if (TryValidateWithTypeReader(underlyingType, node, DependencyCollection, context, out var valid)) return valid; + + if (typeof(ISelfSerialize).IsAssignableFrom(underlyingType)) + return node is ValueDataNode valueDataNode ? new ValidatedValueNode(valueDataNode) : new ErrorNode(node, "Invalid nodetype for ISelfSerialize", true); + + if (TryGetDataDefinition(underlyingType, out var dataDefinition)) + { + return node switch + { + ValueDataNode valueDataNode => valueDataNode.Value == "" ? new ValidatedValueNode(valueDataNode) : new ErrorNode(node, "Invalid nodetype for Datadefinition", false), + MappingDataNode mappingDataNode => dataDefinition.Validate(this, mappingDataNode, context), + _ => new ErrorNode(node, "Invalid nodetype for Datadefinition", true) + }; + } + + return new ErrorNode(node, "Failed to read node.", false); + } + + public ValidationNode ValidateNode(DataNode node, ISerializationContext? context = null) + { + return ValidateNode(typeof(T), node, context); + } + + public DeserializationResult CreateDataDefinition(DeserializedFieldEntry[] fields, bool skipHook = false) + where T : notnull, new() + { + var obj = new T(); + return PopulateDataDefinition(obj, new DeserializedDefinition(obj, fields), skipHook); + } + + public DeserializationResult PopulateDataDefinition(T obj, DeserializedDefinition definition, bool skipHook = false) + where T : notnull, new() + { + return PopulateDataDefinition(obj, (IDeserializedDefinition) definition, skipHook); + } + + public DeserializationResult PopulateDataDefinition(object obj, IDeserializedDefinition definition, bool skipHook = false) + { + if (!TryGetDataDefinition(obj.GetType(), out var dataDefinition)) + throw new ArgumentException($"Provided Type is not a data definition ({obj.GetType()})"); + + if (obj is IPopulateDefaultValues populateDefaultValues) + { + populateDefaultValues.PopulateDefaultValues(); + } + + var res = dataDefinition.InvokePopulateDelegate(obj, definition.Mapping); + + if (!skipHook && res.RawValue is ISerializationHooks serializationHooksAfter) + { + serializationHooksAfter.AfterDeserialization(); + } + + return res; + } + + private SerializationDataDefinition? GetDataDefinition(Type type) + { + if (_dataDefinitions.TryGetValue(type, out var dataDefinition)) return dataDefinition; + + return null; + } + + private bool TryGetDataDefinition(Type type, [NotNullWhen(true)] out SerializationDataDefinition? dataDefinition) + { + dataDefinition = GetDataDefinition(type); + return dataDefinition != null; + } + + public DeserializationResult Read(Type type, DataNode node, ISerializationContext? context = null, bool skipHook = false) + { + var underlyingType = type.EnsureNotNullableType(); + + // val primitives + if (underlyingType.IsPrimitive || underlyingType == typeof(decimal)) + { + if (node is not ValueDataNode valueDataNode) throw new InvalidNodeTypeException(); + var foo = TypeDescriptor.GetConverter(type); + return DeserializationResult.Value(foo.ConvertFromInvariantString(valueDataNode.Value)); + } + + // array + if (underlyingType.IsArray) + { + if (node is not SequenceDataNode sequenceDataNode) throw new InvalidNodeTypeException(); + var newArray = (Array) Activator.CreateInstance(type, sequenceDataNode.Sequence.Count)!; + var results = new List(); + + var idx = 0; + foreach (var entryNode in sequenceDataNode.Sequence) + { + var value = Read(type.GetElementType()!, entryNode, context, skipHook); + results.Add(value); + newArray.SetValue(value.RawValue, idx++); + } + + return new DeserializedArray(newArray, results); + } + + if (underlyingType.IsEnum) + { + return DeserializationResult.Value(node switch + { + ValueDataNode valueNode => Enum.Parse(underlyingType, valueNode.Value, true), + SequenceDataNode sequenceNode => Enum.Parse(underlyingType, string.Join(", ", sequenceNode.Sequence), true), + _ => throw new InvalidNodeTypeException($"Cannot serialize node as {underlyingType}, unsupported node type {node.GetType()}") + }); + } + + if (node.Tag?.StartsWith("!type:") == true) + { + var typeString = node.Tag.Substring(6); + underlyingType = ResolveConcreteType(underlyingType, typeString); + } + + if (TryReadWithTypeSerializers(underlyingType, node, DependencyCollection, out var serializedObj, skipHook, context)) + { + return serializedObj; + } + + if (typeof(ISelfSerialize).IsAssignableFrom(underlyingType)) + { + if (node is not ValueDataNode valueDataNode) throw new InvalidNodeTypeException(); + + var selfSerObj = (ISelfSerialize) Activator.CreateInstance(underlyingType)!; + selfSerObj.Deserialize(valueDataNode.Value); + + return DeserializationResult.Value(selfSerObj); + } + + //if (node is not MappingDataNode mappingDataNode) throw new InvalidNodeTypeException(); + + if (underlyingType.IsInterface || underlyingType.IsAbstract) + { + throw new InvalidOperationException($"Unable to create an instance of an interface or abstract type. Type: {underlyingType}"); + } + + var obj = Activator.CreateInstance(underlyingType)!; + + if (obj is IPopulateDefaultValues populateDefaultValues) + { + populateDefaultValues.PopulateDefaultValues(); + } + + if (!TryGetDataDefinition(underlyingType, out var dataDef)) + { + throw new InvalidOperationException($"No data definition found for type {underlyingType} with node type {node.GetType()} when reading"); + } + + if (node is not MappingDataNode mappingDataNode) + { + if(node is not ValueDataNode emptyValueDataNode || emptyValueDataNode.Value != "") + throw new ArgumentException($"No mapping node provided for type {type}"); + mappingDataNode = new MappingDataNode(); //if we get an emptyValueDataNode we just use an empty mapping + } + + var res = dataDef.InvokePopulateDelegate(obj, mappingDataNode, this, context, skipHook); + + if (!skipHook && res.RawValue is ISerializationHooks serHooks) + { + serHooks.AfterDeserialization(); + } + + return res; + } + + public object? ReadValue(Type type, DataNode node, ISerializationContext? context = null, bool skipHook = false) + { + return Read(type, node, context, skipHook).RawValue; + } + + public T? ReadValueCast(Type type, DataNode node, ISerializationContext? context = null, bool skipHook = false) + { + var value = Read(type, node, context, skipHook); + + if (value.RawValue == null) + { + return default; + } + + return (T?) value.RawValue; + } + + public T? ReadValue(DataNode node, ISerializationContext? context = null, bool skipHook = false) + { + return ReadValueCast(typeof(T), node, context, skipHook); + } + + public DataNode WriteValue(T value, bool alwaysWrite = false, + ISerializationContext? context = null) where T : notnull + { + return WriteValue(typeof(T), value, alwaysWrite, context); + } + + public DataNode WriteValue(Type type, object? value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + var underlyingType = Nullable.GetUnderlyingType(type) ?? type; + + if (value == null) return new MappingDataNode(); + + if (underlyingType.IsPrimitive || + underlyingType.IsEnum || + underlyingType == typeof(decimal)) + { + // All primitives and enums implement IConvertible. + // Need it for the culture overload. + var convertible = (IConvertible) value; + return new ValueDataNode(convertible.ToString(CultureInfo.InvariantCulture)); + } + + // array + if (underlyingType.IsArray) + { + var sequenceNode = new SequenceDataNode(); + var array = (Array) value; + + foreach (var val in array) + { + var serializedVal = WriteValue(val.GetType(), val, alwaysWrite, context); + sequenceNode.Add(serializedVal); + } + + return sequenceNode; + } + + if (value is ISerializationHooks serHook) + serHook.BeforeSerialization(); + + if (TryWriteWithTypeSerializers(underlyingType, value, out var node, alwaysWrite, context)) + { + return node; + } + + if (typeof(ISelfSerialize).IsAssignableFrom(underlyingType)) + { + var selfSerObj = (ISelfSerialize)value; + return new ValueDataNode(selfSerObj.Serialize()); + } + + var currentType = underlyingType; + var mapping = new MappingDataNode(); + if (underlyingType.IsAbstract || underlyingType.IsInterface) + { + mapping.Tag = $"!type:{value.GetType().Name}"; + currentType = value.GetType(); + } + + if (!TryGetDataDefinition(currentType, out var dataDef)) + { + throw new InvalidOperationException($"No data definition found for type {type} when writing"); + } + + if (dataDef.CanCallWith(value) != true) + { + throw new ArgumentException($"Supplied value does not fit with data definition of {type}."); + } + + var newMapping = dataDef.InvokeSerializeDelegate(value, this, context, alwaysWrite); + mapping = mapping.Merge(newMapping); + + return mapping; + } + + private object? CopyToTarget(object? source, object? target, ISerializationContext? context = null, bool skipHook = false) + { + if (source == null || target == null) + { + return source; + } + + var sourceType = source.GetType(); + var targetType = target.GetType(); + + if (sourceType.IsPrimitive && targetType.IsPrimitive) + { + //todo does this work + //i think it does + //todo validate we can assign source + return source; + } + + if (source.GetType().IsPrimitive != target.GetType().IsPrimitive) + { + throw new InvalidOperationException( + $"Source and target do not match. Source ({sourceType}) is primitive type? {sourceType.IsPrimitive}. Target ({targetType}) is primitive type? {targetType.IsPrimitive}"); + } + + if (sourceType.IsValueType && targetType.IsValueType) + { + return source; + } + + if (source.GetType().IsValueType != target.GetType().IsValueType) + { + throw new InvalidOperationException( + $"Source and target do not match. Source ({sourceType}) is value type? {sourceType.IsValueType}. Target ({targetType}) is value type? {targetType.IsValueType}"); + } + + // array + if (sourceType.IsArray && targetType.IsArray) + { + var sourceArray = (Array) source; + var targetArray = (Array) target; + + Array newArray; + if(sourceArray.Length == targetArray.Length) + { + newArray = targetArray; + } + else + { + newArray = (Array) Activator.CreateInstance(sourceArray.GetType(), sourceArray.Length)!; + } + + for (int i = 0; i < sourceArray.Length; i++) + { + newArray.SetValue(CreateCopy(sourceArray.GetValue(i), context, skipHook), i); + } + + return newArray; + } + + if (source.GetType().IsArray != target.GetType().IsArray) + { + throw new InvalidOperationException( + $"Source and target do not match. Source ({sourceType}) is array type? {sourceType.IsArray}. Target ({targetType}) is array type? {targetType.IsArray}"); + } + + var commonType = TypeHelpers.SelectCommonType(source.GetType(), target.GetType()); + if (commonType == null) + { + throw new InvalidOperationException("Could not find common type in Copy!"); + } + + if (_copyByRefRegistrations.Contains(commonType) || commonType.IsEnum) + { + return source; + } + + if (TryCopyWithTypeCopier(commonType, source, ref target, skipHook, context)) + { + return target; + } + + if (target is IPopulateDefaultValues populateDefaultValues) + { + populateDefaultValues.PopulateDefaultValues(); + } + + if (!TryGetDataDefinition(commonType, out var dataDef)) + { + throw new InvalidOperationException($"No data definition found for type {commonType} when copying"); + } + + target = dataDef.InvokeCopyDelegate(source, target, this, context); + + if (!skipHook && target is ISerializationHooks afterHooks) + { + afterHooks.AfterDeserialization(); + } + + return target; + } + + [MustUseReturnValue] + public object? Copy(object? source, object? target, ISerializationContext? context = null, bool skipHook = false) + { + return CopyToTarget(source, target, context, skipHook); + } + + [MustUseReturnValue] + public T? Copy(T? source, T? target, ISerializationContext? context = null, bool skipHook = false) + { + var copy = CopyToTarget(source, target, context, skipHook); + + return copy == null ? default : (T?) copy; + } + + private object? CreateCopyInternal(Type type, object? source, ISerializationContext? context = null, bool skipHook = false) + { + if (source == null) return source; + + if (type.IsPrimitive || type.IsEnum || source is string || _copyByRefRegistrations.Contains(type)) + { + return source; + } + + var target = Activator.CreateInstance(source.GetType())!; + return Copy(source, target, context, skipHook); + } + + public object? CreateCopy(object? source, ISerializationContext? context = null, bool skipHook = false) + { + if (source == null) return null; + return CreateCopyInternal(source.GetType(), source, context, skipHook); + } + + public T? CreateCopy(T? source, ISerializationContext? context = null, bool skipHook = false) + { + var copy = CreateCopyInternal(typeof(T), source, context, skipHook); + + if (copy == null) + { + return default; + } + + return (T?) copy; + } + + private static Type ResolveConcreteType(Type baseType, string typeName) + { + var reflection = IoCManager.Resolve(); + var type = reflection.YamlTypeTagLookup(baseType, typeName); + if (type == null) + { + throw new InvalidOperationException($"Type '{baseType}' is abstract, but could not find concrete type '{typeName}'."); + } + + return type; + } + } +} diff --git a/Robust.Shared/Serialization/Manager/SerializationManagerReadExtensions.cs b/Robust.Shared/Serialization/Manager/SerializationManagerReadExtensions.cs new file mode 100644 index 000000000..e78ed0a83 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/SerializationManagerReadExtensions.cs @@ -0,0 +1,116 @@ +#nullable enable +using System; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; + +namespace Robust.Shared.Serialization.Manager +{ + public static class SerializationManagerReadExtensions + { + public static T ReadValueOrThrow( + this ISerializationManager manager, + DataNode node, + ISerializationContext? context = null, + bool skipHook = false) + { + return manager.ReadValue(node, context, skipHook) ?? throw new NullReferenceException(); + } + + public static T ReadValueOrThrow( + this ISerializationManager manager, + Type type, + DataNode node, + ISerializationContext? context = null, + bool skipHook = false) + { + return manager.ReadValueCast(type, node, context, skipHook) ?? throw new NullReferenceException(); + } + + public static object ReadValueOrThrow( + this ISerializationManager manager, + Type type, + DataNode node, + ISerializationContext? context = null, + bool skipHook = false) + { + return manager.ReadValue(type, node, context, skipHook) ?? throw new NullReferenceException(); + } + + public static (DeserializationResult result, object? value) ReadWithValue( + this ISerializationManager manager, + Type type, DataNode node, + ISerializationContext? context = null, + bool skipHook = false) + { + var result = manager.Read(type, node, context, skipHook); + return (result, result.RawValue); + } + + public static (DeserializationResult result, T? value) ReadWithValue( + this ISerializationManager manager, + DataNode node, + ISerializationContext? context = null, + bool skipHook = false) + { + var result = manager.Read(typeof(T), node, context, skipHook); + + if (result.RawValue == null) + { + return (result, default); + } + + return (result, (T) result.RawValue); + } + + public static (DeserializationResult result, T? value) ReadWithValueCast( + this ISerializationManager manager, + Type type, + DataNode node, + ISerializationContext? context = null, + bool skipHook = false) + { + var result = manager.Read(type, node, context, skipHook); + + if (result.RawValue == null) + { + return (result, default); + } + + return (result, (T) result.RawValue); + } + + + public static (T value, DeserializationResult result) ReadWithValueOrThrow( + this ISerializationManager manager, + DataNode node, + ISerializationContext? context = null, + bool skipHook = false) + { + var result = manager.Read(typeof(T), node, context, skipHook); + + if (result.RawValue == null) + { + throw new NullReferenceException(); + } + + return ((T) result.RawValue, result); + } + + public static (T value, DeserializationResult result) ReadWithValueOrThrow( + this ISerializationManager manager, + Type type, + DataNode node, + ISerializationContext? context = null, + bool skipHook = false) + { + var result = manager.Read(type, node, context, skipHook); + + if (result.RawValue == null) + { + throw new NullReferenceException(); + } + + return ((T) result.RawValue, result); + } + } +} diff --git a/Robust.Shared/Serialization/Manager/SerializationManagerWriteExtensions.cs b/Robust.Shared/Serialization/Manager/SerializationManagerWriteExtensions.cs new file mode 100644 index 000000000..5aba1cfe9 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/SerializationManagerWriteExtensions.cs @@ -0,0 +1,29 @@ +using System; +using Robust.Shared.Serialization.Markdown; + +namespace Robust.Shared.Serialization.Manager +{ + public static class SerializationManagerWriteExtensions + { + public static T WriteValueAs( + this ISerializationManager manager, + object value, + bool alwaysWrite = false, + ISerializationContext? context = null) + where T : DataNode + { + return manager.WriteValueAs(value.GetType(), value, alwaysWrite, context); + } + + public static T WriteValueAs( + this ISerializationManager manager, + Type type, + object value, + bool alwaysWrite = false, + ISerializationContext? context = null) + where T : DataNode + { + return (T) manager.WriteValue(type, value, alwaysWrite, context); + } + } +} diff --git a/Robust.Shared/Serialization/Manager/SerializationManager_FlagsAndConstants.cs b/Robust.Shared/Serialization/Manager/SerializationManager_FlagsAndConstants.cs new file mode 100644 index 000000000..0a4e30f90 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/SerializationManager_FlagsAndConstants.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; + +namespace Robust.Shared.Serialization.Manager +{ + public partial class SerializationManager + { + private readonly Dictionary _constantsMapping = new(); + private readonly Dictionary _flagsMapping = new(); + + private void InitializeFlagsAndConstants() + { + foreach (Type constType in _reflectionManager.FindTypesWithAttribute()) + { + if (!constType.IsEnum) + { + throw new InvalidOperationException($"Could not create ConstantMapping for non-enum {constType}."); + } + + if (Enum.GetUnderlyingType(constType) != typeof(int)) + { + throw new InvalidOperationException($"Could not create ConstantMapping for non-int enum {constType}."); + } + + foreach (var constantsForAttribute in constType.GetCustomAttributes(true)) + { + if (_constantsMapping.ContainsKey(constantsForAttribute.Tag)) + { + throw new NotSupportedException($"Multiple constant enums declared for the tag {constantsForAttribute.Tag}."); + } + + _constantsMapping.Add(constantsForAttribute.Tag, constType); + } + } + + foreach (var bitflagType in _reflectionManager.FindTypesWithAttribute()) + { + if (!bitflagType.IsEnum) + { + throw new InvalidOperationException($"Could not create FlagSerializer for non-enum {bitflagType}."); + } + + if (Enum.GetUnderlyingType(bitflagType) != typeof(int)) + { + throw new InvalidOperationException($"Could not create FlagSerializer for non-int enum {bitflagType}."); + } + + if (!bitflagType.GetCustomAttributes(false).Any()) + { + throw new InvalidOperationException($"Could not create FlagSerializer for non-bitflag enum {bitflagType}."); + } + + foreach (var flagType in bitflagType.GetCustomAttributes(true)) + { + if (_flagsMapping.ContainsKey(flagType.Tag)) + { + throw new NotSupportedException($"Multiple bitflag enums declared for the tag {flagType.Tag}."); + } + + _flagsMapping.Add(flagType.Tag, bitflagType); + } + } + } + + private Type GetFlagTypeFromTag(Type tagType) + { + return _flagsMapping[tagType]; + } + + private Type GetConstantTypeFromTag(Type tagType) + { + return _constantsMapping[tagType]; + } + + public int ReadFlag(Type tagType, DataNode node) + { + var flagType = GetFlagTypeFromTag(tagType); + switch (node) + { + case ValueDataNode valueDataNode: + return int.Parse(valueDataNode.Value); + case SequenceDataNode sequenceDataNode: + var flags = 0; + + foreach (var elem in sequenceDataNode.Sequence) + { + if (elem is not ValueDataNode valueDataNode) throw new InvalidNodeTypeException(); + flags |= (int) Enum.Parse(flagType, valueDataNode.Value); + } + + return flags; + default: + throw new InvalidNodeTypeException(); + } + } + + public ValidationNode ValidateFlag(Type tagType, DataNode node) + { + var flagType = GetFlagTypeFromTag(tagType); + switch (node) + { + case ValueDataNode valueDataNode: + return int.TryParse(valueDataNode.Value, out _) ? new ValidatedValueNode(node) : new ErrorNode(node, "Failed parsing flag.", false); + case SequenceDataNode sequenceDataNode: + foreach (var elem in sequenceDataNode.Sequence) + { + if (elem is not ValueDataNode valueDataNode) return new ErrorNode(node, "Invalid flagtype in flag-sequence.", true); + if (!Enum.TryParse(flagType, valueDataNode.Value, out _)) return new ErrorNode(node, "Failed parsing flag in flag-sequence", false); + } + + return new ValidatedValueNode(node); + default: + return new ErrorNode(node, "Invalid nodetype for flag.", true); + } + } + + public int ReadConstant(Type tagType, DataNode node) + { + if (node is not ValueDataNode valueDataNode) throw new InvalidNodeTypeException(); + var constType = GetConstantTypeFromTag(tagType); + return (int) Enum.Parse(constType, valueDataNode.Value); + } + + public ValidationNode ValidateConstant(Type tagType, DataNode node) + { + if (node is not ValueDataNode valueDataNode) return new ErrorNode(node, "Invalid nodetype for constant.", true); + var constType = GetConstantTypeFromTag(tagType); + return Enum.TryParse(constType, valueDataNode.Value, out _) ? new ValidatedValueNode(node) : new ErrorNode(node, "Failed parsing constant.", false); + } + + public DataNode WriteFlag(Type tagType, int flag) + { + var sequenceNode = new SequenceDataNode(); + var flagType = GetFlagTypeFromTag(tagType); + + // Assumption: a bitflag enum has a constructor for every bit value such that + // that bit is set in some other constructor i.e. if a 1 appears somewhere in + // the bits of one of the enum constructors, there is an enum constructor which + // is 1 just in that position. + // + // Otherwise, this code may throw an exception + var maxFlagValue = ((int[])Enum.GetValues(flagType)).Max(); + + for (var bitIndex = 1; bitIndex <= maxFlagValue; bitIndex = bitIndex << 1) + { + if ((bitIndex & flag) == bitIndex) + { + var flagName = Enum.GetName(flagType, bitIndex); + + if (flagName == null) + { + throw new InvalidOperationException($"No bitflag corresponding to bit {bitIndex} in {flagType}, but it was set anyways."); + } + + sequenceNode.Add(new ValueDataNode(flagName)); + } + } + + return sequenceNode; + } + + public DataNode WriteConstant(Type tagType, int constant) + { + var constType = GetConstantTypeFromTag(tagType); + var constantName = Enum.GetName(constType, constant); + + if (constantName == null) + { + throw new InvalidOperationException($"No constant corresponding to value {constant} in {constType}."); + } + + return new ValueDataNode(constantName); + } + } +} diff --git a/Robust.Shared/Serialization/Manager/SerializationManager_Typeserializers.cs b/Robust.Shared/Serialization/Manager/SerializationManager_Typeserializers.cs new file mode 100644 index 000000000..52163bcb3 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/SerializationManager_Typeserializers.cs @@ -0,0 +1,392 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.Manager +{ + public partial class SerializationManager + { + private readonly Dictionary<(Type Type, Type DataNodeType), object> _typeReaders = new(); + private readonly Dictionary _typeWriters = new(); + private readonly Dictionary _typeCopiers = new(); + + private readonly Dictionary<(Type Type, Type DataNodeType), Type> _genericReaderTypes = new(); + private readonly Dictionary _genericWriterTypes = new(); + private readonly Dictionary _genericCopierTypes = new(); + + private void InitializeTypeSerializers() + { + foreach (var type in _reflectionManager.FindTypesWithAttribute()) + { + RegisterSerializer(type); + } + } + + private object? RegisterSerializer(Type type) + { + var writerInterfaces = type.GetInterfaces() + .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ITypeWriter<>)).ToArray(); + var readerInterfaces = type.GetInterfaces() + .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ITypeReader<,>)).ToArray(); + var copierInterfaces = type.GetInterfaces() + .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ITypeCopier<>)).ToArray(); + + if (readerInterfaces.Length == 0 && writerInterfaces.Length == 0 && copierInterfaces.Length == 0) + { + throw new InvalidOperationException( + "Tried to register TypeReader/Writer/Copier that had none of the interfaces inherited."); + } + + if (type.IsGenericTypeDefinition) + { + foreach (var writerInterface in writerInterfaces) + { + if (!_genericWriterTypes.TryAdd(writerInterface.GetGenericArguments()[0], type)) + Logger.Error(LogCategory, $"Tried registering generic writer for type {writerInterface.GetGenericArguments()[0]} twice"); + } + + foreach (var readerInterface in readerInterfaces) + { + if (!_genericReaderTypes.TryAdd((readerInterface.GetGenericArguments()[0], readerInterface.GetGenericArguments()[1]), type)) + Logger.Error(LogCategory, $"Tried registering generic reader for type {readerInterface.GetGenericArguments()[0]} and datanode {readerInterface.GetGenericArguments()[1]} twice"); + } + + foreach (var copierInterface in copierInterfaces) + { + if (!_genericCopierTypes.TryAdd(copierInterface.GetGenericArguments()[0], type)) + Logger.Error(LogCategory, $"Tried registering generic copier for type {copierInterface.GetGenericArguments()[0]} twice"); + } + + return null; + } + else + { + var serializer = Activator.CreateInstance(type)!; + + foreach (var writerInterface in writerInterfaces) + { + if (!_typeWriters.TryAdd(writerInterface.GetGenericArguments()[0], serializer)) + Logger.Error(LogCategory, $"Tried registering writer for type {writerInterface.GetGenericArguments()[0]} twice"); + } + + foreach (var readerInterface in readerInterfaces) + { + if (!_typeReaders.TryAdd((readerInterface.GetGenericArguments()[0], readerInterface.GetGenericArguments()[1]), serializer)) + Logger.Error(LogCategory, $"Tried registering reader for type {readerInterface.GetGenericArguments()[0]} and datanode {readerInterface.GetGenericArguments()[1]} twice"); + } + + foreach (var copierInterface in copierInterfaces) + { + if (!_typeCopiers.TryAdd(copierInterface.GetGenericArguments()[0], serializer)) + Logger.Error(LogCategory, $"Tried registering copier for type {copierInterface.GetGenericArguments()[0]} twice"); + } + + return serializer; + } + } + + private bool TryGetGenericReader([NotNullWhen(true)] out ITypeReader? rawReader) + where TNode : DataNode where T : notnull + { + rawReader = null; + + if (typeof(T).IsGenericType) + { + var typeDef = typeof(T).GetGenericTypeDefinition(); + + Type? serializerTypeDef = null; + + foreach (var (key, val) in _genericReaderTypes) + { + if (typeDef.HasSameMetadataDefinitionAs(key.Type) && key.DataNodeType.IsAssignableFrom(typeof(TNode))) + { + serializerTypeDef = val; + break; + } + } + + if (serializerTypeDef == null) return false; + + var serializerType = serializerTypeDef.MakeGenericType(typeof(T).GetGenericArguments()); + rawReader = (ITypeReader) RegisterSerializer(serializerType)!; + + return true; + } + + return false; + } + + private bool TryGetGenericWriter([NotNullWhen(true)] out ITypeWriter? rawWriter) where T : notnull + { + rawWriter = null; + + if (typeof(T).IsGenericType) + { + var typeDef = typeof(T).GetGenericTypeDefinition(); + + Type? serializerTypeDef = null; + + foreach (var (key, val) in _genericWriterTypes) + { + if (typeDef.HasSameMetadataDefinitionAs(key)) + { + serializerTypeDef = val; + break; + } + } + + if (serializerTypeDef == null) return false; + + var serializerType = serializerTypeDef.MakeGenericType(typeof(T).GetGenericArguments()); + rawWriter = (ITypeWriter) RegisterSerializer(serializerType)!; + + return true; + } + + return false; + } + + private bool TryGetGenericCopier([NotNullWhen(true)] out ITypeCopier? rawCopier) where T : notnull + { + rawCopier = null; + + if (typeof(T).IsGenericType) + { + var typeDef = typeof(T).GetGenericTypeDefinition(); + + Type? serializerTypeDef = null; + + foreach (var (key, val) in _genericCopierTypes) + { + if (typeDef.HasSameMetadataDefinitionAs(key)) + { + serializerTypeDef = val; + break; + } + } + + if (serializerTypeDef == null) return false; + + var serializerType = serializerTypeDef.MakeGenericType(typeof(T).GetGenericArguments()); + rawCopier = (ITypeCopier) RegisterSerializer(serializerType)!; + + return true; + } + + return false; + } + + private bool TryReadWithTypeSerializers( + Type type, + DataNode node, + IDependencyCollection dependencies, + [NotNullWhen(true)] out DeserializationResult? obj, + bool skipHook, + ISerializationContext? context = null) + { + //TODO Paul: do this shit w/ delegates + var method = typeof(SerializationManager).GetRuntimeMethods() + .First(m => m.Name == nameof(TryReadWithTypeSerializers) && m.GetParameters().Length == 5) + .MakeGenericMethod(type, node.GetType()); + + obj = default; + + var arr = new object?[] {node, dependencies, obj, skipHook, context}; + var res = method.Invoke(this, arr); + + if (res as bool? ?? false) + { + obj = (DeserializationResult) arr[2]!; + return true; + } + + return false; + } + + private bool TryValidateWithTypeReader( + Type type, + DataNode node, + IDependencyCollection dependencies, + ISerializationContext? context, + [NotNullWhen(true)] out ValidationNode? valid) + { + //TODO Paul: do this shit w/ delegates + var method = typeof(SerializationManager).GetRuntimeMethods().First(m => + m.Name == nameof(TryValidateWithTypeReader) && m.GetParameters().Length == 4).MakeGenericMethod(type, node.GetType()); + + var arr = new object?[] {node, dependencies, context, null}; + var res = method.Invoke(this, arr); + + if (res as bool? ?? false) + { + valid = (ValidationNode) arr[3]!; + return true; + } + + valid = null; + return false; + } + + private bool TryValidateWithTypeReader( + TNode node, + IDependencyCollection dependencies, + ISerializationContext? context, + [NotNullWhen(true)] out ValidationNode? valid) + where T : notnull + where TNode : DataNode + { + if (TryGetReader(null, out var reader)) + { + valid = reader.Validate(this, node, dependencies, context); + return true; + } + + valid = null; + return false; + } + + private bool TryGetReader( + ISerializationContext? context, + [NotNullWhen(true)] out ITypeReader? reader) + where T : notnull + where TNode : DataNode + { + if (context != null && context.TypeReaders.TryGetValue((typeof(T), typeof(TNode)), out var rawTypeReader) || + _typeReaders.TryGetValue((typeof(T), typeof(TNode)), out rawTypeReader)) + { + reader = (ITypeReader) rawTypeReader; + return true; + } + + return TryGetGenericReader(out reader); + } + + private bool TryReadWithTypeSerializers( + TNode node, + IDependencyCollection dependencies, + [NotNullWhen(true)] out DeserializationResult? obj, + bool skipHook, + ISerializationContext? context = null) + where T : notnull + where TNode : DataNode + { + if (TryGetReader(context, out var reader)) + { + obj = reader.Read(this, node, dependencies, skipHook, context); + return true; + } + + obj = null; + return false; + } + + private bool TryWriteWithTypeSerializers( + Type type, + object obj, + [NotNullWhen(true)] out DataNode? node, + bool alwaysWrite = false, + ISerializationContext? context = null) + { + //TODO Paul: do this shit w/ delegates + var method = typeof(SerializationManager).GetRuntimeMethods().First(m => + m.Name == nameof(TryWriteWithTypeSerializers) && m.GetParameters().Length == 4).MakeGenericMethod(type); + + node = null; + + var arr = new[] {obj, node, alwaysWrite, context}; + var res = method.Invoke(this, arr); + + if (res as bool? ?? false) + { + node = (DataNode) arr[1]!; + return true; + } + + return false; + } + + private bool TryGetWriter(ISerializationContext? context, [NotNullWhen(true)] out ITypeWriter? writer) where T : notnull + { + if (context != null && context.TypeWriters.TryGetValue(typeof(T), out var rawTypeWriter) || + _typeWriters.TryGetValue(typeof(T), out rawTypeWriter)) + { + writer = (ITypeWriter) rawTypeWriter; + return true; + } + + return TryGetGenericWriter(out writer); + } + + private bool TryWriteWithTypeSerializers( + T obj, + [NotNullWhen(true)] out DataNode? node, + bool alwaysWrite = false, + ISerializationContext? context = null) where T : notnull + { + node = default; + if (TryGetWriter(context, out var writer)) + { + node = writer.Write(this, obj, alwaysWrite, context); + return true; + } + + return false; + } + + private bool TryCopyWithTypeCopier(Type type, object source, ref object target, bool skipHook, ISerializationContext? context = null) + { + //TODO Paul: do this shit w/ delegates + var method = typeof(SerializationManager).GetRuntimeMethods().First(m => + m.Name == nameof(TryCopyWithTypeCopier) && m.GetParameters().Length == 4).MakeGenericMethod(type, source.GetType(), target.GetType()); + + var arr = new[] {source, target, skipHook, context}; + var res = method.Invoke(this, arr); + + if (res as bool? ?? false) + { + target = arr[1]!; + return true; + } + + return false; + } + + private bool TryCopyWithTypeCopier( + TSource source, + ref TTarget target, + bool skipHook, + ISerializationContext? context = null) + where TSource : TCommon + where TTarget : TCommon + where TCommon : notnull + { + object? rawTypeCopier; + + if (context != null && + context.TypeCopiers.TryGetValue(typeof(TCommon), out rawTypeCopier) || + _typeCopiers.TryGetValue(typeof(TCommon), out rawTypeCopier)) + { + var ser = (ITypeCopier) rawTypeCopier; + target = (TTarget) ser.Copy(this, source, target, skipHook, context); + return true; + } + + if (TryGetGenericCopier(out ITypeCopier? genericTypeWriter)) + { + target = (TTarget) genericTypeWriter.Copy(this, source, target, skipHook, context); + return true; + } + + return false; + } + } +} diff --git a/Robust.Shared/Serialization/Markdown/DataNode.cs b/Robust.Shared/Serialization/Markdown/DataNode.cs new file mode 100644 index 000000000..8a88bcfa2 --- /dev/null +++ b/Robust.Shared/Serialization/Markdown/DataNode.cs @@ -0,0 +1,40 @@ +using Robust.Shared.Serialization.Manager; + +namespace Robust.Shared.Serialization.Markdown +{ + public abstract class DataNode + { + public string? Tag; + public NodeMark Start; + public NodeMark End; + + public DataNode(NodeMark start, NodeMark end) + { + Start = start; + End = end; + } + + public abstract DataNode Copy(); + public abstract DataNode? Except(DataNode node); + + public T CopyCast() where T : DataNode + { + return (T) Copy(); + } + } + + public abstract class DataNode : DataNode where T : DataNode + { + protected DataNode(NodeMark start, NodeMark end) : base(start, end) + { } + + public abstract override T Copy(); + public abstract T? Except(T node); + + public override DataNode? Except(DataNode node) + { + if (node is not T tNode) throw new InvalidNodeTypeException(); + return Except(tNode); + } + } +} diff --git a/Robust.Shared/Serialization/Markdown/MappingDataNode.cs b/Robust.Shared/Serialization/Markdown/MappingDataNode.cs new file mode 100644 index 000000000..fce6c2ffa --- /dev/null +++ b/Robust.Shared/Serialization/Markdown/MappingDataNode.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using Robust.Shared.Utility; +using YamlDotNet.RepresentationModel; + +namespace Robust.Shared.Serialization.Markdown +{ + public class MappingDataNode : DataNode + { + private Dictionary _mapping = new(); + public IReadOnlyDictionary Children => _mapping; + + public MappingDataNode() : base(NodeMark.Invalid, NodeMark.Invalid) + { } + + public MappingDataNode(YamlMappingNode mapping) : base(mapping.Start, mapping.End) + { + foreach (var (key, val) in mapping.Children) + { + _mapping.Add(key.ToDataNode(), val.ToDataNode()); + } + + Tag = mapping.Tag; + } + + public MappingDataNode(Dictionary nodes) : this() + { + foreach (var (key, val) in nodes) + { + _mapping.Add(key, val); + } + } + + public KeyValuePair this[int key] => Children.ElementAt(key); + + public DataNode this[string index] + { + get => GetNode(index); + set => AddNode(index, value); + } + + public YamlMappingNode ToMappingNode() + { + var mapping = new YamlMappingNode(); + foreach (var (key, val) in _mapping) + { + mapping.Add(key.ToYamlNode(), val.ToYamlNode()); + } + + mapping.Tag = Tag; + + return mapping; + } + + public T Cast(string index) where T : DataNode + { + return (T) this[index]; + } + + public DataNode GetNode(DataNode key) + { + return _mapping[key]; + } + + public DataNode GetNode(string key) + { + return GetNode(_getFetchNode(key)); + } + + public bool TryGetNode(DataNode key, [NotNullWhen(true)] out DataNode? node) + { + if (_mapping.TryGetValue(key, out node)) + { + return true; + } + + node = null; + return false; + } + + public bool TryGetNode(string key, [NotNullWhen(true)] out DataNode? node) + { + return TryGetNode(_getFetchNode(key), out node); + } + + public bool HasNode(DataNode key) + { + return _mapping.ContainsKey(key); + } + + public bool HasNode(string key) + { + return HasNode(_getFetchNode(key)); + } + + public void AddNode(DataNode key, DataNode node) + { + _mapping.Add(key, node); + } + + public void AddNode(string key, DataNode node) + { + AddNode(new ValueDataNode(key), node); + } + + public MappingDataNode RemoveNode(DataNode key) + { + _mapping.Remove(key); + return this; + } + + public MappingDataNode RemoveNode(string key) + { + return RemoveNode(_getFetchNode(key)); + } + + public MappingDataNode Merge(MappingDataNode otherMapping) + { + var newMapping = Copy(); + foreach (var (key, val) in otherMapping.Children) + { + //intentionally provokes argumentexception + newMapping.AddNode(key.Copy(), val.Copy()); + } + + newMapping.Tag = Tag; + + //todo paul should prob make this smarter + newMapping.Start = Start; + newMapping.End = End; + + return newMapping; + } + + public override MappingDataNode Copy() + { + var newMapping = new MappingDataNode() + { + Tag = Tag, + Start = Start, + End = End + }; + + foreach (var (key, val) in _mapping) + { + newMapping.AddNode(key.Copy(), val.Copy()); + } + + return newMapping; + } + + public override MappingDataNode? Except(MappingDataNode node) + { + var mappingNode = new MappingDataNode(){Tag = Tag, Start = Start, End = End}; + + foreach (var (key, val) in _mapping) + { + var other = node._mapping.FirstOrNull(p => p.Key.Equals(key)); + if (other == null) + { + mappingNode.AddNode(key.Copy(), val.Copy()); + } + else + { + var newValue = val.Except(other.Value.Value); + if(newValue == null) continue; + mappingNode.AddNode(key.Copy(), newValue); + } + } + + if (mappingNode._mapping.Count == 0) return null; + + return mappingNode; + } + + public override int GetHashCode() + { + var code = new HashCode(); + foreach (var (key, value) in _mapping) + { + code.Add(key); + code.Add(value); + } + + return code.ToHashCode(); + } + + // To fetch nodes by key name with YAML, we NEED a YamlScalarNode. + // We use a thread local one to avoid allocating one every fetch, since we just replace the inner value. + // Obviously thread local to avoid threading issues. + private static readonly ThreadLocal FetchNode = + new(() => new ValueDataNode("")); + + private static ValueDataNode _getFetchNode(string key) + { + var node = FetchNode.Value!; + node.Value = key; + return node; + } + } +} diff --git a/Robust.Shared/Serialization/Markdown/NodeMark.cs b/Robust.Shared/Serialization/Markdown/NodeMark.cs new file mode 100644 index 000000000..7978af39e --- /dev/null +++ b/Robust.Shared/Serialization/Markdown/NodeMark.cs @@ -0,0 +1,81 @@ +using System; +using YamlDotNet.Core; + +namespace Robust.Shared.Serialization.Markdown +{ + public readonly struct NodeMark : IEquatable, IComparable + { + public static NodeMark Invalid => new(-1, -1); + + public NodeMark(int line, int column) + { + Line = line; + Column = column; + } + + public NodeMark(Mark mark) : this(mark.Line, mark.Column) + { + } + + public int Line { get; init; } + public int Column { get; init; } + + public override string ToString() + { + return $"Line: {Line}, Col: {Column}"; + } + + public bool Equals(NodeMark other) + { + return Line == other.Line && Column == other.Column; + } + + public override bool Equals(object? obj) + { + return obj is NodeMark other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(Line, Column); + } + + public int CompareTo(NodeMark other) + { + var lineNum = Line.CompareTo(other.Line); + return lineNum == 0 ? Column.CompareTo(other.Column) : lineNum; + } + + public static implicit operator NodeMark(Mark mark) => new(mark); + + public static bool operator ==(NodeMark left, NodeMark right) + { + return left.Equals(right); + } + + public static bool operator !=(NodeMark left, NodeMark right) + { + return !left.Equals(right); + } + + public static bool operator <(NodeMark? left, NodeMark? right) + { + if (left == null || right == null) + { + return false; + } + + return left.Value.CompareTo(right.Value) < 0; + } + + public static bool operator >(NodeMark? left, NodeMark? right) + { + if (left == null || right == null) + { + return false; + } + + return left.Value.CompareTo(right.Value) > 0; + } + } +} diff --git a/Robust.Shared/Serialization/Markdown/SequenceDataNode.cs b/Robust.Shared/Serialization/Markdown/SequenceDataNode.cs new file mode 100644 index 000000000..e4166e271 --- /dev/null +++ b/Robust.Shared/Serialization/Markdown/SequenceDataNode.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using YamlDotNet.RepresentationModel; + +namespace Robust.Shared.Serialization.Markdown +{ + public class SequenceDataNode : DataNode + { + private readonly List _nodes = new(); + + public SequenceDataNode() : base(NodeMark.Invalid, NodeMark.Invalid) { } + + public SequenceDataNode(List nodes) : this() + { + _nodes = nodes; + } + + public SequenceDataNode(YamlSequenceNode sequenceNode) : base(sequenceNode.Start, sequenceNode.End) + { + foreach (var node in sequenceNode.Children) + { + _nodes.Add(node.ToDataNode()); + } + + Tag = sequenceNode.Tag; + } + + public SequenceDataNode(params DataNode[] nodes) : this() + { + foreach (var node in nodes) + { + _nodes.Add(node); + } + } + + public SequenceDataNode(params string[] strings) : this() + { + foreach (var s in strings) + { + _nodes.Add(new ValueDataNode(s)); + } + } + + public YamlSequenceNode ToSequenceNode() + { + var node = new YamlSequenceNode(); + foreach (var dataNode in _nodes) + { + node.Children.Add(dataNode.ToYamlNode()); + } + + node.Tag = Tag; + + return node; + } + + public IReadOnlyList Sequence => _nodes; + + public DataNode this[int index] => _nodes[index]; + + public void Add(DataNode node) + { + _nodes.Add(node); + } + + public void Remove(DataNode node) + { + _nodes.Remove(node); + } + + public T Cast(int index) where T : DataNode + { + return (T) this[index]; + } + + public override SequenceDataNode Copy() + { + var newSequence = new SequenceDataNode() + { + Tag = Tag, + Start = Start, + End = End + }; + + foreach (var node in Sequence) + { + newSequence.Add(node.Copy()); + } + + return newSequence; + } + + public override int GetHashCode() + { + var code = new HashCode(); + foreach (var dataNode in _nodes) + { + code.Add(dataNode); + } + + return code.ToHashCode(); + } + + public override SequenceDataNode? Except(SequenceDataNode node) + { + var set = new HashSet(node._nodes); + var newList = new List(); + foreach (var nodeNode in node._nodes) + { + if (!set.Contains(nodeNode)) newList.Add(nodeNode); + } + + if(newList.Count > 0) + { + return new SequenceDataNode(newList) + { + Tag = Tag, + Start = Start, + End = End + }; + } + + return null; + } + } +} diff --git a/Robust.Shared/Serialization/Markdown/Validation/ErrorNode.cs b/Robust.Shared/Serialization/Markdown/Validation/ErrorNode.cs new file mode 100644 index 000000000..45accc120 --- /dev/null +++ b/Robust.Shared/Serialization/Markdown/Validation/ErrorNode.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; + +namespace Robust.Shared.Serialization.Markdown.Validation +{ + public class ErrorNode : ValidationNode + { + public readonly DataNode Node; + public readonly string ErrorReason; + public readonly bool AlwaysRelevant; + + public ErrorNode(DataNode node, string errorReason, bool alwaysRelevant = true) + { + Node = node; + ErrorReason = errorReason; + AlwaysRelevant = alwaysRelevant; + } + + public override bool Valid => false; + + public override IEnumerable GetErrors() + { + yield return this; + } + + public override int GetHashCode() + { + var code = new HashCode(); + code.Add(Node.Start.GetHashCode()); + code.Add(Node.End.GetHashCode()); + code.Add(ErrorReason.GetHashCode()); + return code.ToHashCode(); + } + + public override bool Equals(object? obj) + { + if (obj is not ErrorNode node) return false; + return Node.GetHashCode() == node.Node.GetHashCode(); // ErrorReason == node.ErrorReason + } + } +} diff --git a/Robust.Shared/Serialization/Markdown/Validation/InconclusiveNode.cs b/Robust.Shared/Serialization/Markdown/Validation/InconclusiveNode.cs new file mode 100644 index 000000000..f5b950c3b --- /dev/null +++ b/Robust.Shared/Serialization/Markdown/Validation/InconclusiveNode.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Robust.Shared.Serialization.Markdown.Validation +{ + public class InconclusiveNode : ValidationNode + { + public readonly DataNode DataNode; + + public InconclusiveNode(DataNode dataNode) + { + DataNode = dataNode; + } + + public override bool Valid => true; + + public override IEnumerable GetErrors() => Enumerable.Empty(); + + public override string? ToString() + { + return DataNode.ToString(); + } + } +} diff --git a/Robust.Shared/Serialization/Markdown/Validation/ValidatedMappingNode.cs b/Robust.Shared/Serialization/Markdown/Validation/ValidatedMappingNode.cs new file mode 100644 index 000000000..fb0e9b81b --- /dev/null +++ b/Robust.Shared/Serialization/Markdown/Validation/ValidatedMappingNode.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Robust.Shared.Serialization.Markdown.Validation +{ + public class ValidatedMappingNode : ValidationNode + { + public readonly Dictionary Mapping; + public override bool Valid => Mapping.All(p => p.Key.Valid && p.Value.Valid); + public override IEnumerable GetErrors() + { + foreach (var (key, value) in Mapping.Where(p => !p.Key.Valid || !p.Value.Valid)) + { + foreach (var invalid in key.GetErrors()) + { + yield return invalid; + } + + foreach (var invalid in value.GetErrors()) + { + yield return invalid; + } + } + } + + public ValidatedMappingNode(Dictionary mapping) + { + Mapping = mapping; + } + } +} diff --git a/Robust.Shared/Serialization/Markdown/Validation/ValidatedSequenceNode.cs b/Robust.Shared/Serialization/Markdown/Validation/ValidatedSequenceNode.cs new file mode 100644 index 000000000..8974d47c5 --- /dev/null +++ b/Robust.Shared/Serialization/Markdown/Validation/ValidatedSequenceNode.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Robust.Shared.Serialization.Markdown.Validation +{ + public class ValidatedSequenceNode : ValidationNode + { + public readonly List Sequence; + + public override bool Valid => Sequence.All(p => p.Valid); + public override IEnumerable GetErrors() + { + for (int i = 0; i < Sequence.Count; i++) + { + foreach (var invalid in Sequence[i].GetErrors()) + { + yield return invalid; + } + } + } + + public ValidatedSequenceNode(List sequence) + { + Sequence = sequence; + } + } +} diff --git a/Robust.Shared/Serialization/Markdown/Validation/ValidatedValueNode.cs b/Robust.Shared/Serialization/Markdown/Validation/ValidatedValueNode.cs new file mode 100644 index 000000000..03eeec131 --- /dev/null +++ b/Robust.Shared/Serialization/Markdown/Validation/ValidatedValueNode.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Robust.Shared.Serialization.Markdown.Validation +{ + public class ValidatedValueNode : ValidationNode + { + public readonly DataNode DataNode; + + public ValidatedValueNode(DataNode dataNode) + { + DataNode = dataNode; + } + + public override bool Valid => true; + + public override IEnumerable GetErrors() => Enumerable.Empty(); + + public override string? ToString() + { + return DataNode.ToString(); + } + } +} diff --git a/Robust.Shared/Serialization/Markdown/Validation/ValidationNode.cs b/Robust.Shared/Serialization/Markdown/Validation/ValidationNode.cs new file mode 100644 index 000000000..6c607b2f3 --- /dev/null +++ b/Robust.Shared/Serialization/Markdown/Validation/ValidationNode.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Robust.Shared.Serialization.Markdown.Validation +{ + public abstract class ValidationNode + { + public abstract bool Valid { get; } + + public abstract IEnumerable GetErrors(); + } +} diff --git a/Robust.Shared/Serialization/Markdown/ValueDataNode.cs b/Robust.Shared/Serialization/Markdown/ValueDataNode.cs new file mode 100644 index 000000000..50636227f --- /dev/null +++ b/Robust.Shared/Serialization/Markdown/ValueDataNode.cs @@ -0,0 +1,52 @@ +using YamlDotNet.RepresentationModel; + +namespace Robust.Shared.Serialization.Markdown +{ + public class ValueDataNode : DataNode + { + public ValueDataNode(string value) : base(NodeMark.Invalid, NodeMark.Invalid) + { + Value = value; + } + + public ValueDataNode(YamlScalarNode node) : base(node.Start, node.End) + { + Value = node.Value ?? ""; + Tag = node.Tag; + } + + public string Value { get; set; } + + public override ValueDataNode Copy() + { + return new ValueDataNode(Value) + { + Tag = Tag, + Start = Start, + End = End + }; + } + + public override ValueDataNode? Except(ValueDataNode node) + { + if (node.Value == Value) return null; + return Copy(); + } + + public override bool Equals(object? obj) + { + if(obj is not ValueDataNode node) return false; + return node.Value == Value; + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } + + public override string ToString() + { + return Value; + } + } +} diff --git a/Robust.Shared/Serialization/Markdown/YamlNodeHelpers.cs b/Robust.Shared/Serialization/Markdown/YamlNodeHelpers.cs new file mode 100644 index 000000000..37c35ea11 --- /dev/null +++ b/Robust.Shared/Serialization/Markdown/YamlNodeHelpers.cs @@ -0,0 +1,35 @@ +using System; +using YamlDotNet.RepresentationModel; + +namespace Robust.Shared.Serialization.Markdown +{ + public static class YamlNodeHelpers + { + public static DataNode ToDataNode(this YamlNode node) + { + return node switch + { + YamlScalarNode scalarNode => new ValueDataNode(scalarNode), + YamlMappingNode mappingNode => new MappingDataNode(mappingNode), + YamlSequenceNode sequenceNode => new SequenceDataNode(sequenceNode), + _ => throw new ArgumentOutOfRangeException(nameof(node)) + }; + } + + public static T ToDataNodeCast(this YamlNode node) where T : DataNode + { + return (T) node.ToDataNode(); + } + + public static YamlNode ToYamlNode(this DataNode node) + { + return node switch + { + ValueDataNode valueDataNode => new YamlScalarNode(valueDataNode.Value){Tag = valueDataNode.Tag}, + MappingDataNode mappingDataNode => mappingDataNode.ToMappingNode(), + SequenceDataNode sequenceNode => sequenceNode.ToSequenceNode(), + _ => throw new ArgumentOutOfRangeException(nameof(node)) + }; + } + } +} diff --git a/Robust.Shared/Serialization/ObjectSerializer.cs b/Robust.Shared/Serialization/ObjectSerializer.cs deleted file mode 100644 index 803ec965d..000000000 --- a/Robust.Shared/Serialization/ObjectSerializer.cs +++ /dev/null @@ -1,407 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq.Expressions; -using System.Reflection; -using Robust.Shared.ContentPack; -using Robust.Shared.IoC; -using Robust.Shared.Reflection; - -namespace Robust.Shared.Serialization -{ - /// - /// Handles serialization of objects to/from a storage medium (which medium is implementation defined). - /// Provides methods for most common cases of data field reading/writing. - /// - /// - /// Object serialization may be "cached". - /// This is a non-guaranteed on-request case where, if the serializer is sure it's deserialized something before, - /// it can return a previous instance of the value instead of running deserialization logic again. - /// Caching only occurs for cases where data would the same, e.g. deserializing the same prototype twice. - /// Most methods that read data have a cached variant, which MAY return data shared with other objects (for reference objects). - /// This is not guaranteed and should be considered a situational optimization only. - /// It is also possible to write cached "data" fields that are not stored anywhere, but can be referenced later in other deserialization runs. - /// This is useful for complex cases like sprites which cannot be expressed with a single cached field read, but would like to still take advantage of caching. - /// The persistence of this cached data is in no way guaranteed. - /// - public abstract class ObjectSerializer - { - public const string LogCategory = "serialization"; - - public delegate void ReadFunctionDelegate(T value); - - public delegate T WriteFunctionDelegate(); - - public delegate TSource WriteConvertFunc(TTarget target) where TSource : notnull; - - public delegate TTarget ReadConvertFunc(TSource source) where TSource : notnull; - - // Must be set for Expression DataField to work to ensure no sandbox escape. - internal Type? CurrentType { get; set; } - - public void SetDeserializingType(Type type) - { - if (!IoCManager.Resolve().IsContentTypeAccessAllowed(type)) - { - throw new SandboxArgumentException("Type access for this type not allowed."); - } - - CurrentType = type; - } - - /// - /// True if this serializer is reading, false if it is writing. - /// - public bool Reading { get; protected set; } - - public bool Writing => !Reading; - - /// - /// Writes or reads a simple field by reference. - /// - /// The reference to the field that will be read/written into. - /// The name of the field in the serialization medium. Most likely the name in YAML. - /// A default value. Used if the field does not exist while reading or to know if writing would be redundant. - /// If true, always write this field to map saving, even if it matches the default. - /// The type of the field that will be read/written. - public void DataField(ref T value, string name, T defaultValue, bool alwaysWrite = false) - { - DataField(ref value, name, defaultValue, WithFormat.NoFormat, alwaysWrite); - } - - /// - /// Writes or reads a simple field by reference. - /// - /// The reference to the field that will be read/written into. - /// The name of the field in the serialization medium. Most likely the name in YAML. - /// A default value. Used if the field does not exist while reading or to know if writing would be redundant. - /// The formatter to use for representing this particular value in the medium. - /// If true, always write this field to map saving, even if it matches the default. - /// The type of the field that will be read/written. - public abstract void DataField(ref T value, string name, T defaultValue, WithFormat withFormat, - bool alwaysWrite = false); - - /// - /// Writes or reads a field or property via reflection. - /// - /// The reference to the object that has the property or field referenced. - /// The reference to the field or property that will be read/written into. - /// The name of the field in the serialization medium. Most likely the name in YAML. - /// A default value. Used if the field does not exist while reading or to know if writing would be redundant. - /// If true, always write this field to map saving, even if it matches the default. - /// The type of the object that has the property or field referenced. - /// The type of the field that will be read/written. - /// - /// - /// szr.DataField(this, x => x.SomeProperty, "some-property", SomeDefaultValue); - /// - /// - public virtual void DataField(TRoot o, Expression> expr, string name, T defaultValue, - bool alwaysWrite = false) - { - if (o == null) - { - throw new ArgumentNullException(nameof(o)); - } - - if (expr == null) - { - throw new ArgumentNullException(nameof(expr)); - } - - if (!(expr.Body is MemberExpression mExpr)) - { - throw new NotSupportedException("Cannot handle expressions of types other than MemberExpression."); - } - - if ((CurrentType == null || !o.GetType().IsAssignableTo(CurrentType)) && - !IoCManager.Resolve().IsContentTypeAccessAllowed(o.GetType())) - { - throw new SandboxArgumentException( - "Expression-based DataField requires this serializer to be correctly configured for sandboxing."); - } - - WriteFunctionDelegate getter; - ReadFunctionDelegate setter; - switch (mExpr.Member) - { - case FieldInfo fi: - getter = () => (T) fi.GetValue(o)!; - setter = v => fi.SetValue(o, v); - break; - case PropertyInfo pi: - getter = () => (T) pi.GetValue(o)!; - setter = v => pi.SetValue(o, v); - break; - default: - throw new NotSupportedException( - "Cannot handle member expressions of types other than FieldInfo or PropertyInfo."); - } - - DataReadWriteFunction(name, defaultValue, setter, getter, alwaysWrite); - } - - - /// - /// Writes or reads a simple field by reference. - /// This method can cache results and share them with other objects. - /// As such, when reading, your value may NOT be private. Do not modify it as if it's purely your own. - /// This can cut out parsing steps and memory cost for commonly used objects such as walls. - /// - /// The reference to the field that will be read/written into. - /// The name of the field in the serialization medium. Most likely the name in YAML. - /// A default value. Used if the field does not exist while reading or to know if writing would be redundant. - /// If true, always write this field to map saving, even if it matches the default. - /// The type of the field that will be read/written. - public virtual void DataFieldCached(ref T value, string name, T defaultValue, bool alwaysWrite = false) - { - DataField(ref value, name, defaultValue, WithFormat.NoFormat, alwaysWrite); - } - - /// - /// Writes or reads a simple field by reference. - /// This method can cache results and share them with other objects. - /// As such, when reading, your value may NOT be private. Do not modify it as if it's purely your own. - /// This can cut out parsing steps and memory cost for commonly used objects such as walls. - /// - /// The reference to the field that will be read/written into. - /// The name of the field in the serialization medium. Most likely the name in YAML. - /// A default value. Used if the field does not exist while reading or to know if writing would be redundant. - /// The formatter to use for representing this particular value in the medium. - /// If true, always write this field to map saving, even if it matches the default. - /// The type of the field that will be read/written. - public virtual void DataFieldCached(ref T value, string name, T defaultValue, WithFormat withFormat, - bool alwaysWrite = false) - { - DataField(ref value, name, defaultValue, withFormat, alwaysWrite); - } - - /// - /// Writes or reads a simple field by reference. - /// Runs the provided delegate to do conversion from a more easy to (de)serialize data type. - /// - /// The reference to the field that will be read/written into. - /// The name of the field in the serialization medium. Most likely the name in YAML. - /// A default value. Used if the field does not exist while reading or to know if writing would be redundant. - /// - /// A delegate invoked to convert the intermediate serialization object - /// to the actual value while reading. - /// - /// - /// A delegate invoked to convert the actual value - /// to an intermediate serialization object that will be written. - /// - /// If true, always write this field to map saving, even if it matches the default. - /// The type of the field that will be read/written. - /// The type of the intermediate object that will be (de)serialized. - public abstract void DataField( - ref TTarget value, - string name, - TTarget defaultValue, - ReadConvertFunc ReadConvertFunc, - WriteConvertFunc? WriteConvertFunc = null, - bool alwaysWrite = false - ) where TSource : notnull; - - /// - /// Writes or reads a simple field by reference. - /// This method can cache results and share them with other objects. - /// As such, when reading, your value may NOT be private. - /// This can cut out parsing steps and memory cost for commonly used objects such as walls. - /// This method can cache results and share them with other objects. - /// As such, when reading, your value may NOT be private. Do not modify it as if it's purely your own. - /// This can cut out parsing steps and memory cost for commonly used objects such as walls. - /// - /// The reference to the field that will be read/written into. - /// The name of the field in the serialization medium. Most likely the name in YAML. - /// A default value. Used if the field does not exist while reading or to know if writing would be redundant. - /// - /// A delegate invoked to convert the intermediate serialization object - /// to the actual value while reading. - /// - /// - /// A delegate invoked to convert the actual value - /// to an intermediate serialization object that will be written. - /// - /// If true, always write this field to map saving, even if it matches the default. - /// The type of the field that will be read/written. - /// The type of the intermediate object that will be (de)serialized. - public virtual void DataFieldCached( - ref TTarget value, - string name, - TTarget defaultValue, - ReadConvertFunc ReadConvertFunc, - WriteConvertFunc? WriteConvertFunc = null, - bool alwaysWrite = false - ) where TSource : notnull - { - DataField(ref value, name, defaultValue, ReadConvertFunc, WriteConvertFunc, alwaysWrite); - } - - /// - /// While reading, reads a data field and immediately returns it as value. - /// - /// The name of the field to read. - /// Default value of the field if it does not exist. - /// The type of the field. - /// The value of the field. - /// - /// Thrown if the reader is not currently reading. - /// - public abstract T ReadDataField(string name, T defaultValue); - - /// - /// While reading, reads a data field and immediately returns it as value. - /// - /// The name of the field to read. - /// The type of the field. - /// The value of the field. - /// - /// Thrown if the reader is not currently reading. - /// - /// - /// Thrown if the field does not exist. - /// - public virtual T ReadDataField(string name) - { - if (TryReadDataField(name, out var val)) - { - return val; - } - - throw new KeyNotFoundException(name); - } - - /// - /// While reading, reads a data field and immediately returns it as value. - /// This method can cache results and share them with other objects. - /// As such, when reading, your value may NOT be private. Do not modify it as if it's purely your own. - /// This can cut out parsing steps and memory cost for commonly used objects such as walls. - /// - /// The name of the field to read. - /// Default value of the field if it does not exist. - /// The type of the field. - /// The value of the field. - /// - /// Thrown if the reader is not currently reading. - /// - public virtual T ReadDataFieldCached(string name, T defaultValue) - { - return ReadDataField(name, defaultValue); - } - - /// - /// Try- pattern version of . - /// - public virtual bool TryReadDataField(string name, [MaybeNullWhen(false)] out T value) - { - return TryReadDataField(name, WithFormat.NoFormat, out value); - } - - public abstract bool TryReadDataField(string name, WithFormat format, [MaybeNullWhen(false)] out T value); - - /// - /// Try- pattern version of . - /// - public virtual bool TryReadDataFieldCached(string name, [MaybeNullWhen(false)] out T value) - { - return TryReadDataFieldCached(name, WithFormat.NoFormat, out value); - } - - public virtual bool TryReadDataFieldCached(string name, WithFormat format, - [MaybeNullWhen(false)] out T value) - { - return TryReadDataField(name, format, out value); - } - - /// - /// Sets a cached field for this serialization context. - /// This field does not get written in any way, - /// but can be recalled during other serialization runs of the same data with or . - /// - /// The cache key to write to. - /// The object to write. - public virtual void SetCacheData(string key, object value) - { - } - - /// - /// Gets cached data set by in a previous deserialization run of the same data. - /// - /// The key to recall. - /// The type to cast the return object to. - /// The data previously stored. - public virtual T GetCacheData(string key) - { - throw new NotImplementedException(); - } - - /// - /// Try- pattern version of . - /// - public virtual bool TryGetCacheData(string key, [MaybeNullWhen(false)] out T data) - { - data = default; - return false; - } - - /// - /// Provides a delegate to parse data from a more simpler type that can be mapped to mediums like YAML. - /// This is useful if your data does not map 1:1 to the prototype. - /// - /// The name of the field in the serialization medium. Most likely the name in YAML. - /// A default value to read in case the field is not exist. - /// A delegate that takes in the simpler data and is expected to set internal state on the caller. - /// The type of the data that will be read from the storage medium. - public abstract void DataReadFunction(string name, T defaultValue, ReadFunctionDelegate func); - - /// - /// Provides a delegate to write custom data to a more simpler type that can be mapped to mediums like YAML. - /// This is useful if your data does not map 1:1 to the prototype. - /// - /// The name of the field in the serialization medium. Most likely the name in YAML. - /// The default value. Used to check if writing can be skipped when is true. - /// A delegate that produces simpler data based on the internal state of the caller. - /// If true, data will always be written even if it matches . - /// The type of the data that will be written to the storage medium. - public abstract void DataWriteFunction(string name, T defaultValue, WriteFunctionDelegate func, - bool alwaysWrite = false); - - /// - /// It's and in one, so you don't need to pass name and default twice! - /// Marvelous! - /// - public virtual void DataReadWriteFunction(string name, T defaultValue, ReadFunctionDelegate readFunc, - WriteFunctionDelegate writeFunc, bool alwaysWrite = false) - { - if (Reading) - { - DataReadFunction(name, defaultValue, readFunc); - } - else - { - DataWriteFunction(name, defaultValue, writeFunc, alwaysWrite); - } - } - - /// - /// Returns a "string or enum" key value from a field. - /// These values are either a string, or an enum. - /// This is good for identifiers that can either be a string (any value, prototypes go wild), - /// or an enum when type safety is required for the code. - /// - /// The name of the field to read the key from. - /// - public virtual object ReadStringEnumKey(string fieldName) - { - var reflectionManager = IoCManager.Resolve(); - var keyString = ReadDataField(fieldName); - if (reflectionManager.TryParseEnumReference(keyString, out var @enum)) - { - return @enum; - } - - return keyString; - } - } -} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/AngleSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/AngleSerializer.cs new file mode 100644 index 000000000..06cee04e9 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/AngleSerializer.cs @@ -0,0 +1,56 @@ +using System.Globalization; +using JetBrains.Annotations; +using Robust.Shared.IoC; +using Robust.Shared.Maths; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations +{ + [TypeSerializer] + public class AngleSerializer : ITypeSerializer + { + public DeserializationResult Read(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context = null) + { + var nodeContents = node.Value; + + var angle = nodeContents.EndsWith("rad") + ? new Angle(double.Parse(nodeContents.Substring(0, nodeContents.Length - 3), + CultureInfo.InvariantCulture)) + : Angle.FromDegrees(double.Parse(nodeContents, CultureInfo.InvariantCulture)); + + return new DeserializedValue(angle); + } + + public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null) + { + var nodeValue = node.Value; + var value = nodeValue.EndsWith("rad") ? nodeValue.Substring(0, nodeValue.Length - 3) : nodeValue; + + return double.TryParse(value, out _) ? new ValidatedValueNode(node) : new ErrorNode(node, "Failed parsing angle."); + } + + public DataNode Write(ISerializationManager serializationManager, Angle value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + return new ValueDataNode($"{value.Theta.ToString(CultureInfo.InvariantCulture)} rad"); + } + + [MustUseReturnValue] + public Angle Copy(ISerializationManager serializationManager, Angle source, Angle target, + bool skipHook, + ISerializationContext? context = null) + { + return new(source); + } + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/Box2Serializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/Box2Serializer.cs new file mode 100644 index 000000000..94f4f9ee5 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/Box2Serializer.cs @@ -0,0 +1,76 @@ +using System.Globalization; +using JetBrains.Annotations; +using Robust.Shared.IoC; +using Robust.Shared.Maths; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations +{ + [TypeSerializer] + public class Box2Serializer : ITypeSerializer + { + public DeserializationResult Read(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context = null) + { + var args = node.Value.Split(','); + + if (args.Length != 4) + { + throw new InvalidMappingException($"Could not parse {nameof(Box2)}: '{node.Value}'"); + } + + var b = float.Parse(args[0], CultureInfo.InvariantCulture); + var l = float.Parse(args[1], CultureInfo.InvariantCulture); + var t = float.Parse(args[2], CultureInfo.InvariantCulture); + var r = float.Parse(args[3], CultureInfo.InvariantCulture); + + return new DeserializedValue(new Box2(l, b, r, t)); + } + + public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null) + { + var args = node.Value.Split(','); + + if (args.Length != 4) + { + return new ErrorNode(node, "Invalid amount of args for Box2."); + } + + return float.TryParse(args[0], out _) && + float.TryParse(args[1], out _) && + float.TryParse(args[2], out _) && + float.TryParse(args[3], out _) + ? new ValidatedValueNode(node) + : new ErrorNode(node, "Failed parsing values of Box2."); + } + + public DataNode Write(ISerializationManager serializationManager, Box2 value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + var nodeValue = + $"{value.Bottom.ToString(CultureInfo.InvariantCulture)}," + + $"{value.Left.ToString(CultureInfo.InvariantCulture)}," + + $"{value.Top.ToString(CultureInfo.InvariantCulture)}," + + $"{value.Right.ToString(CultureInfo.InvariantCulture)}"; + + return new ValueDataNode(nodeValue); + } + + [MustUseReturnValue] + public Box2 Copy(ISerializationManager serializationManager, Box2 source, Box2 target, + bool skipHook, + ISerializationContext? context = null) + { + return new(source.Left, source.Bottom, source.Right, source.Top); + } + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/ColorSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/ColorSerializer.cs new file mode 100644 index 000000000..69d516073 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/ColorSerializer.cs @@ -0,0 +1,51 @@ +using JetBrains.Annotations; +using Robust.Shared.IoC; +using Robust.Shared.Maths; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations +{ + [TypeSerializer] + public class ColorSerializer : ITypeSerializer + { + public DeserializationResult Read(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context = null) + { + var deserializedColor = Color.TryFromName(node.Value, out var color) + ? color : + Color.FromHex(node.Value); + + return new DeserializedValue(deserializedColor); + } + + public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null) + { + return Color.TryFromName(node.Value, out _) || Color.TryFromHex(node.Value) != null + ? new ValidatedValueNode(node) + : new ErrorNode(node, "Failed parsing Color."); + } + + public DataNode Write(ISerializationManager serializationManager, Color value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + return new ValueDataNode(value.ToHex()); + } + + [MustUseReturnValue] + public Color Copy(ISerializationManager serializationManager, Color source, Color target, + bool skipHook, + ISerializationContext? context = null) + { + return new(source.R, source.G, source.B, source.A); + } + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs new file mode 100644 index 000000000..255aa4f89 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; +using static Robust.Shared.Prototypes.EntityPrototype; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations +{ + [TypeSerializer] + public class ComponentRegistrySerializer : ITypeSerializer + { + public DeserializationResult Read(ISerializationManager serializationManager, + SequenceDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context = null) + { + var factory = dependencies.Resolve(); + var components = new ComponentRegistry(); + var mappings = new Dictionary(); + + foreach (var componentMapping in node.Sequence.Cast()) + { + string compType = ((ValueDataNode) componentMapping.GetNode("type")).Value; + // See if type exists to detect errors. + switch (factory.GetComponentAvailability(compType)) + { + case ComponentAvailability.Available: + break; + + case ComponentAvailability.Ignore: + continue; + + case ComponentAvailability.Unknown: + Logger.Error(SerializationManager.LogCategory, $"Unknown component '{compType}' in prototype!"); + continue; + } + + // Has this type already been added? + if (components.Keys.Contains(compType)) + { + Logger.Error(SerializationManager.LogCategory, $"Component of type '{compType}' defined twice in prototype!"); + continue; + } + + var copy = componentMapping.Copy()!; + copy.RemoveNode("type"); + + var type = factory.GetRegistration(compType).Type; + var read = serializationManager.ReadWithValueOrThrow(type, copy, skipHook: skipHook); + + components[compType] = read.value; + mappings.Add(DeserializationResult.Value(compType), read.result); + } + + var referenceTypes = new List(); + // Assert that there are no conflicting component references. + foreach (var componentName in components.Keys) + { + var registration = factory.GetRegistration(componentName); + foreach (var compType in registration.References) + { + if (referenceTypes.Contains(compType)) + { + throw new InvalidOperationException( + $"Duplicate component reference in prototype: '{compType}'"); + } + + referenceTypes.Add(compType); + } + } + + return new DeserializedComponentRegistry(components, mappings); + } + + public ValidationNode Validate(ISerializationManager serializationManager, + SequenceDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null) + { + var factory = dependencies.Resolve(); + var components = new ComponentRegistry(); + var list = new List(); + + foreach (var componentMapping in node.Sequence.Cast()) + { + string compType = ((ValueDataNode) componentMapping.GetNode("type")).Value; + // See if type exists to detect errors. + switch (factory.GetComponentAvailability(compType)) + { + case ComponentAvailability.Available: + break; + + case ComponentAvailability.Ignore: + list.Add(new ValidatedValueNode(componentMapping)); + continue; + + case ComponentAvailability.Unknown: + list.Add(new ErrorNode(componentMapping, "Unknown ComponentType.")); + continue; + } + + // Has this type already been added? + if (components.Keys.Contains(compType)) + { + list.Add(new ErrorNode(componentMapping, "Duplicate Component.")); + continue; + } + + var copy = componentMapping.Copy()!; + copy.RemoveNode("type"); + + var type = factory.GetRegistration(compType).Type; + + list.Add(serializationManager.ValidateNode(type, copy, context)); + } + + var referenceTypes = new List(); + + // Assert that there are no conflicting component references. + foreach (var componentName in components.Keys) + { + var registration = factory.GetRegistration(componentName); + + foreach (var compType in registration.References) + { + if (referenceTypes.Contains(compType)) + { + return new ErrorNode(node, "Duplicate ComponentReference."); + } + + referenceTypes.Add(compType); + } + } + + return new ValidatedSequenceNode(list); + } + + public DataNode Write(ISerializationManager serializationManager, ComponentRegistry value, + bool alwaysWrite = false, + ISerializationContext? context = null) + { + var compSequence = new SequenceDataNode(); + foreach (var (type, component) in value) + { + var node = serializationManager.WriteValue(component.GetType(), component, alwaysWrite, context); + if (node is not MappingDataNode mapping) throw new InvalidNodeTypeException(); +// if (mapping.Children.Count != 0) +// { + mapping.AddNode("type", new ValueDataNode(type)); + compSequence.Add(mapping); +// } + } + + return compSequence; + } + + [MustUseReturnValue] + public ComponentRegistry Copy(ISerializationManager serializationManager, ComponentRegistry source, + ComponentRegistry target, bool skipHook, ISerializationContext? context = null) + { + target.Clear(); + target.EnsureCapacity(source.Count); + + foreach (var (id, component) in source) + { + target.Add(id, serializationManager.CreateCopy(component, context)!); + } + + return target; + } + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/EntitySerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/EntitySerializer.cs new file mode 100644 index 000000000..52ff72c28 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/EntitySerializer.cs @@ -0,0 +1,51 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations +{ + [TypeSerializer] + public class EntitySerializer : ITypeReaderWriter + { + public DeserializationResult Read( + ISerializationManager serializationManager, + ValueDataNode node, + IDependencyCollection dependencies, bool skipHook, + ISerializationContext? context = null) + { + if (!EntityUid.TryParse(node.Value, out var uid) || + !uid.IsValid()) + { + throw new InvalidMappingException($"{node.Value} is not a valid entity uid."); + } + + var entity = dependencies.Resolve().GetEntity(uid); + + // TODO Paul what type to return here + return new DeserializedValue(entity); + } + + public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null) + { + // TODO Paul should we be checking entity exists here + return EntityUid.TryParse(node.Value, out var uid) && + uid.IsValid() && + dependencies.Resolve().EntityExists(uid) + ? new ValidatedValueNode(node) + : new ErrorNode(node, "Failed parsing EntityUid"); + } + + public DataNode Write(ISerializationManager serializationManager, IEntity value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + return new ValueDataNode(value.Uid.ToString()); + } + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/FormattedMessageSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/FormattedMessageSerializer.cs new file mode 100644 index 000000000..fd9ffdeeb --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/FormattedMessageSerializer.cs @@ -0,0 +1,46 @@ +using JetBrains.Annotations; +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; +using Robust.Shared.Utility; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations +{ + [TypeSerializer] + public class FormattedMessageSerializer : ITypeSerializer + { + public DeserializationResult Read(ISerializationManager serializationManager, + ValueDataNode node, IDependencyCollection dependencies, bool skipHook, + ISerializationContext? context = null) + { + return new DeserializedValue(FormattedMessage.FromMarkup(node.Value)); + } + + public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null) + { + return FormattedMessage.ValidMarkup(node.Value) + ? new ValidatedValueNode(node) + : new ErrorNode(node, "Invalid markup in FormattedMessage."); + } + + public DataNode Write(ISerializationManager serializationManager, FormattedMessage value, + bool alwaysWrite = false, + ISerializationContext? context = null) + { + return new ValueDataNode(value.ToString()); + } + + [MustUseReturnValue] + public FormattedMessage Copy(ISerializationManager serializationManager, FormattedMessage source, + FormattedMessage target, bool skipHook, ISerializationContext? context = null) + { + return new(source); + } + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/DictionarySerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/DictionarySerializer.cs new file mode 100644 index 000000000..8c581ca3e --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/DictionarySerializer.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Generic +{ + [TypeSerializer] + public class DictionarySerializer : + ITypeSerializer, MappingDataNode>, + ITypeSerializer, MappingDataNode>, + ITypeSerializer, MappingDataNode> where TKey : notnull + { + private MappingDataNode InterfaceWrite( + ISerializationManager serializationManager, + IDictionary value, + bool alwaysWrite = false, + ISerializationContext? context = null) + { + var mappingNode = new MappingDataNode(); + + foreach (var (key, val) in value) + { + mappingNode.AddNode( + serializationManager.WriteValue(key, alwaysWrite, context), + serializationManager.WriteValue(typeof(TValue), val, alwaysWrite, context)); + } + + return mappingNode; + } + + public DeserializationResult Read(ISerializationManager serializationManager, + MappingDataNode node, IDependencyCollection dependencies, bool skipHook, ISerializationContext? context) + { + var dict = new Dictionary(); + var mappedFields = new Dictionary(); + + foreach (var (key, value) in node.Children) + { + var (keyVal, keyResult) = serializationManager.ReadWithValueOrThrow(key, context, skipHook); + var (valueResult, valueVal) = serializationManager.ReadWithValueCast(typeof(TValue), value, context, skipHook); + + dict.Add(keyVal, valueVal!); + mappedFields.Add(keyResult, valueResult); + } + + return new DeserializedDictionary, TKey, TValue>(dict, mappedFields, dictInstance => dictInstance); + } + + ValidationNode ITypeReader, MappingDataNode>.Validate( + ISerializationManager serializationManager, MappingDataNode node, IDependencyCollection dependencies, + ISerializationContext? context) + { + return Validate(serializationManager, node, context); + } + + ValidationNode ITypeReader, MappingDataNode>.Validate( + ISerializationManager serializationManager, MappingDataNode node, IDependencyCollection dependencies, + ISerializationContext? context) + { + return Validate(serializationManager, node, context); + } + + ValidationNode ITypeReader, MappingDataNode>.Validate( + ISerializationManager serializationManager, + MappingDataNode node, IDependencyCollection dependencies, ISerializationContext? context) + { + return Validate(serializationManager, node, context); + } + + ValidationNode Validate(ISerializationManager serializationManager, MappingDataNode node, ISerializationContext? context) + { + var mapping = new Dictionary(); + foreach (var (key, val) in node.Children) + { + mapping.Add(serializationManager.ValidateNode(typeof(TKey), key, context), serializationManager.ValidateNode(typeof(TValue), val, context)); + } + + return new ValidatedMappingNode(mapping); + } + + public DataNode Write(ISerializationManager serializationManager, Dictionary value, + bool alwaysWrite = false, + ISerializationContext? context = null) + { + return InterfaceWrite(serializationManager, value, alwaysWrite, context); + } + + public DataNode Write(ISerializationManager serializationManager, SortedDictionary value, + bool alwaysWrite = false, + ISerializationContext? context = null) + { + return InterfaceWrite(serializationManager, value, alwaysWrite, context); + } + + public DataNode Write(ISerializationManager serializationManager, IReadOnlyDictionary value, + bool alwaysWrite = false, + ISerializationContext? context = null) + { + return InterfaceWrite(serializationManager, value.ToDictionary(k => k.Key, v => v.Value), alwaysWrite, context); + } + + DeserializationResult + ITypeReader, MappingDataNode>.Read( + ISerializationManager serializationManager, MappingDataNode node, + IDependencyCollection dependencies, + bool skipHook, ISerializationContext? context) + { + var dict = new Dictionary(); + var mappedFields = new Dictionary(); + + foreach (var (key, value) in node.Children) + { + var (keyVal, keyResult) = serializationManager.ReadWithValueOrThrow(key, context, skipHook); + var (valueResult, valueVal) = serializationManager.ReadWithValueCast(typeof(TValue), value, context, skipHook); + + dict.Add(keyVal, valueVal!); + mappedFields.Add(keyResult, valueResult); + } + + return new DeserializedDictionary, TKey, TValue>(dict, mappedFields, dictInstance => dictInstance); + } + + DeserializationResult + ITypeReader, MappingDataNode>.Read( + ISerializationManager serializationManager, MappingDataNode node, + IDependencyCollection dependencies, + bool skipHook, ISerializationContext? context) + { + var dict = new SortedDictionary(); + var mappedFields = new Dictionary(); + + foreach (var (key, value) in node.Children) + { + var (keyVal, keyResult) = serializationManager.ReadWithValueOrThrow(key, context, skipHook); + var (valueResult, valueVal) = serializationManager.ReadWithValueCast(typeof(TValue), value, context, skipHook); + + dict.Add(keyVal, valueVal!); + mappedFields.Add(keyResult, valueResult); + } + + return new DeserializedDictionary, TKey, TValue>(dict, mappedFields, dictInstance => new SortedDictionary(dictInstance)); + } + + [MustUseReturnValue] + private T CopyInternal(ISerializationManager serializationManager, IReadOnlyDictionary source, T target, ISerializationContext? context) where T : IDictionary + { + target.Clear(); + + foreach (var (key, value) in source) + { + var keyCopy = serializationManager.CreateCopy(key, context) ?? throw new NullReferenceException(); + var valueCopy = serializationManager.CreateCopy(value, context)!; + + target.Add(keyCopy, valueCopy); + } + + return target; + } + + [MustUseReturnValue] + public Dictionary Copy(ISerializationManager serializationManager, + Dictionary source, Dictionary target, + bool skipHook, ISerializationContext? context = null) + { + return CopyInternal(serializationManager, source, target, context); + } + + [MustUseReturnValue] + public IReadOnlyDictionary Copy(ISerializationManager serializationManager, + IReadOnlyDictionary source, IReadOnlyDictionary target, + bool skipHook, + ISerializationContext? context = null) + { + if (target is Dictionary targetDictionary) + { + return CopyInternal(serializationManager, source, targetDictionary, context); + } + + var dictionary = new Dictionary(source.Count); + + foreach (var (key, value) in source) + { + var keyCopy = serializationManager.CreateCopy(key, context) ?? throw new NullReferenceException(); + var valueCopy = serializationManager.CreateCopy(value, context)!; + + dictionary.Add(keyCopy, valueCopy); + } + + return dictionary; + } + + [MustUseReturnValue] + public SortedDictionary Copy(ISerializationManager serializationManager, + SortedDictionary source, SortedDictionary target, + bool skipHook, + ISerializationContext? context = null) + { + return CopyInternal(serializationManager, source, target, context); + } + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/HashSetSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/HashSetSerializer.cs new file mode 100644 index 000000000..50b826948 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/HashSetSerializer.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using JetBrains.Annotations; +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Generic +{ + [TypeSerializer] + public class HashSetSerializer : + ITypeSerializer, SequenceDataNode>, + ITypeSerializer, SequenceDataNode> + { + DeserializationResult ITypeReader, SequenceDataNode>.Read(ISerializationManager serializationManager, + SequenceDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context) + { + var set = new HashSet(); + var mappings = new List(); + + foreach (var dataNode in node.Sequence) + { + var (value, result) = serializationManager.ReadWithValueOrThrow(dataNode, context, skipHook); + + set.Add(value); + mappings.Add(result); + } + + return new DeserializedCollection, T>(set, mappings, elements => new HashSet(elements)); + } + + ValidationNode ITypeReader, SequenceDataNode>.Validate( + ISerializationManager serializationManager, + SequenceDataNode node, IDependencyCollection dependencies, ISerializationContext? context) + { + return Validate(serializationManager, node, context); + } + + ValidationNode ITypeReader, SequenceDataNode>.Validate(ISerializationManager serializationManager, + SequenceDataNode node, IDependencyCollection dependencies, ISerializationContext? context) + { + return Validate(serializationManager, node, context); + } + + ValidationNode Validate(ISerializationManager serializationManager, SequenceDataNode node, ISerializationContext? context) + { + var list = new List(); + foreach (var elem in node.Sequence) + { + list.Add(serializationManager.ValidateNode(typeof(T), elem, context)); + } + + return new ValidatedSequenceNode(list); + } + + public DataNode Write(ISerializationManager serializationManager, ImmutableHashSet value, + bool alwaysWrite = false, + ISerializationContext? context = null) + { + return Write(serializationManager, value.ToHashSet(), alwaysWrite, context); + } + + public DataNode Write(ISerializationManager serializationManager, HashSet value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + var sequence = new SequenceDataNode(); + + foreach (var elem in value) + { + sequence.Add(serializationManager.WriteValue(typeof(T), elem, alwaysWrite, context)); + } + + return sequence; + } + + DeserializationResult ITypeReader, SequenceDataNode>.Read( + ISerializationManager serializationManager, + SequenceDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context) + { + var set = ImmutableHashSet.CreateBuilder(); + var mappings = new List(); + + foreach (var dataNode in node.Sequence) + { + var (value, result) = serializationManager.ReadWithValueOrThrow(dataNode, context, skipHook); + + set.Add(value); + mappings.Add(result); + } + + return new DeserializedCollection, T>(set.ToImmutable(), mappings, elements => ImmutableHashSet.Create(elements.ToArray())); + } + + [MustUseReturnValue] + public HashSet Copy(ISerializationManager serializationManager, HashSet source, HashSet target, + bool skipHook, + ISerializationContext? context = null) + { + target.Clear(); + target.EnsureCapacity(source.Count); + + foreach (var element in source) + { + var elementCopy = serializationManager.CreateCopy(element, context) ?? throw new NullReferenceException(); + target.Add(elementCopy); + } + + return target; + } + + [MustUseReturnValue] + public ImmutableHashSet Copy(ISerializationManager serializationManager, ImmutableHashSet source, + ImmutableHashSet target, bool skipHook, ISerializationContext? context = null) + { + var builder = ImmutableHashSet.CreateBuilder(); + + foreach (var element in source) + { + var elementCopy = serializationManager.CreateCopy(element, context) ?? throw new NullReferenceException(); + builder.Add(elementCopy); + } + + return builder.ToImmutable(); + } + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/ListSerializers.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/ListSerializers.cs new file mode 100644 index 000000000..59857b3e3 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/ListSerializers.cs @@ -0,0 +1,249 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using JetBrains.Annotations; +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Generic +{ + [TypeSerializer] + public class ListSerializers : + ITypeSerializer, SequenceDataNode>, + ITypeSerializer, SequenceDataNode>, + ITypeSerializer, SequenceDataNode>, + ITypeSerializer, SequenceDataNode> + { + private DataNode WriteInternal(ISerializationManager serializationManager, IEnumerable value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + var sequence = new SequenceDataNode(); + + foreach (var elem in value) + { + sequence.Add(serializationManager.WriteValue(typeof(T), elem, alwaysWrite, context)); + } + + return sequence; + } + + public DataNode Write(ISerializationManager serializationManager, ImmutableList value, + bool alwaysWrite = false, + ISerializationContext? context = null) + { + return WriteInternal(serializationManager, value, alwaysWrite, context); + } + + public DataNode Write(ISerializationManager serializationManager, List value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + return WriteInternal(serializationManager, value, alwaysWrite, context); + } + + public DataNode Write(ISerializationManager serializationManager, IReadOnlyCollection value, + bool alwaysWrite = false, + ISerializationContext? context = null) + { + return WriteInternal(serializationManager, value, alwaysWrite, context); + } + + public DataNode Write(ISerializationManager serializationManager, IReadOnlyList value, + bool alwaysWrite = false, + ISerializationContext? context = null) + { + return WriteInternal(serializationManager, value, alwaysWrite, context); + } + + DeserializationResult ITypeReader, SequenceDataNode>.Read(ISerializationManager serializationManager, + SequenceDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context) + { + var list = new List(); + var results = new List(); + + foreach (var dataNode in node.Sequence) + { + var (value, result) = serializationManager.ReadWithValueOrThrow(typeof(T), dataNode, context, skipHook); + list.Add(value); + results.Add(result); + } + + return new DeserializedCollection, T>(list, results, elements => elements); + } + + ValidationNode ITypeReader, SequenceDataNode>.Validate( + ISerializationManager serializationManager, + SequenceDataNode node, IDependencyCollection dependencies, ISerializationContext? context) + { + return Validate(serializationManager, node, context); + } + + ValidationNode ITypeReader, SequenceDataNode>.Validate( + ISerializationManager serializationManager, + SequenceDataNode node, IDependencyCollection dependencies, ISerializationContext? context) + { + return Validate(serializationManager, node, context); + } + + ValidationNode ITypeReader, SequenceDataNode>.Validate( + ISerializationManager serializationManager, + SequenceDataNode node, IDependencyCollection dependencies, ISerializationContext? context) + { + return Validate(serializationManager, node, context); + } + + ValidationNode ITypeReader, SequenceDataNode>.Validate(ISerializationManager serializationManager, + SequenceDataNode node, IDependencyCollection dependencies, ISerializationContext? context) + { + return Validate(serializationManager, node, context); + } + + ValidationNode Validate(ISerializationManager serializationManager, SequenceDataNode sequenceDataNode, ISerializationContext? context) + { + var list = new List(); + foreach (var elem in sequenceDataNode.Sequence) + { + list.Add(serializationManager.ValidateNode(typeof(T), elem, context)); + } + + return new ValidatedSequenceNode(list); + } + + DeserializationResult ITypeReader, SequenceDataNode>.Read( + ISerializationManager serializationManager, SequenceDataNode node, + IDependencyCollection dependencies, + bool skipHook, ISerializationContext? context) + { + var list = new List(); + var results = new List(); + + foreach (var dataNode in node.Sequence) + { + var (value, result) = serializationManager.ReadWithValueOrThrow(dataNode, context, skipHook); + + list.Add(value); + results.Add(result); + } + + return new DeserializedCollection, T>(list, results, l => l); + } + + DeserializationResult ITypeReader, SequenceDataNode>.Read( + ISerializationManager serializationManager, SequenceDataNode node, + IDependencyCollection dependencies, + bool skipHook, ISerializationContext? context) + { + var list = new List(); + var results = new List(); + + foreach (var dataNode in node.Sequence) + { + var (value, result) = serializationManager.ReadWithValueOrThrow(dataNode, context, skipHook); + list.Add(value); + results.Add(result); + } + + return new DeserializedCollection, T>(list, results, l => l); + } + + DeserializationResult ITypeReader, SequenceDataNode>.Read( + ISerializationManager serializationManager, SequenceDataNode node, + IDependencyCollection dependencies, + bool skipHook, ISerializationContext? context) + { + var list = ImmutableList.CreateBuilder(); + var results = new List(); + + foreach (var dataNode in node.Sequence) + { + var (value, result) = serializationManager.ReadWithValueOrThrow(dataNode, context, skipHook); + list.Add(value); + results.Add(result); + } + + return new DeserializedCollection,T>(list.ToImmutable(), results, elements => ImmutableList.Create(elements.ToArray())); + } + + [MustUseReturnValue] + private TList CopyInternal(ISerializationManager serializationManager, IEnumerable source, TList target, ISerializationContext? context = null) where TList : IList + { + target.Clear(); + + foreach (var element in source) + { + var elementCopy = serializationManager.CreateCopy(element, context)!; + target.Add(elementCopy); + } + + return target; + } + + [MustUseReturnValue] + public List Copy(ISerializationManager serializationManager, List source, List target, + bool skipHook, + ISerializationContext? context = null) + { + return CopyInternal(serializationManager, source, target, context); + } + + [MustUseReturnValue] + public IReadOnlyList Copy(ISerializationManager serializationManager, IReadOnlyList source, + IReadOnlyList target, bool skipHook, ISerializationContext? context = null) + { + if (target is List targetList) + { + return CopyInternal(serializationManager, source, targetList); + } + + var list = new List(); + + foreach (var element in source) + { + var elementCopy = serializationManager.CreateCopy(element, context)!; + list.Add(elementCopy); + } + + return list; + } + + [MustUseReturnValue] + public IReadOnlyCollection Copy(ISerializationManager serializationManager, IReadOnlyCollection source, + IReadOnlyCollection target, bool skipHook, ISerializationContext? context = null) + { + if (target is List targetList) + { + return CopyInternal(serializationManager, source, targetList, context); + } + + var list = new List(); + + foreach (var element in source) + { + var elementCopy = serializationManager.CreateCopy(element, context)!; + list.Add(elementCopy); + } + + return list; + } + + public ImmutableList Copy(ISerializationManager serializationManager, ImmutableList source, + ImmutableList target, bool skipHook, ISerializationContext? context = null) + { + var builder = ImmutableList.CreateBuilder(); + + foreach (var element in source) + { + var elementCopy = serializationManager.CreateCopy(element, context)!; + builder.Add(elementCopy); + } + + return builder.ToImmutable(); + } + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/ValueTupleSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/ValueTupleSerializer.cs new file mode 100644 index 000000000..bdb6a40f2 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/ValueTupleSerializer.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Generic +{ + [TypeSerializer] + public class ValueTupleSerializer : ITypeSerializer, MappingDataNode> + { + public DeserializationResult Read(ISerializationManager serializationManager, MappingDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context = null) + { + if (node.Children.Count != 1) + throw new InvalidMappingException("Less than or more than 1 mappings provided to ValueTupleSerializer"); + + var entry = node.Children.First(); + var v1 = serializationManager.ReadValueOrThrow(entry.Key, context, skipHook); + var v2 = serializationManager.ReadValueOrThrow(entry.Value, context, skipHook); + + return DeserializationResult.Value(new ValueTuple(v1, v2)); + } + + public ValidationNode Validate(ISerializationManager serializationManager, MappingDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null) + { + if (node.Children.Count != 1) return new ErrorNode(node, "More or less than 1 Mapping for ValueTuple found."); + + var entry = node.Children.First(); + var dict = new Dictionary + { + { + serializationManager.ValidateNode(typeof(T1), entry.Key, context), + serializationManager.ValidateNode(typeof(T2), entry.Value, context) + } + }; + + return new ValidatedMappingNode(dict); + } + + public DataNode Write(ISerializationManager serializationManager, (T1, T2) value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + var mapping = new MappingDataNode(); + + mapping.AddNode( + serializationManager.WriteValue(typeof(T1), value.Item1, alwaysWrite, context), + serializationManager.WriteValue(typeof(T2), value.Item2, alwaysWrite, context) + ); + + return mapping; + } + + public (T1, T2) Copy(ISerializationManager serializationManager, (T1, T2) source, (T1, T2) target, + bool skipHook, + ISerializationContext? context = null) + { + return (serializationManager.Copy(source.Item1, target.Item1)!, + serializationManager.Copy(source.Item2, source.Item2)!); + } + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/MapIdSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/MapIdSerializer.cs new file mode 100644 index 000000000..1b79e748b --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/MapIdSerializer.cs @@ -0,0 +1,50 @@ +using System.Globalization; +using JetBrains.Annotations; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations +{ + [TypeSerializer] + public class MapIdSerializer : ITypeSerializer + { + public DeserializationResult Read(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context = null) + { + var val = int.Parse(node.Value, CultureInfo.InvariantCulture); + return new DeserializedValue(new MapId(val)); + } + + public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null) + { + return int.TryParse(node.Value, out _) + ? new ValidatedValueNode(node) + : new ErrorNode(node, "Failed parsing MapId"); + } + + public DataNode Write(ISerializationManager serializationManager, MapId value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + var val = (int)value; + return new ValueDataNode(val.ToString()); + } + + [MustUseReturnValue] + public MapId Copy(ISerializationManager serializationManager, MapId source, MapId target, + bool skipHook, + ISerializationContext? context = null) + { + return new(source.Value); + } + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/RegexSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/RegexSerializer.cs new file mode 100644 index 000000000..149fc8064 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/RegexSerializer.cs @@ -0,0 +1,55 @@ +using System; +using System.Text.RegularExpressions; +using JetBrains.Annotations; +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations +{ + [TypeSerializer] + public class RegexSerializer : ITypeSerializer + { + public DeserializationResult Read(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context = null) + { + return new DeserializedValue(new Regex(node.Value, RegexOptions.Compiled)); + } + + public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null) + { + try + { + _ = new Regex(node.Value); + } + catch (Exception) + { + return new ErrorNode(node, "Failed compiling regex."); + } + + return new ValidatedValueNode(node); + } + + public DataNode Write(ISerializationManager serializationManager, Regex value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + return new ValueDataNode(value.ToString()); + } + + [MustUseReturnValue] + public Regex Copy(ISerializationManager serializationManager, Regex source, Regex target, + bool skipHook, + ISerializationContext? context = null) + { + return new(source.ToString(), source.Options, source.MatchTimeout); + } + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/ResourcePathSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/ResourcePathSerializer.cs new file mode 100644 index 000000000..ed80d54ea --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/ResourcePathSerializer.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq; +using JetBrains.Annotations; +using Robust.Shared.ContentPack; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; +using Robust.Shared.Utility; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations +{ + [TypeSerializer] + public class ResourcePathSerializer : ITypeSerializer + { + public DeserializationResult Read(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context = null) + { + return new DeserializedValue(new ResourcePath(node.Value)); + } + + public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null) + { + var path = new ResourcePath(node.Value); + + if (path.Extension.Equals("rsi")) + { + path /= "meta.json"; + } + + if (!path.EnumerateSegments().First().Equals("Textures", StringComparison.InvariantCultureIgnoreCase)) + { + path = SharedSpriteComponent.TextureRoot / path; + } + + path = path.ToRootedPath(); + + try + { + return IoCManager.Resolve().ContentFileExists(path) + ? new ValidatedValueNode(node) + : new ErrorNode(node, $"File not found. ({path})"); + } + catch (Exception e) + { + return new ErrorNode(node, $"Failed parsing filepath. ({path}) ({e.Message})"); + } + } + + public DataNode Write(ISerializationManager serializationManager, ResourcePath value, + bool alwaysWrite = false, + ISerializationContext? context = null) + { + return new ValueDataNode(value.ToString()); + } + + [MustUseReturnValue] + public ResourcePath Copy(ISerializationManager serializationManager, ResourcePath source, ResourcePath target, + bool skipHook, + ISerializationContext? context = null) + { + return new(source.ToString()); + } + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/SpriteSpecifierSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/SpriteSpecifierSerializer.cs new file mode 100644 index 000000000..cb3b00ac0 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/SpriteSpecifierSerializer.cs @@ -0,0 +1,193 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; +using Robust.Shared.Utility; +using static Robust.Shared.Utility.SpriteSpecifier; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations +{ + [TypeSerializer] + public class SpriteSpecifierSerializer : + ITypeSerializer, + ITypeSerializer, + ITypeSerializer, + ITypeReader, + ITypeReader + { + DeserializationResult ITypeReader.Read(ISerializationManager serializationManager, + ValueDataNode node, + IDependencyCollection dependencies, + bool skipHook, ISerializationContext? context) + { + var path = serializationManager.ReadValueOrThrow(node, context, skipHook); + return DeserializationResult.Value(new Texture(path)); + } + + DeserializationResult ITypeReader.Read( + ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + bool skipHook, ISerializationContext? context) + { + try + { + return ((ITypeReader) this).Read(serializationManager, node, dependencies, skipHook, context); + } + catch { /* ignored */ } + + try + { + return ((ITypeReader) this).Read(serializationManager, node, dependencies, skipHook, context); + } + catch { /* ignored */ } + + throw new InvalidMappingException( + "SpriteSpecifier was neither a Texture nor an EntityPrototype but got provided a ValueDataNode"); + } + + DeserializationResult ITypeReader.Read( + ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + bool skipHook, ISerializationContext? context) + { + return DeserializationResult.Value(new EntityPrototype(node.Value)); + } + + DeserializationResult ITypeReader.Read(ISerializationManager serializationManager, + MappingDataNode node, + IDependencyCollection dependencies, + bool skipHook, ISerializationContext? context) + { + if (!node.TryGetNode("sprite", out var pathNode)) + { + throw new InvalidMappingException("Expected sprite-node"); + } + + if (!node.TryGetNode("state", out var stateNode) || stateNode is not ValueDataNode valueDataNode) + { + throw new InvalidMappingException("Expected state-node as a valuenode"); + } + + var path = serializationManager.ReadValueOrThrow(pathNode, context, skipHook); + return DeserializationResult.Value(new Rsi(path, valueDataNode.Value)); + } + + + DeserializationResult ITypeReader.Read( + ISerializationManager serializationManager, MappingDataNode node, + IDependencyCollection dependencies, + bool skipHook, ISerializationContext? context) + { + return ((ITypeReader) this).Read(serializationManager, node, dependencies, skipHook, context); + } + + ValidationNode ITypeReader.Validate(ISerializationManager serializationManager, + ValueDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context) + { + var texNode = ((ITypeReader) this).Validate(serializationManager, node, dependencies, context); + if (texNode is ErrorNode) return texNode; + + var protNode = ((ITypeReader) this).Validate(serializationManager, node, dependencies, context); + if (protNode is ErrorNode) return texNode; + + return new ValidatedValueNode(node); + } + + ValidationNode ITypeReader.Validate(ISerializationManager serializationManager, + ValueDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context) + { + //todo paul actually validate the id + return string.IsNullOrWhiteSpace(node.Value) + ? new ErrorNode(node, "Invalid entityprototypeid") + : new ValidatedValueNode(node); + } + + + ValidationNode ITypeReader.Validate(ISerializationManager serializationManager, + ValueDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context) + { + return serializationManager.ValidateNode(typeof(ResourcePath), new ValueDataNode($"{SharedSpriteComponent.TextureRoot / node.Value}"), context); + } + + ValidationNode ITypeReader.Validate( + ISerializationManager serializationManager, MappingDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context) + { + return ((ITypeReader) this).Validate(serializationManager, node, dependencies, context); + } + + ValidationNode ITypeReader.Validate(ISerializationManager serializationManager, + MappingDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context) + { + if (!node.TryGetNode("sprite", out var pathNode) || pathNode is not ValueDataNode valuePathNode) + { + return new ErrorNode(node, "Missing/Invalid sprite node"); + } + + if (!node.TryGetNode("state", out var stateNode) || stateNode is not ValueDataNode) + { + return new ErrorNode(node, "Missing/Invalid state node"); + } + + var path = serializationManager.ValidateNode(typeof(ResourcePath), + new ValueDataNode($"{SharedSpriteComponent.TextureRoot / valuePathNode.Value}"), context); + + if (path is ErrorNode) return path; + + return new ValidatedValueNode(node); + } + + public DataNode Write(ISerializationManager serializationManager, Texture value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + return serializationManager.WriteValue(value.TexturePath, alwaysWrite, context); + } + + public DataNode Write(ISerializationManager serializationManager, EntityPrototype value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + return new ValueDataNode(value.EntityPrototypeId); + } + + + public DataNode Write(ISerializationManager serializationManager, Rsi value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + var mapping = new MappingDataNode(); + mapping.AddNode("sprite", serializationManager.WriteValue(value.RsiPath)); + mapping.AddNode("state", new ValueDataNode(value.RsiState)); + return mapping; + } + + public Texture Copy(ISerializationManager serializationManager, Texture source, Texture target, bool skipHook, + ISerializationContext? context = null) + { + return new(source.TexturePath); + } + + public EntityPrototype Copy(ISerializationManager serializationManager, EntityPrototype source, EntityPrototype target, + bool skipHook, ISerializationContext? context = null) + { + return new(source.EntityPrototypeId); + } + + public Rsi Copy(ISerializationManager serializationManager, Rsi source, Rsi target, bool skipHook, + ISerializationContext? context = null) + { + return new(source.RsiPath, source.RsiState); + } + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/StringSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/StringSerializer.cs new file mode 100644 index 000000000..7b7332812 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/StringSerializer.cs @@ -0,0 +1,44 @@ +using JetBrains.Annotations; +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations +{ + [TypeSerializer] + public class StringSerializer : ITypeSerializer + { + public DeserializationResult Read(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context = null) + { + return new DeserializedValue(node.Value); + } + + public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null) + { + return new ValidatedValueNode(node); + } + + public DataNode Write(ISerializationManager serializationManager, string value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + return new ValueDataNode(value); + } + + [MustUseReturnValue] + public string Copy(ISerializationManager serializationManager, string source, string target, + bool skipHook, + ISerializationContext? context = null) + { + return source; + } + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/TimespanSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/TimespanSerializer.cs new file mode 100644 index 000000000..e598772c6 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/TimespanSerializer.cs @@ -0,0 +1,49 @@ +using System; +using System.Globalization; +using JetBrains.Annotations; +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations +{ + [TypeSerializer] + public class TimespanSerializer : ITypeSerializer + { + public DeserializationResult Read(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context = null) + { + var seconds = double.Parse(node.Value, CultureInfo.InvariantCulture); + return new DeserializedValue(TimeSpan.FromSeconds(seconds)); + } + + public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null) + { + return double.TryParse(node.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out _) + ? new ValidatedValueNode(node) + : new ErrorNode(node, "Failed parsing TimeSpan"); + } + + public DataNode Write(ISerializationManager serializationManager, TimeSpan value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + return new ValueDataNode(value.TotalSeconds.ToString(CultureInfo.InvariantCulture)); + } + + [MustUseReturnValue] + public TimeSpan Copy(ISerializationManager serializationManager, TimeSpan source, TimeSpan target, + bool skipHook, + ISerializationContext? context = null) + { + return source; + } + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/UIBox2Serializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/UIBox2Serializer.cs new file mode 100644 index 000000000..3636f1225 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/UIBox2Serializer.cs @@ -0,0 +1,74 @@ +using System.Globalization; +using JetBrains.Annotations; +using Robust.Shared.IoC; +using Robust.Shared.Maths; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations +{ + [TypeSerializer] + public class UIBox2Serializer : ITypeSerializer + { + public DeserializationResult Read(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context = null) + { + var args = node.Value.Split(','); + + if (args.Length != 4) + { + throw new InvalidMappingException($"Could not parse {nameof(UIBox2)}: '{node.Value}'"); + } + + var t = float.Parse(args[0], CultureInfo.InvariantCulture); + var l = float.Parse(args[1], CultureInfo.InvariantCulture); + var b = float.Parse(args[2], CultureInfo.InvariantCulture); + var r = float.Parse(args[3], CultureInfo.InvariantCulture); + + return new DeserializedValue(new UIBox2(l, t, r, b)); + } + + public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null) + { + string raw = node.Value; + string[] args = raw.Split(','); + + if (args.Length != 4) + { + return new ErrorNode(node, "Invalid amount of arguments for UIBox2."); + } + + return float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out _) && + float.TryParse(args[1], NumberStyles.Any, CultureInfo.InvariantCulture, out _) && + float.TryParse(args[2], NumberStyles.Any, CultureInfo.InvariantCulture, out _) && + float.TryParse(args[3], NumberStyles.Any, CultureInfo.InvariantCulture, out _) + ? new ValidatedValueNode(node) + : new ErrorNode(node, "Failed parsing values for UIBox2."); + } + + public DataNode Write(ISerializationManager serializationManager, UIBox2 value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + return new ValueDataNode($"{value.Top.ToString(CultureInfo.InvariantCulture)}," + + $"{value.Left.ToString(CultureInfo.InvariantCulture)}," + + $"{value.Bottom.ToString(CultureInfo.InvariantCulture)}," + + $"{value.Right.ToString(CultureInfo.InvariantCulture)}"); + } + + [MustUseReturnValue] + public UIBox2 Copy(ISerializationManager serializationManager, UIBox2 source, UIBox2 target, + bool skipHook, + ISerializationContext? context = null) + { + return new(source.Left, source.Top, source.Right, source.Bottom); + } + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/Vector2Serializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/Vector2Serializer.cs new file mode 100644 index 000000000..10ac6f337 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/Vector2Serializer.cs @@ -0,0 +1,68 @@ +using System.Globalization; +using Robust.Shared.IoC; +using Robust.Shared.Maths; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations +{ + [TypeSerializer] + public class Vector2Serializer : ITypeSerializer + { + public DeserializationResult Read(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context = null) + { + string raw = node.Value; + string[] args = raw.Split(','); + + if (args.Length != 2) + { + throw new InvalidMappingException($"Could not parse {nameof(Vector2)}: '{raw}'"); + } + + var x = float.Parse(args[0], CultureInfo.InvariantCulture); + var y = float.Parse(args[1], CultureInfo.InvariantCulture); + var vector = new Vector2(x, y); + + return new DeserializedValue(vector); + } + + public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null) + { + string raw = node.Value; + string[] args = raw.Split(','); + + if (args.Length != 2) + { + return new ErrorNode(node, "Invalid amount of arguments for Vector2."); + } + + return float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out _) && + float.TryParse(args[1], NumberStyles.Any, CultureInfo.InvariantCulture, out _) + ? new ValidatedValueNode(node) + : new ErrorNode(node, "Failed parsing values for Vector2."); + } + + public DataNode Write(ISerializationManager serializationManager, Vector2 value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + return new ValueDataNode($"{value.X.ToString(CultureInfo.InvariantCulture)}," + + $"{value.Y.ToString(CultureInfo.InvariantCulture)}"); + } + + public Vector2 Copy(ISerializationManager serializationManager, Vector2 source, Vector2 target, + bool skipHook, + ISerializationContext? context = null) + { + return new(source.X, source.Y); + } + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/Vector2iSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/Vector2iSerializer.cs new file mode 100644 index 000000000..b9bb00037 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/Vector2iSerializer.cs @@ -0,0 +1,68 @@ +using System.Globalization; +using Robust.Shared.IoC; +using Robust.Shared.Maths; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations +{ + [TypeSerializer] + public class Vector2iSerializer : ITypeSerializer + { + public DeserializationResult Read(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context = null) + { + string raw = node.Value; + string[] args = raw.Split(','); + + if (args.Length != 2) + { + throw new InvalidMappingException($"Could not parse {nameof(Vector2)}: '{raw}'"); + } + + var x = int.Parse(args[0], CultureInfo.InvariantCulture); + var y = int.Parse(args[1], CultureInfo.InvariantCulture); + var vector = new Vector2i(x, y); + + return new DeserializedValue(vector); + } + + public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null) + { + string raw = node.Value; + string[] args = raw.Split(','); + + if (args.Length != 2) + { + return new ErrorNode(node, "Invalid amount of arguments for Vector2i."); + } + + return int.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out _) && + int.TryParse(args[1], NumberStyles.Any, CultureInfo.InvariantCulture, out _) + ? new ValidatedValueNode(node) + : new ErrorNode(node, "Failed parsing values for Vector2i."); + } + + public DataNode Write(ISerializationManager serializationManager, Vector2i value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + return new ValueDataNode($"{value.X.ToString(CultureInfo.InvariantCulture)}," + + $"{value.Y.ToString(CultureInfo.InvariantCulture)}"); + } + + public Vector2i Copy(ISerializationManager serializationManager, Vector2i source, Vector2i target, + bool skipHook, + ISerializationContext? context = null) + { + return new(source.X, source.Y); + } + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/Vector3Serializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/Vector3Serializer.cs new file mode 100644 index 000000000..6883ef195 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/Vector3Serializer.cs @@ -0,0 +1,71 @@ +using System.Globalization; +using Robust.Shared.IoC; +using Robust.Shared.Maths; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations +{ + [TypeSerializer] + public class Vector3Serializer : ITypeSerializer + { + public DeserializationResult Read(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context = null) + { + string raw = node.Value; + string[] args = raw.Split(','); + + if (args.Length != 3) + { + throw new InvalidMappingException($"Could not parse {nameof(Vector3)}: '{raw}'"); + } + + var x = float.Parse(args[0], CultureInfo.InvariantCulture); + var y = float.Parse(args[1], CultureInfo.InvariantCulture); + var z = float.Parse(args[2], CultureInfo.InvariantCulture); + var vector = new Vector3(x, y, z); + + return new DeserializedValue(vector); + } + + public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null) + { + string raw = node.Value; + string[] args = raw.Split(','); + + if (args.Length != 3) + { + return new ErrorNode(node, "Invalid amount of arguments for Vector3."); + } + + return float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out _) && + float.TryParse(args[1], NumberStyles.Any, CultureInfo.InvariantCulture, out _) && + float.TryParse(args[2], NumberStyles.Any, CultureInfo.InvariantCulture, out _) + ? new ValidatedValueNode(node) + : new ErrorNode(node, "Failed parsing values for Vector3."); + } + + public DataNode Write(ISerializationManager serializationManager, Vector3 value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + return new ValueDataNode($"{value.X.ToString(CultureInfo.InvariantCulture)}," + + $"{value.Y.ToString(CultureInfo.InvariantCulture)}," + + $"{value.Z.ToString(CultureInfo.InvariantCulture)}"); + } + + public Vector3 Copy(ISerializationManager serializationManager, Vector3 source, Vector3 target, + bool skipHook, + ISerializationContext? context = null) + { + return new(source); + } + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/Vector4Serializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/Vector4Serializer.cs new file mode 100644 index 000000000..b94bacfcc --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/Vector4Serializer.cs @@ -0,0 +1,74 @@ +using System.Globalization; +using Robust.Shared.IoC; +using Robust.Shared.Maths; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations +{ + [TypeSerializer] + public class Vector4Serializer : ITypeSerializer + { + public DeserializationResult Read(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context = null) + { + string raw = node.Value; + string[] args = raw.Split(','); + + if (args.Length != 4) + { + throw new InvalidMappingException($"Could not parse {nameof(Vector4)}: '{raw}'"); + } + + var x = float.Parse(args[0], CultureInfo.InvariantCulture); + var y = float.Parse(args[1], CultureInfo.InvariantCulture); + var z = float.Parse(args[2], CultureInfo.InvariantCulture); + var w = float.Parse(args[3], CultureInfo.InvariantCulture); + var vector = new Vector4(x, y, z, w); + + return new DeserializedValue(vector); + } + + public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null) + { + string raw = node.Value; + string[] args = raw.Split(','); + + if (args.Length != 4) + { + return new ErrorNode(node, "Invalid amount of arguments for Vector4."); + } + + return float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out _) && + float.TryParse(args[1], NumberStyles.Any, CultureInfo.InvariantCulture, out _) && + float.TryParse(args[2], NumberStyles.Any, CultureInfo.InvariantCulture, out _) && + float.TryParse(args[3], NumberStyles.Any, CultureInfo.InvariantCulture, out _) + ? new ValidatedValueNode(node) + : new ErrorNode(node, "Failed parsing values for Vector4."); + } + + public DataNode Write(ISerializationManager serializationManager, Vector4 value, bool alwaysWrite = false, + ISerializationContext? context = null) + { + return new ValueDataNode($"{value.X.ToString(CultureInfo.InvariantCulture)}," + + $"{value.Y.ToString(CultureInfo.InvariantCulture)}," + + $"{value.Z.ToString(CultureInfo.InvariantCulture)}," + + $"{value.W.ToString(CultureInfo.InvariantCulture)}"); + } + + public Vector4 Copy(ISerializationManager serializationManager, Vector4 source, Vector4 target, + bool skipHook, + ISerializationContext? context = null) + { + return new(source); + } + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Interfaces/ITypeCopier.cs b/Robust.Shared/Serialization/TypeSerializers/Interfaces/ITypeCopier.cs new file mode 100644 index 000000000..446527872 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Interfaces/ITypeCopier.cs @@ -0,0 +1,13 @@ +using JetBrains.Annotations; +using Robust.Shared.Serialization.Manager; + +namespace Robust.Shared.Serialization.TypeSerializers.Interfaces +{ + public interface ITypeCopier + { + [MustUseReturnValue] + TType Copy(ISerializationManager serializationManager, TType source, TType target, + bool skipHook, + ISerializationContext? context = null); + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Interfaces/ITypeReader.cs b/Robust.Shared/Serialization/TypeSerializers/Interfaces/ITypeReader.cs new file mode 100644 index 000000000..4cd04e23e --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Interfaces/ITypeReader.cs @@ -0,0 +1,23 @@ +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Result; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; + +namespace Robust.Shared.Serialization.TypeSerializers.Interfaces +{ + public interface ITypeReader where TType : notnull where TNode : DataNode + { + DeserializationResult Read(ISerializationManager serializationManager, + TNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context = null); + + ValidationNode Validate( + ISerializationManager serializationManager, + TNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null); + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Interfaces/ITypeReaderWriter.cs b/Robust.Shared/Serialization/TypeSerializers/Interfaces/ITypeReaderWriter.cs new file mode 100644 index 000000000..107fbfbb5 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Interfaces/ITypeReaderWriter.cs @@ -0,0 +1,12 @@ +using Robust.Shared.Serialization.Markdown; + +namespace Robust.Shared.Serialization.TypeSerializers.Interfaces +{ + public interface ITypeReaderWriter : + ITypeReader, + ITypeWriter + where TType : notnull + where TNode : DataNode + { + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Interfaces/ITypeSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Interfaces/ITypeSerializer.cs new file mode 100644 index 000000000..b7f1b4e27 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Interfaces/ITypeSerializer.cs @@ -0,0 +1,12 @@ +using Robust.Shared.Serialization.Markdown; + +namespace Robust.Shared.Serialization.TypeSerializers.Interfaces +{ + public interface ITypeSerializer : + ITypeReaderWriter, + ITypeCopier + where TType : notnull + where TNode : DataNode + { + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Interfaces/ITypeWriter.cs b/Robust.Shared/Serialization/TypeSerializers/Interfaces/ITypeWriter.cs new file mode 100644 index 000000000..2227e84b4 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Interfaces/ITypeWriter.cs @@ -0,0 +1,11 @@ +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown; + +namespace Robust.Shared.Serialization.TypeSerializers.Interfaces +{ + public interface ITypeWriter where TType : notnull + { + DataNode Write(ISerializationManager serializationManager, TType value, bool alwaysWrite = false, + ISerializationContext? context = null); + } +} diff --git a/Robust.Shared/Serialization/WithFormat.cs b/Robust.Shared/Serialization/WithFormat.cs deleted file mode 100644 index a964a6534..000000000 --- a/Robust.Shared/Serialization/WithFormat.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Robust.Shared.Serialization -{ - /// - /// Interface for controlling custom value representation in an abstract storage medium. - /// - public abstract class WithFormat - { - /// - /// The underlying type used for representation in the storage medium. - /// - public abstract Type Format { get; } - - [return: NotNull] - public abstract T FromCustomFormat(object obj); - public abstract object ToCustomFormat(T t); // t is never null here. Promise. - - /// - /// Get the corresponding YAML serializer for the custom representation. - /// - public virtual YamlObjectSerializer.TypeSerializer GetYamlSerializer() - { - return new YamlCustomFormatSerializer(this); - } - - private class DoNothing : WithFormat - { - public override Type Format => typeof(T); - [return: NotNull] - public override T FromCustomFormat(object obj) { return (T)obj; } - public override object ToCustomFormat(T t) { return t!; } - - private readonly YamlCustomFormatSerializer _serializer; - - internal DoNothing() - { - _serializer = new YamlCustomFormatSerializer(this); - } - - public override YamlObjectSerializer.TypeSerializer GetYamlSerializer() - { - return _serializer; - } - } - - /// - /// The identity format i.e. the format which represents a value as itself. - /// - public static readonly WithFormat NoFormat = new DoNothing(); - } -} diff --git a/Robust.Shared/Serialization/YamlConstantSerializer.cs b/Robust.Shared/Serialization/YamlConstantSerializer.cs deleted file mode 100644 index ad6cb5da0..000000000 --- a/Robust.Shared/Serialization/YamlConstantSerializer.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using YamlDotNet.RepresentationModel; -using Robust.Shared.Utility; - -namespace Robust.Shared.Serialization -{ - /// - /// Serializer for converting integer to/from named constant representation in YAML. - /// - /// This serializes an integer value as a named constant, based on the values of some - /// enum type. - /// - internal class YamlConstantSerializer : YamlCustomFormatSerializer - { - private readonly Type _constantType; - private readonly WithConstantRepresentation _formatter; - - /// - /// Create a YamlConstantSerializer using the given bitflag representation type. - /// - /// - /// The enum for which the constructors will be used to represent constant values. - /// - /// - public YamlConstantSerializer(Type type, WithConstantRepresentation formatter) : base(formatter) - { - _constantType = type; - _formatter = formatter; - } - - /// - /// Turn a YAML node into an integer value of the corresponding constant. - /// - public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer objectSerializer) - { - if (!(node is YamlScalarNode scalar)) - { - throw new FormatException("Constant must be a YAML scalar."); - } - - return _formatter.FromCustomFormatText(scalar.AsString()); - } - - /// - /// Turn an integer into a YAML node with the corresponding constant name. - /// - /// - /// A string node of the constant corresponding to the given value. - /// - /// - /// Thrown if there is no corresponding constant name to the value passed in. - /// - public override YamlNode TypeToNode(object obj, YamlObjectSerializer objectSerializer) - { - return base.TypeToNode(obj, objectSerializer); - } - } - - [AttributeUsage(AttributeTargets.Enum, AllowMultiple = true, Inherited = false)] - /// - /// Attribute for marking an enum type as being the constant representation for a field. - /// - /// Some fields are arbitrary ints, but it's helpful for readability to have them be - /// named constants instead. This allows for that. - /// - /// NB: AllowMultiple is true - don't assume the same representation cannot - /// be reused between multiple fields. - /// - public class ConstantsForAttribute : Attribute - { - private readonly Type _tag; - public Type Tag => _tag; - - // NB: This is not generic because C# does not allow generic attributes - - /// - /// An attribute with tag type - /// - /// - /// An arbitrary tag type used for coordinating between the data field and the - /// representation. Not actually used for serialization/deserialization. - /// - public ConstantsForAttribute(Type tag) - { - _tag = tag; - } - } -} diff --git a/Robust.Shared/Serialization/YamlCustomFormatSerializer.cs b/Robust.Shared/Serialization/YamlCustomFormatSerializer.cs deleted file mode 100644 index fbad1feaf..000000000 --- a/Robust.Shared/Serialization/YamlCustomFormatSerializer.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Robust.Shared.IoC; -using System; -using YamlDotNet.RepresentationModel; - -namespace Robust.Shared.Serialization -{ - /// - /// Helper for doing custom value representation in YAML. - /// - /// The type for which this gives a custom representation. - public class YamlCustomFormatSerializer : YamlObjectSerializer.TypeSerializer - { - private readonly WithFormat _formatter; - - public YamlCustomFormatSerializer(WithFormat formatter) - { - _formatter = formatter; - } - - public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer) - { - return _formatter.FromCustomFormat(serializer.NodeToType(_formatter.Format, node)); - } - - public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer) - { - var t = (T)obj; - return serializer.TypeToNode(_formatter.ToCustomFormat(t)); - } - } - - /// - /// Convenience class for static access to custom formatters. - /// - public static class WithFormat - { - public static WithFormat Flags() - { - return IoCManager.Resolve().FlagFormat(); - } - - public static WithFormat Constants() - { - return IoCManager.Resolve().ConstantFormat(); - } - } -} diff --git a/Robust.Shared/Serialization/YamlFlagSerializer.cs b/Robust.Shared/Serialization/YamlFlagSerializer.cs deleted file mode 100644 index ffebc8cb2..000000000 --- a/Robust.Shared/Serialization/YamlFlagSerializer.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using System.Collections.Generic; -using YamlDotNet.RepresentationModel; - -namespace Robust.Shared.Serialization -{ - /// - /// Serializer for converting integer to/from named bitflag representation in YAML. - /// - /// This allows for serializing/deserializing integer values as list of bitflag - /// names, to give a readable YAML representation. It also supports plain integer - /// values in the YAML for reading. - /// - internal class YamlFlagSerializer : YamlCustomFormatSerializer - { - private readonly Type _flagType; - - /// - /// Create a YamlFlagSerializer using the given bitflag representation type. - /// - /// - /// The bitflag enum for which the constructors will be used to represent - /// bits being set in the integer value. - /// - /// - /// Thrown if the bitflag type is not a enum, or does not have the - /// attribute. - /// - public YamlFlagSerializer(Type type, WithFormat formatter) : base(formatter) - { - _flagType = type; - } - - /// - /// Turn a YAML node into an integer value with the correct flags set. - /// - public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer objectSerializer) - { - // Fallback to just a number, if it's not in flag format yet - // This is a hack, but it's only for legacy representations, so it's not so bad - if (node is YamlScalarNode) - { - return (int)objectSerializer.NodeToType(typeof(int), node); - } - - return base.NodeToType(type, node, objectSerializer); - } - - /// - /// Turn bitflags into a YAML node with the corresponding constructors. - /// - /// - /// A sequence node of the flag names if the flags are non-zero, or the - /// sequence node of the zero flag name if it is defined on the representation - /// type. Otherwise, the scalar node 0. - /// - /// - /// Thrown if the serializer encounters a bit set in a position for which it - /// cannot find a corresponding constructor. - /// - public override YamlNode TypeToNode(object obj, YamlObjectSerializer objectSerializer) - { - var flags = (int)obj; - - if (flags == 0) - { - var zeroName = Enum.GetName(_flagType, 0); - - // If there's no name for 0, just write 0 - if (zeroName == null) - { - return objectSerializer.TypeToNode(0); - } else { - return objectSerializer.TypeToNode(new List { zeroName }); - } - - } else { - return base.TypeToNode(flags, objectSerializer); - } - } - } - - /// - /// Attribute for marking an enum type as being the bitflag representation for a field. - /// - /// Some int values in the engine are bitflags, but the actual bitflag definitions - /// are reserved for the content layer. This means that serialization/deserialization - /// of those flags into readable YAML is impossible, unless the engine is notified - /// that a certain representation should be used. That's the role of this attribute. - /// - /// NB: AllowMultiple is true - don't assume the same representation cannot - /// be reused between multiple fields. - /// - [AttributeUsage(AttributeTargets.Enum, AllowMultiple = true, Inherited = false)] - public class FlagsForAttribute : Attribute - { - private readonly Type _tag; - public Type Tag => _tag; - - // NB: This is not generic because C# does not allow generic attributes - - /// - /// An attribute with tag type - /// - /// - /// An arbitrary tag type used for coordinating between the data field and the - /// representation. Not actually used for serialization/deserialization. - /// - public FlagsForAttribute(Type tag) - { - _tag = tag; - } - } -} diff --git a/Robust.Shared/Serialization/YamlObjectSerializer.cs b/Robust.Shared/Serialization/YamlObjectSerializer.cs deleted file mode 100644 index 8f99fe954..000000000 --- a/Robust.Shared/Serialization/YamlObjectSerializer.cs +++ /dev/null @@ -1,1504 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Reflection; -using Robust.Shared.IoC; -using Robust.Shared.Log; -using Robust.Shared.Map; -using Robust.Shared.Maths; -using Robust.Shared.Reflection; -using Robust.Shared.Utility; -using YamlDotNet.Core; -using YamlDotNet.RepresentationModel; - -namespace Robust.Shared.Serialization -{ - /// - /// Object serializer that serializes to/from YAML. - /// - public class YamlObjectSerializer : ObjectSerializer - { - private const string TagSkipTag = "SRZ_DO_NOT_TAG"; - - private static readonly Dictionary _typeSerializers; - public static IReadOnlyDictionary TypeSerializers => _typeSerializers; - private static readonly StructSerializer _structSerializer; - - private YamlMappingNode? WriteMap; - private List? ReadMaps; - private Context? _context; - - static YamlObjectSerializer() - { - _structSerializer = new StructSerializer(); - _typeSerializers = new Dictionary - { - { typeof(Color), new ColorSerializer() }, - { typeof(Vector2), new Vector2Serializer() }, - { typeof(Vector2i), new Vector2iSerializer() }, - { typeof(Vector3), new Vector3Serializer() }, - { typeof(Vector4), new Vector4Serializer() }, - { typeof(Angle), new AngleSerializer() }, - { typeof(UIBox2), new UIBox2Serializer() }, - { typeof(Box2), new Box2Serializer() }, - { typeof(ResourcePath), new ResourcePathSerializer() }, - { typeof(GridId), new GridIdSerializer() }, - { typeof(MapId), new MapIdSerializer() }, - { typeof(SpriteSpecifier), new SpriteSpecifierSerializer() }, - { typeof(TimeSpan), new TimeSpanSerializer() }, - }; - } - - // Use NewReader or NewWriter instead. - private YamlObjectSerializer() - { - } - - /// - /// Creates a new serializer to be used for reading from YAML data. - /// - /// - /// The YAML mapping to read data from. - /// - /// - /// An optional context that can provide additional capabitilies such as caching and custom type serializers. - /// - public static YamlObjectSerializer NewReader(YamlMappingNode readMap, Context? context = null) - { - return NewReader(new List(1) { readMap }, context); - } - - public static YamlObjectSerializer NewReader(YamlMappingNode readMap, Type deserializingType, - Context? context = null) - { - var r = NewReader(readMap, context); - r.SetDeserializingType(deserializingType); - return r; - } - - /// - /// Creates a new serializer to be used for reading from YAML data. - /// - /// - /// A list of maps to read from. The first list will be used first, - /// then the second if the first does not contain a specific key, and so on. - /// - /// - /// An optional context that can provide additional capabilities such as caching and custom type serializers. - /// - public static YamlObjectSerializer NewReader(List readMaps, Context? context = null) - { - return new() - { - ReadMaps = readMaps, - _context = context, - Reading = true, - }; - } - - /// - /// Creates a new serializer to be used from writing into YAML data. - /// - /// - /// The mapping to write into. - /// Gets modified directly in place. - /// - /// - /// An optional context that can provide additional capabitilies such as caching and custom type serializers. - /// - public static YamlObjectSerializer NewWriter(YamlMappingNode writeMap, Context? context = null) - { - return new() - { - WriteMap = writeMap, - _context = context, - Reading = false, - }; - } - - // TODO: Theoretical optimization. - // Might be a good idea to make DataField use caching for value types without references too. - /// - public override void DataField(ref T value, string name, T defaultValue, WithFormat format, bool alwaysWrite = false) - { - if (Reading) // read - { - foreach (var map in ReadMaps!) - { - if (map.TryGetNode(name, out var node)) - { - var customFormatter = format.GetYamlSerializer(); - value = (T)customFormatter.NodeToType(typeof(T), node, this); - return; - } - } - value = defaultValue; - return; - } - else // write - { - // don't write if value is null or default - if (!alwaysWrite && IsValueDefault(name, value, defaultValue, format)) - return; - - // if value AND defaultValue are null then IsValueDefault above will abort. - var customFormatter = format.GetYamlSerializer(); - var key = name; - var val = value == null ? customFormatter.TypeToNode(defaultValue!, this) : customFormatter.TypeToNode(value, this); - - // write the concrete type tag - AssignTag(typeof(T), value, defaultValue, val); - - WriteMap!.Add(key, val); - } - } - - private static void AssignTag(Type t, T value, T defaultValue, YamlNode node) - { - // This TagSkipTag thing is a hack - // to get the serializer to SKIP writing type-tags - // for some special cases like serializing IReadOnlyList. - // If the skip tag is set, we clear it and don't set a tag. - if (node.Tag == TagSkipTag) - { - node.Tag = null; - return; - } - - if (t.IsAbstract || t.IsInterface) - { - var concreteType = value == null ? defaultValue!.GetType() : value.GetType(); - node.Tag = $"!type:{concreteType.Name}"; - } - } - - /// - public override void DataFieldCached(ref T value, string name, T defaultValue, WithFormat format, bool alwaysWrite = false) - { - if (Reading) // read - { - if (_context != null && _context.TryGetCachedField(name, out var theValue)) - { - // Itermediate field so value doesn't get reset to default(T) if this fails. - value = theValue; - return; - } - foreach (var map in ReadMaps!) - { - if (map.TryGetNode(name, out var node)) - { - var customFormatter = format.GetYamlSerializer(); - value = (T)customFormatter.NodeToType(typeof(T), node, this); - _context?.SetCachedField(name, value); - return; - } - } - value = defaultValue; - _context?.SetCachedField(name, value); - return; - } - else // write - { - DataField(ref value, name, defaultValue, format, alwaysWrite); - } - } - - /// - public override void DataField( - ref TTarget value, - string name, - TTarget defaultValue, - ReadConvertFunc ReadConvertFunc, - WriteConvertFunc? WriteConvertFunc = null, - bool alwaysWrite = false) - { - if (Reading) - { - foreach (var map in ReadMaps!) - { - if (map.TryGetNode(name, out var node)) - { - value = ReadConvertFunc((TSource)NodeToType(typeof(TSource), node)); - return; - } - } - value = defaultValue; - } - else - { - if (WriteConvertFunc == null) - { - // TODO: More verbosity diagnostics. - Logger.WarningS(LogCategory, "Field '{0}' not written due to lack of WriteConvertFunc.", name); - return; - } - - // don't write if value is null or default - if (!alwaysWrite && IsValueDefault(name, value, defaultValue, WithFormat.NoFormat)) - { - return; - } - - var key = name; - var val = value == null ? TypeToNode(WriteConvertFunc(defaultValue!)) : TypeToNode(WriteConvertFunc(value!)); - - // write the concrete type tag - AssignTag(typeof(TTarget), value, defaultValue, val); - - WriteMap!.Add(key, val); - } - } - - /// - public override void DataFieldCached( - ref TTarget value, - string name, - TTarget defaultValue, - ReadConvertFunc ReadConvertFunc, - WriteConvertFunc? WriteConvertFunc = null, - bool alwaysWrite = false) - { - if (Reading) - { - if (_context != null && _context.TryGetCachedField(name, out var theValue)) - { - // Itermediate field so value doesn't get reset to default(T) if this fails. - value = theValue; - return; - } - foreach (var map in ReadMaps!) - { - if (map.TryGetNode(name, out var node)) - { - value = ReadConvertFunc((TSource)NodeToType(typeof(TSource), node)); - _context?.SetCachedField(name, value); - return; - } - } - value = defaultValue; - _context?.SetCachedField(name, value); - } - else - { - DataField(ref value, name, defaultValue, ReadConvertFunc, WriteConvertFunc, alwaysWrite); - } - } - - /// - public override T ReadDataField(string name, T defaultValue) - { - if (!Reading) - { - throw new InvalidOperationException("Cannot use ReadDataField while not reading."); - } - - foreach (var map in ReadMaps!) - { - if (map.TryGetNode(name, out var node)) - { - return (T)NodeToType(typeof(T), node); - - } - } - return defaultValue; - } - - /// - public override T ReadDataFieldCached(string name, T defaultValue) - { - if (!Reading) - { - throw new InvalidOperationException("Cannot use ReadDataField while not reading."); - } - - if (_context != null && _context.TryGetCachedField(name, out var val)) - { - return val; - } - - foreach (var map in ReadMaps!) - { - if (map.TryGetNode(name, out var node)) - { - val = (T)NodeToType(typeof(T), node); - _context?.SetCachedField(name, val); - return val; - } - } - _context?.SetCachedField(name, defaultValue); - return defaultValue; - } - - /// - public override bool TryReadDataField(string name, WithFormat format, [MaybeNullWhen(false)] out T value) - { - if (!Reading) - { - throw new InvalidOperationException("Cannot use ReadDataField while not reading."); - } - - foreach (var map in ReadMaps!) - { - if (map.TryGetNode(name, out var node)) - { - var customFormatter = format.GetYamlSerializer(); - value = (T)customFormatter.NodeToType(typeof(T), node, this); - return true; - } - } - value = default; - return false; - } - - public override bool TryReadDataFieldCached(string name, WithFormat format, [MaybeNullWhen(false)] out T value) - { - if (!Reading) - { - throw new InvalidOperationException("Cannot use ReadDataField while not reading."); - } - - if (_context != null && _context.TryGetCachedField(name, out value)) - { - return true; - } - - foreach (var map in ReadMaps!) - { - if (map.TryGetNode(name, out var node)) - { - var customFormatter = format.GetYamlSerializer(); - value = (T)customFormatter.NodeToType(typeof(T), node, this); - _context?.SetCachedField(name, value); - return true; - } - } - value = default; - return false; - } - - - /// - public override void DataReadFunction(string name, T defaultValue, ReadFunctionDelegate func) - { - if (!Reading) return; - - foreach (var map in ReadMaps!) - { - if (map.TryGetNode(name, out var node)) - { - func((T)NodeToType(typeof(T), node)); - return; - } - } - - func(defaultValue); - } - - /// - public override void DataWriteFunction(string name, T defaultValue, WriteFunctionDelegate func, bool alwaysWrite = false) - { - if (Reading) return; - - var value = func.Invoke(); - - // don't write if value is null or default - if (!alwaysWrite && IsValueDefault(name, value, defaultValue, WithFormat.NoFormat)) - return; - - var key = name; - var val = value == null ? TypeToNode(defaultValue!) : TypeToNode(value); - - // write the concrete type tag - AssignTag(typeof(T), value, defaultValue, val); - - WriteMap!.Add(key, val); - } - - /// - public override void SetCacheData(string key, object value) - { - _context?.SetDataCache(key, value); - } - - /// - public override T GetCacheData(string key) - { - if (_context != null && _context.TryGetDataCache(key, out var value)) - { - return (T) value!; - } - throw new KeyNotFoundException(); - } - - /// - public override bool TryGetCacheData(string key, [MaybeNullWhen(false)] out T data) - { - if (_context != null && _context.TryGetDataCache(key, out var value)) - { - data = (T) value!; - return true; - } - - data = default; - return false; - } - - public object NodeToType(Type type, YamlNode node) - { - var underlyingType = Nullable.GetUnderlyingType(type) ?? type; - - // special snowflake string - if (type == typeof(String)) - return node.ToString(); - - // val primitives - if (underlyingType.IsPrimitive || underlyingType == typeof(decimal)) - { - return StringToType(type, node.ToString()); - } - - // array - if (type.IsArray) - { - var listNode = (YamlSequenceNode)node; - var newArray = (Array)Activator.CreateInstance(type, listNode.Children.Count)!; - - var idx = 0; - foreach (var entryNode in listNode) - { - var value = NodeToType(type.GetElementType()!, entryNode); - newArray.SetValue(value, idx++); - } - - return newArray; - } - - // val enum - if (underlyingType.IsEnum) - return Enum.Parse(underlyingType, node.ToString()); - - // IReadOnlyList/IReadOnlyCollection - if (TryGenericReadOnlyCollectionType(type, out var collectionType)) - { - var listNode = (YamlSequenceNode)node; - var elems = listNode.Children; - // Deserialize to an array because that is much more efficient, and allowed. - var newList = (IList)Array.CreateInstance(collectionType, elems.Count); - - for (var i = 0; i < elems.Count; i++) - { - newList[i] = NodeToType(collectionType, elems[i]); - } - - return newList; - } - - // List - if (TryGenericListType(type, out var listType)) - { - var listNode = (YamlSequenceNode)node; - var newList = (IList)Activator.CreateInstance(type, listNode.Children.Count)!; - - foreach (var entryNode in listNode) - { - var value = NodeToType(listType, entryNode); - newList.Add(value); - } - - return newList; - } - - if (TryGenericImmutableListType(type, out var immutableListType)) - { - var listNode = (YamlSequenceNode) node; - var elems = listNode.Children; - - var newList = Array.CreateInstance(immutableListType, elems.Count); - - for (var i = 0; i < elems.Count; i++) - { - newList.SetValue(NodeToType(immutableListType, elems[i]), i); - } - - var list = typeof(ImmutableList); - var add = list.GetMethod("CreateRange")!.MakeGenericMethod(immutableListType); - - return add.Invoke(null, new object?[] {newList})!; - } - - // Dictionary/IReadOnlyDictionary - if (TryGenericReadDictType(type, out var keyType, out var valType, out var dictType)) - { - var dictNode = (YamlMappingNode)node; - var newDict = (IDictionary)Activator.CreateInstance(dictType, dictNode.Children.Count)!; - - foreach (var kvEntry in dictNode.Children) - { - var keyValue = NodeToType(keyType, kvEntry.Key); - var valValue = NodeToType(valType, kvEntry.Value); - - newDict.Add(keyValue, valValue); - } - - return newDict; - } - - // HashSet - if (TryGenericHashSetType(type, out var setType)) - { - var nodes = ((YamlSequenceNode) node).Children; - var valuesArray = Array.CreateInstance(setType, new[] {nodes.Count})!; - - for (var i = 0; i < nodes.Count; i++) - { - var value = NodeToType(setType, nodes[i]); - valuesArray.SetValue(value, i); - } - - var newSet = Activator.CreateInstance(type, valuesArray)!; - - return newSet; - } - - // KeyValuePair - if (TryGenericKeyValuePairType(type, out var kvpKeyType, out var kvpValType)) - { - var pairType = typeof(KeyValuePair<,>).MakeGenericType(kvpKeyType, kvpValType); - var pairNode = (YamlMappingNode) node; - - switch (pairNode.Children.Count) - { - case 0: - return Activator.CreateInstance(pairType)!; - case 1: - { - using var enumerator = pairNode.GetEnumerator(); - enumerator.MoveNext(); - var yamlPair = enumerator.Current; - var keyValue = NodeToType(kvpKeyType, yamlPair.Key); - var valValue = NodeToType(kvpValType, yamlPair.Value); - var pair = Activator.CreateInstance(pairType, keyValue, valValue)!; - - return pair; - } - default: - throw new InvalidOperationException( - $"Cannot read KeyValuePair from mapping node with more than one child."); - } - } - - // Hand it to the context. - if (_context != null && _context.TryNodeToType(node, type, out var contextObj)) - { - return contextObj; - } - - // custom TypeSerializer - if (_typeSerializers.TryGetValue(type, out var serializer)) - return serializer.NodeToType(type, node, this); - - // IExposeData. - if (typeof(IExposeData).IsAssignableFrom(type)) - { - var concreteType = type; - - if (type.IsAbstract || type.IsInterface) - { - var tag = node.Tag; - if (string.IsNullOrWhiteSpace(tag)) - throw new YamlException($"Type '{type}' is abstract, but there is no yaml tag for the concrete type."); - - if (tag.StartsWith("!type:")) - { - concreteType = ResolveConcreteType(type, tag["!type:".Length..]); - } - else - { - throw new YamlException("Malformed type tag."); - } - } - - var instance = (IExposeData)Activator.CreateInstance(concreteType)!; - - if (node is not YamlMappingNode mapNode) - { - return instance; - } - - // TODO: Might be worth it to cut down on allocations here by using ourselves instead of creating a fork. - // Seems doable. - if (_context != null) - { - _context.StackDepth++; - } - var fork = NewReader(mapNode, _context); - fork.CurrentType = concreteType; - if (_context != null) - { - _context.StackDepth--; - } - instance.ExposeData(fork); - return instance; - } - - // ISelfSerialize - if (typeof(ISelfSerialize).IsAssignableFrom(type)) - { - var instance = (ISelfSerialize)Activator.CreateInstance(type)!; - instance.Deserialize(node.ToString()); - return instance; - } - - // other val (struct) - if (type.IsValueType) - return _structSerializer.NodeToType(type, (YamlMappingNode)node, this); - - // ref type that isn't a custom TypeSerializer - throw new ArgumentException($"Type {type.FullName} is not supported.", nameof(type)); - } - - public T NodeToType(YamlNode node, string name) - { - return (T) NodeToType(typeof(T), node); - } - - private static Type ResolveConcreteType(Type baseType, string typeName) - { - var reflection = IoCManager.Resolve(); - var type = reflection.YamlTypeTagLookup(baseType, typeName); - if (type == null) - { - throw new YamlException($"Type '{baseType}' is abstract, but could not find concrete type '{typeName}'."); - } - - return type; - } - - public YamlNode TypeToNode(object obj) - { - // special snowflake string - if (obj is string s) - return s; - - var type = obj.GetType(); - type = Nullable.GetUnderlyingType(type) ?? type; - - // val primitives and val enums - if (type.IsPrimitive || type.IsEnum || type == typeof(decimal)) - { - // All primitives and enums implement IConvertible. - // Need it for the culture overload. - var convertible = (IConvertible) obj; - return convertible.ToString(CultureInfo.InvariantCulture); - } - - // array - if (type.IsArray) - { - var sequence = new YamlSequenceNode(); - var element = type.GetElementType()!; - - foreach (var entry in (IEnumerable) obj) - { - if (entry == null) - { - continue; - throw new ArgumentException("Cannot serialize null value inside array."); - } - - var entryNode = TypeToNode(entry); - - // write the concrete type tag - AssignTag(element, entry, null, entryNode); - - sequence.Add(entryNode); - } - - return sequence; - } - - // List/IReadOnlyCollection/IReadOnlyList - if (TryGenericListType(type, out var listType) || TryGenericReadOnlyCollectionType(type, out listType)) - { - var node = new YamlSequenceNode(); - node.Tag = TagSkipTag; - - foreach (var entry in (IEnumerable)obj) - { - if (entry == null) - { - throw new ArgumentException("Cannot serialize null value inside list."); - } - - var entryNode = TypeToNode(entry); - - // write the concrete type tag - AssignTag(listType, entry, null, entryNode); - - node.Add(entryNode); - } - - return node; - } - - if (TryGenericImmutableListType(type, out var immutableListType)) - { - var node = new YamlSequenceNode {Tag = TagSkipTag}; - - foreach (var entry in (IEnumerable) obj) - { - if (entry == null) - { - throw new ArgumentException("Cannot serialize null value inside list."); - } - - var entryNode = TypeToNode(entry); - - // write the concrete type tag - AssignTag(immutableListType, entry, null, entryNode); - - node.Add(entryNode); - } - - return node; - } - - // Dictionary - if (TryGenericDictType(type, out var keyType, out var valType) - || TryGenericReadOnlyDictType(type, out keyType, out valType)) - { - var node = new YamlMappingNode(); - node.Tag = TagSkipTag; - - foreach (var oEntry in (IDictionary)obj) - { - var entry = (DictionaryEntry) oEntry!; - var keyNode = TypeToNode(entry.Key); - if (entry.Value == null) - { - throw new ArgumentException("Cannot serialize null value inside dictionary."); - } - - var valNode = TypeToNode(entry.Value); - - // write the concrete type tag - AssignTag(valType, entry.Value, null, valNode); - - node.Add(keyNode, valNode); - } - - return node; - } - - // HashSet - if (TryGenericHashSetType(type, out var setType)) - { - var node = new YamlSequenceNode(); - node.Tag = TagSkipTag; - - foreach (var entry in (IEnumerable)obj) - { - if (entry == null) - { - throw new ArgumentException("Cannot serialize null value inside hashset."); - } - - var entryNode = TypeToNode(entry); - - // write the concrete type tag - AssignTag(setType, entry, null, entryNode); - - node.Add(entryNode); - } - - return node; - } - - if (TryGenericKeyValuePairType(type, out var kvpKeyType, out var kvpValType)) - { - var node = new YamlMappingNode {Tag = TagSkipTag}; - dynamic pair = obj; - var keyNode = TypeToNode(pair.Key); - var valNode = TypeToNode(pair.Value); - - // write the concrete type tag - AssignTag(kvpValType, pair, null, valNode); - - node.Add(keyNode, valNode); - - return node; - } - - // Hand it to the context. - if (_context != null && _context.TryTypeToNode(obj, out var contextNode)) - { - return contextNode; - } - - // custom TypeSerializer - if (_typeSerializers.TryGetValue(type, out var serializer)) - return serializer.TypeToNode(obj, this); - - // IExposeData. - if (obj is IExposeData exposable) - { - var mapping = new YamlMappingNode(); - if (_context != null) - { - _context.StackDepth++; - } - var fork = NewWriter(mapping, _context); - fork.CurrentType = type; - if (_context != null) - { - _context.StackDepth--; - } - exposable.ExposeData(fork); - return mapping; - } - - // ISelfSerialize - if (typeof(ISelfSerialize).IsAssignableFrom(type)) - { - var instance = (ISelfSerialize) obj!; - return instance.Serialize(); - } - - // other val (struct) - if (type.IsValueType) - return _structSerializer.TypeToNode(obj, this); - - // ref type that isn't a custom TypeSerializer - throw new ArgumentException($"Type {type.FullName} is not supported.", nameof(obj)); - } - - bool IsValueDefault(string field, T value, T providedDefault, WithFormat format) - { - if ((value != null || providedDefault == null) && (value == null || IsSerializedEqual(value, providedDefault))) - { - return true; - } - - if (_context != null) - { - return _context.IsValueDefault(field, value, format); - } - - return false; - - } - - internal static bool IsSerializedEqual(object? a, object? b) - { - var type = a?.GetType(); - if (type != b?.GetType()) - { - return false; - } - - if (a == null) // Also implies b is null since it'd have failed the type equality check otherwise. - { - return true; - } - - if (TryGenericListType(type!, out _) || - TryGenericImmutableListType(type!, out _)) - { - var listA = (IList) a; - var listB = (IList) b!; - - if (listA.Count != listB.Count) - { - return false; - } - - for (var i = 0; i < listA.Count; i++) - { - var elemA = listA[i]; - var elemB = listB[i]; - - if (!IsSerializedEqual(elemA, elemB)) - { - return false; - } - } - - return true; - } - - if (TryGenericDictType(type!, out _, out _)) - { - var dictA = (IDictionary) a; - var dictB = (IDictionary) b!; - - foreach (var entryMaybe in dictA) - { - var entry = (DictionaryEntry) entryMaybe!; - var k = entry.Key; - var v = entry.Value; - - if (!dictB.Contains(k)) - { - return false; - } - - if (!IsSerializedEqual(v, dictB[k])) - { - return false; - } - } - - return true; - } - - if (TryGenericHashSetType(type!, out _)) - { - var setA = ((IEnumerable) a).GetEnumerator(); - var setB = ((IEnumerable) b!).GetEnumerator(); - - while (setA.MoveNext()) - { - if (!setB.MoveNext()) - { - return false; - } - - if (!IsSerializedEqual(setA.Current, setB.Current)) - { - return false; - } - } - - if (setB.MoveNext()) - { - return false; - } - - return true; - } - - if (TryGenericKeyValuePairType(type!, out _, out _)) - { - dynamic tupleA = a; - dynamic tupleB = b!; - - if (!IsSerializedEqual(tupleA.Key, tupleB.Key)) - { - return false; - } - - if (!IsSerializedEqual(tupleA.Value, tupleB.Value)) - { - return false; - } - - return true; - } - - if (typeof(IExposeData).IsAssignableFrom(type)) - { - // Serialize both, see if output matches. - var testA = new YamlMappingNode(); - var testB = new YamlMappingNode(); - var serA = NewWriter(testA); - var serB = NewWriter(testB); - - var expA = (IExposeData) a; - var expB = (IExposeData) b!; - - expA.ExposeData(serA); - expB.ExposeData(serB); - - // Does deep equality. - return testA.Equals(testB); - } - - return a.Equals(b); - } - - - private static object StringToType(Type type, string str) - { - var foo = TypeDescriptor.GetConverter(type); - return foo.ConvertFromInvariantString(str); - } - - public static void RegisterTypeSerializer(Type type, TypeSerializer serializer) - { - if (!_typeSerializers.ContainsKey(type)) - _typeSerializers.Add(type, serializer); - } - - private static bool TryGenericReadOnlyCollectionType(Type type, [NotNullWhen(true)] out Type? listType) - { - if (!type.GetTypeInfo().IsGenericType) - { - listType = default; - return false; - } - - var baseGeneric = type.GetGenericTypeDefinition(); - var isList = baseGeneric == typeof(IReadOnlyCollection<>) || baseGeneric == typeof(IReadOnlyList<>); - - if (isList) - { - listType = type.GetGenericArguments()[0]; - return true; - } - - listType = default; - return false; - } - - private static bool TryGenericListType(Type type, [NotNullWhen(true)] out Type? listType) - { - var isList = type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>); - - if (isList) - { - listType = type.GetGenericArguments()[0]; - return true; - } - - listType = default; - return false; - } - - private static bool TryGenericImmutableListType(Type type, [NotNullWhen(true)] out Type? listType) - { - var isImmutableList = type.GetTypeInfo().IsGenericType && - type.GetGenericTypeDefinition() == typeof(ImmutableList<>); - - if (isImmutableList) - { - listType = type.GetGenericArguments()[0]; - return true; - } - - listType = default; - return false; - } - - private static bool TryGenericReadOnlyDictType(Type type, [NotNullWhen(true)] out Type? keyType, [NotNullWhen(true)] out Type? valType) - { - var isDict = type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>); - - if (isDict) - { - var genArgs = type.GetGenericArguments(); - keyType = genArgs[0]; - valType = genArgs[1]; - return true; - } - - keyType = default; - valType = default; - return false; - } - - private static bool TryGenericReadDictType(Type type, [NotNullWhen(true)] out Type? keyType, - [NotNullWhen(true)] out Type? valType, [NotNullWhen(true)] out Type? dictType) - { - if (TryGenericDictType(type, out keyType, out valType)) - { - // Pass through the type directly if it's Dictionary. - // Since that's more efficient. - dictType = type; - return true; - } - - if (TryGenericReadOnlyDictType(type, out keyType, out valType)) - { - // If it's IReadOnlyDictionary we need to make a Dictionary type to use to deserialize. - dictType = typeof(Dictionary<,>).MakeGenericType(keyType, valType); - return true; - } - - dictType = default; - return false; - } - - private static bool TryGenericDictType(Type type, [NotNullWhen(true)] out Type? keyType, [NotNullWhen(true)] out Type? valType) - { - var isDict = type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>); - - if (isDict) - { - var genArgs = type.GetGenericArguments(); - keyType = genArgs[0]; - valType = genArgs[1]; - return true; - } - - keyType = default; - valType = default; - return false; - } - - private static bool TryGenericHashSetType(Type type, [NotNullWhen(true)] out Type? setType) - { - var isSet = type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(HashSet<>); - - if (isSet) - { - setType = type.GetGenericArguments()[0]; - return true; - } - - setType = default; - return false; - } - - private static bool TryGenericKeyValuePairType( - Type type, - [NotNullWhen(true)] out Type? keyType, - [NotNullWhen(true)] out Type? valType) - { - var isPair = type.GetTypeInfo().IsGenericType && - type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>); - - if (isPair) - { - var genArgs = type.GetGenericArguments(); - keyType = genArgs[0]; - valType = genArgs[1]; - return true; - } - - keyType = default; - valType = default; - return false; - } - - public abstract class TypeSerializer - { - public abstract object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer); - public abstract YamlNode TypeToNode(object obj, YamlObjectSerializer serializer); - } - - /// - /// Basically, when you're serializing say a map file, you gotta be a liiiittle smarter than "dump all these variables to YAML". - /// Stuff like entity references need to handled, for example. - /// This can do that. - /// - public abstract class Context - { - /// - /// Current depth of the serialization "stack". - /// Basically, when another sub-serializer gets made (e.g. to handle ), - /// This context will be passed around and this property increased to signal that. - /// - public int StackDepth { get; protected internal set; } = 0; - - public virtual bool TryTypeToNode(object obj, [NotNullWhen(true)] out YamlNode? node) - { - node = null; - return false; - } - - public virtual bool TryNodeToType(YamlNode node, Type type, [NotNullWhen(true)] out object? obj) - { - obj = default; - return false; - } - - public virtual bool IsValueDefault(string field, T value, WithFormat format) - { - return false; - } - - public virtual bool TryGetCachedField(string field, [MaybeNullWhen(false)] out T value) - { - value = default; - return false; - } - - public virtual void SetCachedField(string field, T value) - { - } - - public virtual bool TryGetDataCache(string field, out object? value) - { - value = null; - return false; - } - - public virtual void SetDataCache(string field, object value) - { - } - } - - class StructSerializer : TypeSerializer - { - public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer) - { - var mapNode = (YamlMappingNode)node; - - var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - var instance = Activator.CreateInstance(type)!; - var scalarNode = new YamlScalarNode(); - - foreach (var field in fields) - { - if (field.IsNotSerialized) - continue; - - var fName = field.Name; - var fType = field.FieldType; - - scalarNode.Value = fName; - - if (mapNode.Children.TryGetValue(scalarNode, out var fNode)) - { - var fVal = serializer.NodeToType(fType, fNode); - field.SetValue(instance, fVal); - } - } - - return instance; - } - - public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer) - { - var node = new YamlMappingNode(); - var type = obj.GetType(); - var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - - foreach (var field in fields) - { - if (field.IsNotSerialized) - continue; - - var fVal = field.GetValue(obj); - - if (fVal == null) - { - throw new ArgumentException("Cannot serialize null value inside struct field."); - } - - // Potential recursive infinite loop? - var fTypeNode = serializer.TypeToNode(fVal); - node.Add(field.Name, fTypeNode); - } - - return node; - } - } - - class ColorSerializer : TypeSerializer - { - public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer) - { - return node.AsColor(); - } - - public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer) - { - var color = (Color)obj; - - return new YamlScalarNode(color.ToHex()); - } - } - - class MapIdSerializer : TypeSerializer - { - public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer) - { - var val = int.Parse(node.ToString(), CultureInfo.InvariantCulture); - return new MapId(val); - } - - public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer) - { - var val = (int)(MapId)obj; - return new YamlScalarNode(val.ToString()); - } - } - - class GridIdSerializer : TypeSerializer - { - public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer) - { - return new GridId(node.AsInt()); - } - - public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer) - { - var val = (int)(GridId)obj; - return new YamlScalarNode(val.ToString()); - } - } - - class Vector2Serializer : TypeSerializer - { - public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer) - { - return node.AsVector2(); - } - - public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer) - { - var vec = (Vector2)obj; - return new YamlScalarNode($"{vec.X.ToString(CultureInfo.InvariantCulture)},{vec.Y.ToString(CultureInfo.InvariantCulture)}"); - } - } - - class Vector2iSerializer : TypeSerializer - { - public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer) - { - return node.AsVector2i(); - } - - public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer) - { - var vec = (Vector2i)obj; - return new YamlScalarNode($"{vec.X.ToString(CultureInfo.InvariantCulture)},{vec.Y.ToString(CultureInfo.InvariantCulture)}"); - } - } - - class Vector3Serializer : TypeSerializer - { - public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer) - { - return node.AsVector3(); - } - - public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer) - { - var vec = (Vector3)obj; - return new YamlScalarNode($"{vec.X.ToString(CultureInfo.InvariantCulture)},{vec.Y.ToString(CultureInfo.InvariantCulture)},{vec.Z.ToString(CultureInfo.InvariantCulture)}"); - } - } - - class Vector4Serializer : TypeSerializer - { - public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer) - { - return node.AsVector4(); - } - - public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer) - { - var vec = (Vector4)obj; - return new YamlScalarNode($"{vec.X.ToString(CultureInfo.InvariantCulture)},{vec.Y.ToString(CultureInfo.InvariantCulture)},{vec.Z.ToString(CultureInfo.InvariantCulture)},{vec.W.ToString(CultureInfo.InvariantCulture)}"); - } - } - - class AngleSerializer : TypeSerializer - { - public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer) - { - var nodeContents = node.AsString(); - if (nodeContents.EndsWith("rad")) - { - return new Angle(double.Parse(nodeContents.Substring(0, nodeContents.Length - 3), CultureInfo.InvariantCulture)); - } - return Angle.FromDegrees(double.Parse(nodeContents, CultureInfo.InvariantCulture)); - } - - public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer) - { - var val = ((Angle)obj).Theta; - return new YamlScalarNode($"{val.ToString(CultureInfo.InvariantCulture)} rad"); - } - } - - class UIBox2Serializer : TypeSerializer - { - public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer) - { - var args = node.ToString().Split(','); - - var t = float.Parse(args[0], CultureInfo.InvariantCulture); - var l = float.Parse(args[1], CultureInfo.InvariantCulture); - var b = float.Parse(args[2], CultureInfo.InvariantCulture); - var r = float.Parse(args[3], CultureInfo.InvariantCulture); - - return new UIBox2(l, t, r, b); - } - - public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer) - { - var box = (UIBox2)obj; - return new YamlScalarNode($"{box.Top.ToString(CultureInfo.InvariantCulture)},{box.Left.ToString(CultureInfo.InvariantCulture)},{box.Bottom.ToString(CultureInfo.InvariantCulture)},{box.Right.ToString(CultureInfo.InvariantCulture)}"); - } - } - - class Box2Serializer : TypeSerializer - { - public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer) - { - var args = node.ToString().Split(','); - - var b = float.Parse(args[0], CultureInfo.InvariantCulture); - var l = float.Parse(args[1], CultureInfo.InvariantCulture); - var t = float.Parse(args[2], CultureInfo.InvariantCulture); - var r = float.Parse(args[3], CultureInfo.InvariantCulture); - - return new Box2(l, b, r, t); - } - - public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer) - { - var box = (Box2)obj; - return new YamlScalarNode($"{box.Bottom.ToString(CultureInfo.InvariantCulture)},{box.Left.ToString(CultureInfo.InvariantCulture)},{box.Top.ToString(CultureInfo.InvariantCulture)},{box.Right.ToString(CultureInfo.InvariantCulture)}"); - } - } - - class ResourcePathSerializer : TypeSerializer - { - public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer) - { - return node.AsResourcePath(); - } - - public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer) - { - return new YamlScalarNode(obj.ToString()); - } - } - - class SpriteSpecifierSerializer : TypeSerializer - { - public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer) - { - return SpriteSpecifier.FromYaml(node); - } - - public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer) - { - switch (obj) - { - case SpriteSpecifier.Texture tex: - return tex.TexturePath.ToString(); - case SpriteSpecifier.Rsi rsi: - var mapping = new YamlMappingNode(); - mapping.Add("sprite", rsi.RsiPath.ToString()); - mapping.Add("state", rsi.RsiState); - return mapping; - } - throw new NotImplementedException(); - } - } - - class TimeSpanSerializer : TypeSerializer - { - public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer) - { - var seconds = double.Parse(node.AsString(), CultureInfo.InvariantCulture); - return TimeSpan.FromSeconds(seconds); - } - - public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer) - { - var seconds = ((TimeSpan) obj).TotalSeconds; - return new YamlScalarNode(seconds.ToString(CultureInfo.InvariantCulture)); - } - } - } -} diff --git a/Robust.Shared/SharedIoC.cs b/Robust.Shared/SharedIoC.cs index 9b23dbca7..c6b74411e 100644 --- a/Robust.Shared/SharedIoC.cs +++ b/Robust.Shared/SharedIoC.cs @@ -12,6 +12,7 @@ using Robust.Shared.Physics; using Robust.Shared.Random; using Robust.Shared.Sandboxing; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager; using Robust.Shared.Timing; namespace Robust.Shared @@ -20,6 +21,7 @@ namespace Robust.Shared { public static void RegisterIoC() { + IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); @@ -39,7 +41,6 @@ namespace Robust.Shared IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); - IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); diff --git a/Robust.Shared/Utility/FormattedMessage.MarkupParser.cs b/Robust.Shared/Utility/FormattedMessage.MarkupParser.cs index 4a0d1be3b..0ab2b7f74 100644 --- a/Robust.Shared/Utility/FormattedMessage.MarkupParser.cs +++ b/Robust.Shared/Utility/FormattedMessage.MarkupParser.cs @@ -59,6 +59,11 @@ namespace Robust.Shared.Utility private static readonly Parser> ParsePermissive = ParseTagText.Cast().Or(ParseTagOrFallBack).Many(); + public static bool ValidMarkup(string markup) + { + return Parse.Parse(markup).Success; + } + public void AddMarkup(string markup) { _tags.AddRange(Parse.ParseOrThrow(markup)); diff --git a/Robust.Shared/Utility/ILGeneratorExt.cs b/Robust.Shared/Utility/ILGeneratorExt.cs new file mode 100644 index 000000000..612e119b6 --- /dev/null +++ b/Robust.Shared/Utility/ILGeneratorExt.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.InteropServices; + +namespace Robust.Shared.Utility +{ + public static class ILGeneratorExt + { + public static RobustILGenerator GetRobustGen(this DynamicMethod dynamicMethod) => + new(dynamicMethod.GetILGenerator()); + public static RobustILGenerator GetRobustGen(this ILGenerator generator) => new (generator); + + } + + public class RobustILGenerator + { + private ILGenerator _generator; + private List<(object, object?)> _log = new(); + private List<(Type, int)> _locals = new(); + + public RobustILGenerator(ILGenerator generator) + { + _generator = generator; + } + + public string[] GetStringLog() + { + var full = _locals.Select(e => $"LOCAL: {e.Item1} at {e.Item2}").ToList(); + full.Add("===== CODE ====="); + full.AddRange(_log.Select(e => $"{e.Item1} - {e.Item2}")); + return full.ToArray(); + } + + private void Log(object opcode, object? arg = null) + { + _log.Add((opcode, arg)); + } + + public virtual LocalBuilder DeclareLocal(Type localType) + { + return DeclareLocal(localType, false); + } + + public virtual LocalBuilder DeclareLocal(Type localType, bool pinned) + { + var loc = _generator.DeclareLocal(localType, pinned); + _locals.Add((localType, loc.LocalIndex)); + return loc; + } + + public virtual void ThrowException([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type excType) + { + //this just calls emit + _generator.ThrowException(excType); + } + + public virtual Label DefineLabel() + { + var label = _generator.DefineLabel(); + //Log("DefineLabel", label); + return label; + } + + public virtual void MarkLabel(Label loc) + { + _generator.MarkLabel(loc); + Log("MarkLabel", loc.GetHashCode()); + } + + public virtual void Emit(OpCode opcode) + { + _generator.Emit(opcode); + Log(opcode); + } + + public virtual void Emit(OpCode opcode, byte arg) + { + _generator.Emit(opcode, arg); + Log(opcode, arg); + } + + public void Emit(OpCode opcode, sbyte arg) + { + _generator.Emit(opcode, arg); + Log(opcode, arg); + } + + public virtual void Emit(OpCode opcode, short arg) + { + _generator.Emit(opcode, arg); + Log(opcode, arg); + } + + public virtual void Emit(OpCode opcode, int arg) + { + _generator.Emit(opcode, arg); + Log(opcode, arg); + } + + public virtual void Emit(OpCode opcode, MethodInfo meth) + { + _generator.Emit(opcode, meth); + Log(opcode, meth); + } + + /*public virtual void EmitCalli(OpCode opcode, CallingConventions callingConvention, + Type? returnType, Type[]? parameterTypes, Type[]? optionalParameterTypes) + { + } + + public virtual void EmitCalli(OpCode opcode, CallingConvention unmanagedCallConv, Type? returnType, Type[]? parameterTypes) + { + } + + public virtual void EmitCall(OpCode opcode, MethodInfo methodInfo, Type[]? optionalParameterTypes) + { + }*/ + + public virtual void Emit(OpCode opcode, SignatureHelper signature) + { + _generator.Emit(opcode, signature); + Log(opcode, signature); + } + + public virtual void Emit(OpCode opcode, ConstructorInfo con) + { + _generator.Emit(opcode, con); + Log(opcode, con); + } + + public virtual void Emit(OpCode opcode, Type cls) + { + _generator.Emit(opcode, cls); + Log(opcode, cls); + } + + public virtual void Emit(OpCode opcode, long arg) + { + _generator.Emit(opcode, arg); + Log(opcode, arg); + } + + public virtual void Emit(OpCode opcode, float arg) + { + _generator.Emit(opcode, arg); + Log(opcode, arg); + } + + public virtual void Emit(OpCode opcode, double arg) + { + _generator.Emit(opcode, arg); + Log(opcode, arg); + } + + public virtual void Emit(OpCode opcode, Label label) + { + _generator.Emit(opcode, label); + Log(opcode, label.GetHashCode()); + } + + public virtual void Emit(OpCode opcode, Label[] labels) + { + _generator.Emit(opcode, labels); + Log(opcode, labels); + } + + public virtual void Emit(OpCode opcode, FieldInfo field) + { + _generator.Emit(opcode, field); + Log(opcode, field); + } + + public virtual void Emit(OpCode opcode, string str) + { + _generator.Emit(opcode, str); + Log(opcode, str); + } + + public virtual void Emit(OpCode opcode, LocalBuilder local) + { + _generator.Emit(opcode, local); + Log(opcode, local); + } + } +} diff --git a/Robust.Shared/Utility/NullableHelper.cs b/Robust.Shared/Utility/NullableHelper.cs index 58c6cb701..df335b505 100644 --- a/Robust.Shared/Utility/NullableHelper.cs +++ b/Robust.Shared/Utility/NullableHelper.cs @@ -8,10 +8,26 @@ namespace Robust.Shared.Utility { private const int NotAnnotatedNullableFlag = 1; + private static readonly Dictionary + _nullableAttributeTypeCache = new(); - private static Dictionary _nullableAttributeTypeCache = new(); + private static readonly Dictionary + _nullableContextAttributeTypeCache = new(); - private static Dictionary _nullableContextAttributeTypeCache = new(); + public static Type EnsureNullableType(this Type type) + { + if (type.IsValueType) + { + return typeof(Nullable<>).MakeGenericType(type); + } + + return type; + } + + public static Type EnsureNotNullableType(this Type type) + { + return Nullable.GetUnderlyingType(type) ?? type; + } /// /// Checks if the field has a nullable annotation [?] diff --git a/Robust.Shared/Utility/SpriteSpecifier.cs b/Robust.Shared/Utility/SpriteSpecifier.cs index 02e4e4d0b..d9f10583b 100644 --- a/Robust.Shared/Utility/SpriteSpecifier.cs +++ b/Robust.Shared/Utility/SpriteSpecifier.cs @@ -13,7 +13,7 @@ namespace Robust.Shared.Utility /// Is a reference to EITHER an RSI + RSI State, OR a bare texture path. /// [Serializable, NetSerializable] - public class SpriteSpecifier + public abstract class SpriteSpecifier { public static readonly SpriteSpecifier Invalid = new Texture(new ResourcePath(".")); diff --git a/Robust.Shared/Utility/TypeHelpers.cs b/Robust.Shared/Utility/TypeHelpers.cs index 6925fb2e9..f99bd836b 100644 --- a/Robust.Shared/Utility/TypeHelpers.cs +++ b/Robust.Shared/Utility/TypeHelpers.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; @@ -17,8 +18,11 @@ namespace Robust.Shared.Utility // Even when you pass BindingFlags.NonPublic. foreach (var p in GetClassHierarchy(t)) { - foreach (var field in p.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | - BindingFlags.DeclaredOnly | BindingFlags.Public)) + foreach (var field in p.GetFields( + BindingFlags.NonPublic | + BindingFlags.Instance | + BindingFlags.DeclaredOnly | + BindingFlags.Public)) { yield return field; } @@ -31,8 +35,11 @@ namespace Robust.Shared.Utility public static IEnumerable GetAllProperties(this Type t) { return GetClassHierarchy(t).SelectMany(p => - p.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly | - BindingFlags.Public)); + p.GetProperties( + BindingFlags.NonPublic | + BindingFlags.Instance | + BindingFlags.DeclaredOnly | + BindingFlags.Public)); } /// @@ -75,8 +82,11 @@ namespace Robust.Shared.Utility { foreach (var p in GetClassHierarchy(t)) { - foreach (var field in p.GetNestedTypes(BindingFlags.NonPublic | BindingFlags.Instance | - BindingFlags.DeclaredOnly | BindingFlags.Public)) + foreach (var field in p.GetNestedTypes( + BindingFlags.NonPublic | + BindingFlags.Instance | + BindingFlags.DeclaredOnly | + BindingFlags.Public)) { yield return field; } @@ -85,7 +95,7 @@ namespace Robust.Shared.Utility internal static readonly IComparer TypeInheritanceComparer = new TypeInheritanceComparerImpl(); - private sealed class TypeInheritanceComparerImpl : IComparer + public sealed class TypeInheritanceComparerImpl : IComparer { public int Compare(Type? x, Type? y) { @@ -107,5 +117,287 @@ namespace Robust.Shared.Utility return 0; } } + + public static bool TryGenericReadOnlyCollectionType(Type type, [NotNullWhen(true)] out Type? listType) + { + if (!type.GetTypeInfo().IsGenericType) + { + listType = default; + return false; + } + + var baseGeneric = type.GetGenericTypeDefinition(); + var isList = baseGeneric == typeof(IReadOnlyCollection<>) || baseGeneric == typeof(IReadOnlyList<>); + + if (isList) + { + listType = type.GetGenericArguments()[0]; + return true; + } + + listType = default; + return false; + } + + public static bool TryGenericListType(Type type, [NotNullWhen(true)] out Type? listType) + { + var isList = type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>); + + if (isList) + { + listType = type.GetGenericArguments()[0]; + return true; + } + + listType = default; + return false; + } + + public static bool TryGenericReadOnlyDictType(Type type, [NotNullWhen(true)] out Type? keyType, [NotNullWhen(true)] out Type? valType) + { + var isDict = type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>); + + if (isDict) + { + var genArgs = type.GetGenericArguments(); + keyType = genArgs[0]; + valType = genArgs[1]; + return true; + } + + keyType = default; + valType = default; + return false; + } + + public static bool TryGenericReadDictType(Type type, [NotNullWhen(true)] out Type? keyType, + [NotNullWhen(true)] out Type? valType, [NotNullWhen(true)] out Type? dictType) + { + if (TryGenericDictType(type, out keyType, out valType)) + { + // Pass through the type directly if it's Dictionary. + // Since that's more efficient. + dictType = type; + return true; + } + + if (TryGenericReadOnlyDictType(type, out keyType, out valType)) + { + // If it's IReadOnlyDictionary we need to make a Dictionary type to use to deserialize. + dictType = typeof(Dictionary<,>).MakeGenericType(keyType, valType); + return true; + } + + dictType = default; + return false; + } + + public static bool TryGenericDictType(Type type, [NotNullWhen(true)] out Type? keyType, [NotNullWhen(true)] out Type? valType) + { + var isDict = type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>); + + if (isDict) + { + var genArgs = type.GetGenericArguments(); + keyType = genArgs[0]; + valType = genArgs[1]; + return true; + } + + keyType = default; + valType = default; + return false; + } + + public static bool TryGenericSortedDictType(Type type, [NotNullWhen(true)] out Type? keyType, + [NotNullWhen(true)] out Type? valType) + { + var isDict = type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(SortedDictionary<,>); + + if (isDict) + { + var genArgs = type.GetGenericArguments(); + keyType = genArgs[0]; + valType = genArgs[1]; + return true; + } + + keyType = default; + valType = default; + return false; + } + + public static bool TryGenericHashSetType(Type type, [NotNullWhen(true)] out Type? setType) + { + var isSet = type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(HashSet<>); + + if (isSet) + { + setType = type.GetGenericArguments()[0]; + return true; + } + + setType = default; + return false; + } + + public static bool TryGenericSortedSetType(Type type, [NotNullWhen(true)] out Type? setType) + { + var isSet = type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(SortedSet<>); + + if (isSet) + { + setType = type.GetGenericArguments()[0]; + return true; + } + + setType = default; + return false; + } + + public static IEnumerable GetAllPropertiesAndFields(this Type type) + { + foreach (var field in type.GetAllFields()) + { + yield return new SpecificFieldInfo(field); + } + + foreach (var property in type.GetAllProperties()) + { + yield return new SpecificPropertyInfo(property); + } + } + + public static Type? SelectCommonType(Type type1, Type type2) + { + Type? commonType = null; + if (type1.IsAssignableFrom(type2)) + { + commonType = type1; + }else if (type2.IsAssignableFrom(type1)) + { + commonType = type2; + } + + return commonType; + } + } + + public abstract class AbstractFieldInfo + { + public abstract Type FieldType { get; } + public abstract Type? DeclaringType { get; } + + public abstract object? GetValue(object? obj); + public abstract void SetValue(object? obj, object? value); + + public abstract T? GetCustomAttribute() where T : Attribute; + public abstract IEnumerable GetCustomAttributes() where T : Attribute; + } + + public class SpecificFieldInfo : AbstractFieldInfo + { + public readonly FieldInfo FieldInfo; + public override Type FieldType => FieldInfo.FieldType; + public override Type? DeclaringType => FieldInfo.DeclaringType; + + public SpecificFieldInfo(FieldInfo fieldInfo) + { + FieldInfo = fieldInfo; + } + + public override object? GetValue(object? obj) => FieldInfo.GetValue(obj); + public override void SetValue(object? obj, object? value) => FieldInfo.SetValue(obj, value); + + public override T? GetCustomAttribute() where T : class + { + return (T?)Attribute.GetCustomAttribute(FieldInfo, typeof(T)); + } + + public override IEnumerable GetCustomAttributes() + { + return FieldInfo.GetCustomAttributes(); + } + + public static implicit operator FieldInfo(SpecificFieldInfo f) => f.FieldInfo; + public static explicit operator SpecificFieldInfo(FieldInfo f) => new(f); + + public override string? ToString() + { + return FieldInfo.ToString(); + } + } + + public class SpecificPropertyInfo : AbstractFieldInfo + { + public readonly PropertyInfo PropertyInfo; + public override Type FieldType => PropertyInfo.PropertyType; + public override Type? DeclaringType => PropertyInfo.DeclaringType; + + public SpecificPropertyInfo(PropertyInfo propertyInfo) + { + PropertyInfo = propertyInfo; + } + + public override object? GetValue(object? obj) => PropertyInfo.GetValue(obj); + public override void SetValue(object? obj, object? value) => PropertyInfo.SetValue(obj, value); + + public override T? GetCustomAttribute() where T : class + { + return (T?)Attribute.GetCustomAttribute(PropertyInfo, typeof(T)); + } + + public override IEnumerable GetCustomAttributes() + { + return PropertyInfo.GetCustomAttributes(); + } + + public bool IsVirtual() + { + return (PropertyInfo.GetGetMethod()?.IsVirtual ?? false) || + (PropertyInfo.GetSetMethod()?.IsVirtual ?? false); + } + + public bool IsMostOverridden(Type type) + { + if (DeclaringType == type) + { + return true; + } + + var setBase = PropertyInfo.SetMethod?.GetBaseDefinition(); + var getBase = PropertyInfo.GetMethod?.GetBaseDefinition(); + var currentType = type; + var relevantProperties = type.GetAllProperties().Where(p => p.Name == PropertyInfo.Name).ToList(); + while (currentType != null) + { + foreach (var property in relevantProperties) + { + if(property.DeclaringType != currentType) continue; + + if (setBase != null && setBase == property.SetMethod?.GetBaseDefinition()) + { + return property == PropertyInfo; + } + + if (getBase != null && getBase == property.GetMethod?.GetBaseDefinition()) + { + return property == PropertyInfo; + } + } + + currentType = currentType.BaseType; + } + + return false; + } + + public static implicit operator PropertyInfo(SpecificPropertyInfo f) => f.PropertyInfo; + public static explicit operator SpecificPropertyInfo(PropertyInfo f) => new(f); + + public override string? ToString() + { + return PropertyInfo.ToString(); + } } } diff --git a/Robust.UnitTesting/Client/GameObjects/Components/Transform_Test.cs b/Robust.UnitTesting/Client/GameObjects/Components/Transform_Test.cs index 37c72be2d..06556dd68 100644 --- a/Robust.UnitTesting/Client/GameObjects/Components/Transform_Test.cs +++ b/Robust.UnitTesting/Client/GameObjects/Components/Transform_Test.cs @@ -7,6 +7,7 @@ using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; namespace Robust.UnitTesting.Client.GameObjects.Components { @@ -53,6 +54,7 @@ namespace Robust.UnitTesting.Client.GameObjects.Components MapManager.Initialize(); MapManager.Startup(); + IoCManager.Resolve().Initialize(); var manager = IoCManager.Resolve(); manager.LoadFromStream(new StringReader(PROTOTYPES)); manager.Resync(); diff --git a/Robust.UnitTesting/Server/GameObjects/Components/Container_Test.cs b/Robust.UnitTesting/Server/GameObjects/Components/Container_Test.cs index 702b8f1be..5c0e81d57 100644 --- a/Robust.UnitTesting/Server/GameObjects/Components/Container_Test.cs +++ b/Robust.UnitTesting/Server/GameObjects/Components/Container_Test.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Moq; using NUnit.Framework; @@ -26,7 +26,7 @@ namespace Robust.UnitTesting.Server.GameObjects.Components return sim; } - + const string PROTOTYPES = @" - type: entity name: dummy @@ -182,7 +182,7 @@ namespace Robust.UnitTesting.Server.GameObjects.Components Assert.That(container.Insert(mapEnt), Is.False); Assert.That(container.CanInsert(mapEnt), Is.False); } - + [Test] public void BaseContainer_InsertGrid_False() { diff --git a/Robust.UnitTesting/Server/GameObjects/Components/Transform_Test.cs b/Robust.UnitTesting/Server/GameObjects/Components/Transform_Test.cs index 2d0d015c5..561af5a63 100644 --- a/Robust.UnitTesting/Server/GameObjects/Components/Transform_Test.cs +++ b/Robust.UnitTesting/Server/GameObjects/Components/Transform_Test.cs @@ -7,6 +7,7 @@ using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; using Robust.Shared.Timing; namespace Robust.UnitTesting.Server.GameObjects.Components @@ -56,6 +57,7 @@ namespace Robust.UnitTesting.Server.GameObjects.Components MapManager.CreateMap(); + IoCManager.Resolve().Initialize(); var manager = IoCManager.Resolve(); manager.LoadFromStream(new StringReader(PROTOTYPES)); manager.Resync(); diff --git a/Robust.UnitTesting/Server/GameObjects/ThrowingEntityDeletion_Test.cs b/Robust.UnitTesting/Server/GameObjects/ThrowingEntityDeletion_Test.cs index 28cdfc986..5c1c5792a 100644 --- a/Robust.UnitTesting/Server/GameObjects/ThrowingEntityDeletion_Test.cs +++ b/Robust.UnitTesting/Server/GameObjects/ThrowingEntityDeletion_Test.cs @@ -7,7 +7,7 @@ using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager; namespace Robust.UnitTesting.Server.GameObjects { @@ -23,10 +23,6 @@ namespace Robust.UnitTesting.Server.GameObjects id: throwInAdd components: - type: ThrowsInAdd -- type: entity - id: throwInExposeData - components: - - type: ThrowsInExposeData - type: entity id: throwsInInitialize components: @@ -43,7 +39,6 @@ namespace Robust.UnitTesting.Server.GameObjects _componentFactory = IoCManager.Resolve(); _componentFactory.Register(); - _componentFactory.Register(); _componentFactory.Register(); _componentFactory.Register(); @@ -57,6 +52,7 @@ namespace Robust.UnitTesting.Server.GameObjects MapManager.CreateNewMapEntity(MapId.Nullspace); + IoCManager.Resolve().Initialize(); var manager = IoCManager.Resolve(); manager.LoadFromStream(new StringReader(PROTOTYPES)); manager.Resync(); @@ -65,7 +61,7 @@ namespace Robust.UnitTesting.Server.GameObjects } [Test] - public void Test([Values("throwInAdd", "throwInExposeData", "throwsInInitialize", "throwsInStartup")] + public void Test([Values("throwInAdd", "throwsInInitialize", "throwsInStartup")] string prototypeName) { Assert.That(() => EntityManager.SpawnEntity(prototypeName, MapCoordinates.Nullspace), @@ -81,13 +77,6 @@ namespace Robust.UnitTesting.Server.GameObjects public override void OnAdd() => throw new NotSupportedException(); } - private sealed class ThrowsInExposeDataComponent : Component - { - public override string Name => "ThrowsInExposeData"; - - public override void ExposeData(ObjectSerializer serializer) => throw new NotSupportedException(); - } - private sealed class ThrowsInInitializeComponent : Component { public override string Name => "ThrowsInInitialize"; diff --git a/Robust.UnitTesting/Server/Maps/MapLoaderTest.cs b/Robust.UnitTesting/Server/Maps/MapLoaderTest.cs index e7060ecf1..a4acd792f 100644 --- a/Robust.UnitTesting/Server/Maps/MapLoaderTest.cs +++ b/Robust.UnitTesting/Server/Maps/MapLoaderTest.cs @@ -6,7 +6,8 @@ using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Utility; namespace Robust.UnitTesting.Server.Maps @@ -65,6 +66,8 @@ entities: IoCManager.Resolve().Initialize(); + IoCManager.Resolve().Initialize(); + var resourceManager = IoCManager.Resolve(); resourceManager.Initialize(null); resourceManager.MountString("/TestMap.yml", MapData); @@ -98,22 +101,14 @@ entities: Assert.That(c.Baz, Is.EqualTo(-1)); } + [DataDefinition] private sealed class MapDeserializeTestComponent : Component { public override string Name => "MapDeserializeTest"; - public int Foo { get; set; } - public int Bar { get; set; } - public int Baz { get; set; } - - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - - serializer.DataField(this, p => p.Foo, "foo", -1); - serializer.DataField(this, p => p.Bar, "bar", -1); - serializer.DataField(this, p => p.Baz, "baz", -1); - } + [DataField("foo")] public int Foo { get; set; } = -1; + [DataField("bar")] public int Bar { get; set; } = -1; + [DataField("baz")] public int Baz { get; set; } = -1; } } } diff --git a/Robust.UnitTesting/Server/RobustServerSimulation.cs b/Robust.UnitTesting/Server/RobustServerSimulation.cs index c0b0c19e2..0681e27a3 100644 --- a/Robust.UnitTesting/Server/RobustServerSimulation.cs +++ b/Robust.UnitTesting/Server/RobustServerSimulation.cs @@ -15,6 +15,9 @@ using Robust.Shared.Physics; using Robust.Shared.Prototypes; using Robust.Shared.Reflection; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.TypeSerializers.Implementations; using Robust.Shared.Timing; namespace Robust.UnitTesting.Server @@ -126,7 +129,7 @@ namespace Robust.UnitTesting.Server { var container = new DependencyCollection(); Collection = container; - + IoCManager.InitThread(container, true); //TODO: This is a long term project that should eventually have parity with the actual server/client/SP IoC registration. @@ -144,7 +147,29 @@ namespace Robust.UnitTesting.Server container.Register(); container.Register(); container.RegisterInstance(new Mock().Object); - container.RegisterInstance(new Mock().Object); // tests should not be searching for types + + var reflectionManager = new Mock(); + reflectionManager + .Setup(x => x.FindTypesWithAttribute()) + .Returns(() => new[] + { + typeof(DataDefinition) + }); + + reflectionManager + .Setup(x => x.FindTypesWithAttribute(typeof(DataDefinition))) + .Returns(() => new[] + { + typeof(EntityPrototype), + typeof(TransformComponent), + typeof(MetaDataComponent) + }); + + reflectionManager + .Setup(x => x.FindTypesWithAttribute()) + .Returns(() => new[] {typeof(StringSerializer)}); + + container.RegisterInstance(reflectionManager.Object); // tests should not be searching for types container.RegisterInstance(new Mock().Object); container.RegisterInstance(new Mock().Object); // no disk access for tests container.RegisterInstance(new Mock().Object); // TODO: get timing working similar to RobustIntegrationTest @@ -152,6 +177,7 @@ namespace Robust.UnitTesting.Server //Tier 2: Simulation container.Register(); container.Register(); + container.Register(); container.Register(); container.Register(); container.Register(); @@ -165,15 +191,15 @@ namespace Robust.UnitTesting.Server //TODO: Try to remove these container.RegisterInstance(new Mock().Object); container.RegisterInstance(new Mock().Object); - + _diFactory?.Invoke(container); container.BuildGraph(); - + var logMan = container.Resolve(); logMan.RootSawmill.AddHandler(new TestLogHandler("SIM")); var compFactory = container.Resolve(); - + compFactory.Register(); compFactory.RegisterReference(); @@ -200,6 +226,8 @@ namespace Robust.UnitTesting.Server mapManager.Initialize(); mapManager.Startup(); + container.Resolve().Initialize(); + var protoMan = container.Resolve(); protoMan.RegisterType(typeof(EntityPrototype)); _protoDelegate?.Invoke(protoMan); diff --git a/Robust.UnitTesting/Shared/GameObjects/ComponentDependencies_Tests.cs b/Robust.UnitTesting/Shared/GameObjects/ComponentDependencies_Tests.cs index cea6f2260..c8c0f0ed4 100644 --- a/Robust.UnitTesting/Shared/GameObjects/ComponentDependencies_Tests.cs +++ b/Robust.UnitTesting/Shared/GameObjects/ComponentDependencies_Tests.cs @@ -3,6 +3,8 @@ using NUnit.Framework; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; + // ReSharper disable AccessToStaticMemberViaDerivedType namespace Robust.UnitTesting.Shared.GameObjects @@ -165,6 +167,7 @@ namespace Robust.UnitTesting.Shared.GameObjects var componentManager = IoCManager.Resolve(); componentManager.Initialize(); + IoCManager.Resolve().Initialize(); var prototypeManager = IoCManager.Resolve(); prototypeManager.LoadFromStream(new StringReader(PROTOTYPES)); prototypeManager.Resync(); diff --git a/Robust.UnitTesting/Shared/Localization/LocalizationTests.cs b/Robust.UnitTesting/Shared/Localization/LocalizationTests.cs index d02199927..48ff57425 100644 --- a/Robust.UnitTesting/Shared/Localization/LocalizationTests.cs +++ b/Robust.UnitTesting/Shared/Localization/LocalizationTests.cs @@ -1,12 +1,12 @@ -using System.Globalization; +using System.Globalization; using NUnit.Framework; using Robust.Shared.ContentPack; using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Components.Localization; using Robust.Shared.IoC; using Robust.Shared.Localization; -using Robust.Shared.Maths; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; using Robust.Shared.Utility; namespace Robust.UnitTesting.Shared.Localization @@ -18,6 +18,7 @@ namespace Robust.UnitTesting.Shared.Localization [OneTimeSetUp] public void Setup() { + IoCManager.Resolve().Initialize(); IoCManager.Resolve().Register(); IoCManager.Resolve().Initialize(); @@ -55,7 +56,7 @@ namespace Robust.UnitTesting.Shared.Localization id: GenderTestEntityWithComp components: - type: Grammar - gender: enum.Gender.Female + gender: Female "; private const string FluentCode = @" diff --git a/Robust.UnitTesting/Shared/Map/EntityCoordinates_Tests.cs b/Robust.UnitTesting/Shared/Map/EntityCoordinates_Tests.cs index 414d30a46..96bc2f86a 100644 --- a/Robust.UnitTesting/Shared/Map/EntityCoordinates_Tests.cs +++ b/Robust.UnitTesting/Shared/Map/EntityCoordinates_Tests.cs @@ -5,6 +5,7 @@ using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; // ReSharper disable InconsistentNaming // ReSharper disable AccessToStaticMemberViaDerivedType @@ -31,6 +32,7 @@ namespace Robust.UnitTesting.Shared.Map mapManager.Initialize(); mapManager.Startup(); + IoCManager.Resolve().Initialize(); var prototypeManager = IoCManager.Resolve(); prototypeManager.LoadFromStream(new StringReader(PROTOTYPES)); prototypeManager.Resync(); diff --git a/Robust.UnitTesting/Shared/Maths/Ray_Test.cs b/Robust.UnitTesting/Shared/Maths/Ray_Test.cs index 0fe901db8..7c410943b 100644 --- a/Robust.UnitTesting/Shared/Maths/Ray_Test.cs +++ b/Robust.UnitTesting/Shared/Maths/Ray_Test.cs @@ -1,5 +1,6 @@ using NUnit.Framework; using Robust.Shared.Maths; +using Robust.Shared.Physics; namespace Robust.UnitTesting.Shared.Maths { diff --git a/Robust.UnitTesting/Shared/Prototypes/PrototypeManager_Test.cs b/Robust.UnitTesting/Shared/Prototypes/PrototypeManager_Test.cs index 32daf7d41..e28ff2ccf 100644 --- a/Robust.UnitTesting/Shared/Prototypes/PrototypeManager_Test.cs +++ b/Robust.UnitTesting/Shared/Prototypes/PrototypeManager_Test.cs @@ -1,15 +1,17 @@ -using System.IO; +using System.IO; +using JetBrains.Annotations; using NUnit.Framework; using Robust.Server.GameObjects; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Maths; using Robust.Shared.Prototypes; -using Robust.Shared.Utility; -using YamlDotNet.RepresentationModel; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; namespace Robust.UnitTesting.Shared.Prototypes { + [UsedImplicitly] [TestFixture] public class PrototypeManager_Test : RobustUnitTest { @@ -23,6 +25,7 @@ namespace Robust.UnitTesting.Shared.Prototypes factory.Register(); factory.RegisterClass(); + IoCManager.Resolve().Initialize(); manager = IoCManager.Resolve(); manager.LoadFromStream(new StringReader(DOCUMENT)); manager.Resync(); @@ -34,8 +37,8 @@ namespace Robust.UnitTesting.Shared.Prototypes var prototype = manager.Index("wrench"); Assert.That(prototype.Name, Is.EqualTo("Not a wrench. Tricked!")); - var mapping = prototype.Components["TestBasicPrototypeComponent"]; - Assert.That(mapping.GetNode("foo"), Is.EqualTo(new YamlScalarNode("bar!"))); + var mapping = prototype.Components["TestBasicPrototypeComponent"] as TestBasicPrototypeComponent; + Assert.That(mapping!.Foo, Is.EqualTo("bar!")); } [Test, Combinatorial] @@ -52,11 +55,9 @@ namespace Robust.UnitTesting.Shared.Prototypes Assert.That(prototype.Components, Contains.Key("PointLight")); }); - var componentData = prototype.Components["PointLight"]; - var expected = new YamlMappingNode(); - expected.Children[new YamlScalarNode("startState")] = new YamlScalarNode("Off"); + var componentData = prototype.Components["PointLight"] as PointLightComponent; - Assert.That(componentData, Is.EquivalentTo(expected)); + Assert.That(componentData!.NetSyncEnabled, Is.EqualTo(false)); } [Test] @@ -65,21 +66,22 @@ namespace Robust.UnitTesting.Shared.Prototypes var prototype = manager.Index("yamltester"); Assert.That(prototype.Components, Contains.Key("TestBasicPrototypeComponent")); - var componentData = prototype.Components["TestBasicPrototypeComponent"]; + var componentData = prototype.Components["TestBasicPrototypeComponent"] as TestBasicPrototypeComponent; - Assert.That(componentData["str"].AsString(), Is.EqualTo("hi!")); - Assert.That(componentData["int"].AsInt(), Is.EqualTo(10)); - Assert.That(componentData["float"].AsFloat(), Is.EqualTo(10f)); - Assert.That(componentData["float2"].AsFloat(), Is.EqualTo(10.5f)); - Assert.That(componentData["boolt"].AsBool(), Is.EqualTo(true)); - Assert.That(componentData["boolf"].AsBool(), Is.EqualTo(false)); - Assert.That(componentData["vec2"].AsVector2(), Is.EqualTo(new Vector2(1.5f, 1.5f))); - Assert.That(componentData["vec2i"].AsVector2i(), Is.EqualTo(new Vector2i(1, 1))); - Assert.That(componentData["vec3"].AsVector3(), Is.EqualTo(new Vector3(1.5f, 1.5f, 1.5f))); - Assert.That(componentData["vec4"].AsVector4(), Is.EqualTo(new Vector4(1.5f, 1.5f, 1.5f, 1.5f))); - Assert.That(componentData["color"].AsHexColor(), Is.EqualTo(new Color(0xAA, 0xBB, 0xCC, 0xFF))); - Assert.That(componentData["enumf"].AsEnum(), Is.EqualTo(YamlTestEnum.Foo)); - Assert.That(componentData["enumb"].AsEnum(), Is.EqualTo(YamlTestEnum.Bar)); + Assert.NotNull(componentData); + Assert.That(componentData!.Str, Is.EqualTo("hi!")); + Assert.That(componentData!.int_field, Is.EqualTo(10)); + Assert.That(componentData!.float_field, Is.EqualTo(10f)); + Assert.That(componentData!.float2_field, Is.EqualTo(10.5f)); + Assert.That(componentData!.boolt, Is.EqualTo(true)); + Assert.That(componentData!.boolf, Is.EqualTo(false)); + Assert.That(componentData!.vec2, Is.EqualTo(new Vector2(1.5f, 1.5f))); + Assert.That(componentData!.vec2i, Is.EqualTo(new Vector2i(1, 1))); + Assert.That(componentData!.vec3, Is.EqualTo(new Vector3(1.5f, 1.5f, 1.5f))); + Assert.That(componentData!.vec4, Is.EqualTo(new Vector4(1.5f, 1.5f, 1.5f, 1.5f))); + Assert.That(componentData!.color, Is.EqualTo(new Color(0xAA, 0xBB, 0xCC, 0xFF))); + Assert.That(componentData!.enumf, Is.EqualTo(YamlTestEnum.Foo)); + Assert.That(componentData!.enumb, Is.EqualTo(YamlTestEnum.Bar)); } [Test] @@ -111,7 +113,7 @@ namespace Robust.UnitTesting.Shared.Prototypes Assert.That(prototype.Name, Is.EqualTo(LoadStringTestDummyId)); } - private enum YamlTestEnum : byte + public enum YamlTestEnum : byte { Foo, Bar @@ -132,7 +134,7 @@ namespace Robust.UnitTesting.Shared.Prototypes - type: Transform - type: Sprite - type: PointLight - startState: Off + netsync: False - type: entity id: wallLightChild @@ -183,5 +185,33 @@ namespace Robust.UnitTesting.Shared.Prototypes public class TestBasicPrototypeComponent : Component { public override string Name => "TestBasicPrototypeComponent"; + + [DataField("foo")] public string Foo = null!; + + [DataField("str")] public string Str = null!; + + [DataField("int")] public int? int_field = null!; + + [DataField("float")] public float? float_field = null!; + + [DataField("float2")] public float? float2_field = null!; + + [DataField("boolt")] public bool? @boolt = null!; + + [DataField("boolf")] public bool? @boolf = null!; + + [DataField("vec2")] public Vector2 vec2 = default; + + [DataField("vec2i")] public Vector2i vec2i = default; + + [DataField("vec3")] public Vector3 vec3 = default; + + [DataField("vec4")] public Vector4 vec4 = default; + + [DataField("color")] public Color color = default; + + [DataField("enumf")] public PrototypeManager_Test.YamlTestEnum enumf = default; + + [DataField("enumb")] public PrototypeManager_Test.YamlTestEnum enumb = default; } } diff --git a/Robust.UnitTesting/Shared/Serialization/InheritanceSerializationTest.cs b/Robust.UnitTesting/Shared/Serialization/InheritanceSerializationTest.cs new file mode 100644 index 000000000..07bcbca70 --- /dev/null +++ b/Robust.UnitTesting/Shared/Serialization/InheritanceSerializationTest.cs @@ -0,0 +1,116 @@ +using NUnit.Framework; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Robust.UnitTesting.Shared.Serialization +{ + [TestFixture] + [TestOf(typeof(SerializationDataDefinition))] + public class InheritanceSerializationTest : RobustUnitTest + { + private const string BaseEntityId = "BaseEntity"; + private const string InheritorEntityId = "InheritorEntityId"; + private const string FinalEntityId = "FinalEntityId"; + + private const string BaseComponentFieldValue = "BaseFieldValue"; + private const string InheritorComponentFieldValue = "InheritorFieldValue"; + private const string FinalComponentFieldValue = "FinalFieldValue"; + + private static readonly string Prototypes = $@" +- type: entity + id: {BaseEntityId} + components: + - type: TestBase + baseField: {BaseComponentFieldValue} + +- type: entity + id: {InheritorEntityId} + components: + - type: TestInheritor + baseField: {BaseComponentFieldValue} + inheritorField: {InheritorComponentFieldValue} + +- type: entity + id: {FinalEntityId} + components: + - type: TestFinal + baseField: {BaseComponentFieldValue} + inheritorField: {InheritorComponentFieldValue} + finalField: {FinalComponentFieldValue}"; + + [Test] + public void Test() + { + var componentFactory = IoCManager.Resolve(); + + componentFactory.RegisterClass(); + componentFactory.RegisterClass(); + componentFactory.RegisterClass(); + + var serializationManager = IoCManager.Resolve(); + serializationManager.Initialize(); + + var componentManager = IoCManager.Resolve(); + componentManager.Initialize(); + + var prototypeManager = IoCManager.Resolve(); + + prototypeManager.LoadString(Prototypes); + + var entityManager = IoCManager.Resolve(); + + var mapManager = IoCManager.Resolve(); + mapManager.Initialize(); + mapManager.Startup(); + + var mapId = new MapId(1); + + mapManager.CreateMap(mapId); + + var coordinates = new MapCoordinates(0, 0, mapId); + + var baseEntity = entityManager.SpawnEntity(BaseEntityId, coordinates); + + Assert.That(baseEntity.TryGetComponent(out BaseComponent? baseComponent)); + Assert.That(baseComponent!.BaseField, Is.EqualTo(BaseComponentFieldValue)); + + var inheritorEntity = entityManager.SpawnEntity(InheritorEntityId, coordinates); + + Assert.That(inheritorEntity.TryGetComponent(out InheritorComponent? inheritorComponent)); + Assert.That(inheritorComponent!.BaseField, Is.EqualTo(BaseComponentFieldValue)); + Assert.That(inheritorComponent!.InheritorField, Is.EqualTo(InheritorComponentFieldValue)); + + var finalEntity = entityManager.SpawnEntity(FinalEntityId, coordinates); + + Assert.That(finalEntity.TryGetComponent(out FinalComponent? finalComponent)); + Assert.That(finalComponent!.BaseField, Is.EqualTo(BaseComponentFieldValue)); + Assert.That(finalComponent!.InheritorField, Is.EqualTo(InheritorComponentFieldValue)); + Assert.That(finalComponent!.FinalField, Is.EqualTo(FinalComponentFieldValue)); + } + } + + public class BaseComponent : Component + { + public override string Name => "TestBase"; + + [DataField("baseField")] public string? BaseField; + } + + public class InheritorComponent : BaseComponent + { + public override string Name => "TestInheritor"; + + [DataField("inheritorField")] public string? InheritorField; + } + + public class FinalComponent : InheritorComponent + { + public override string Name => "TestFinal"; + + [DataField("finalField")] public string? FinalField; + } +} diff --git a/Robust.UnitTesting/Shared/Serialization/SerializationPriorityTest.cs b/Robust.UnitTesting/Shared/Serialization/SerializationPriorityTest.cs new file mode 100644 index 000000000..193417aec --- /dev/null +++ b/Robust.UnitTesting/Shared/Serialization/SerializationPriorityTest.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.IO; +using NUnit.Framework; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Markdown; +using YamlDotNet.RepresentationModel; + +// ReSharper disable AccessToStaticMemberViaDerivedType + +namespace Robust.UnitTesting.Shared.Serialization +{ + public class SerializationPriorityTest : RobustUnitTest + { + [Test] + public void Test() + { + var serializationManager = IoCManager.Resolve(); + serializationManager.Initialize(); + + var prototype = @" +- type: PriorityTest + first: A + second: B + third: C"; + + var yamlStream = new YamlStream(); + yamlStream.Load(new StringReader(prototype)); + + var mapping = yamlStream.Documents[0].RootNode.ToDataNodeCast().Cast(0); + + var component = serializationManager.ReadValueOrThrow(mapping); + + Assert.That(component.Strings.Count, Is.EqualTo(3)); + Assert.That(component.First, Is.EqualTo("A")); + Assert.That(component.Second, Is.EqualTo("B")); + Assert.That(component.Third, Is.EqualTo("C")); + } + } + + public class PriorityTestComponent : Component, ISerializationHooks + { + public override string Name => "PriorityTest"; + + public readonly List Strings = new() {string.Empty, string.Empty, string.Empty}; + + [DataField("first", priority: 3)] + public string First + { + get => Strings[0]; + set => Strings.Add(value); + } + + [DataField("second", priority: 2)] + public string Second + { + get => Strings[1]; + set => Strings.Add(value); + } + + [DataField("third", priority: 1)] + public string Third + { + get => Strings[2]; + set => Strings.Add(value); + } + + void ISerializationHooks.AfterDeserialization() + { + Strings.RemoveRange(0, 3); + } + } +} diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/AngleSerializerTest.cs b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/AngleSerializerTest.cs new file mode 100644 index 000000000..db719903f --- /dev/null +++ b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/AngleSerializerTest.cs @@ -0,0 +1,37 @@ +using NUnit.Framework; +using Robust.Shared.Maths; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.TypeSerializers.Implementations; + +// ReSharper disable AccessToStaticMemberViaDerivedType + +namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers +{ + [TestFixture] + [TestOf(typeof(AngleSerializer))] + public class AngleSerializerTest : TypeSerializerTest + { + [Test] + public void SerializationTest() + { + var degrees = 75d; + var angle = Angle.FromDegrees(degrees); + var node = Serialization.WriteValueAs(angle); + var serializedValue = $"{MathHelper.DegreesToRadians(degrees)} rad"; + + Assert.That(node.Value, Is.EqualTo(serializedValue)); + } + + [Test] + public void DeserializationTest() + { + var degrees = 75; + var node = new ValueDataNode(degrees.ToString()); + var deserializedAngle = Serialization.ReadValue(node); + var angle = Angle.FromDegrees(degrees); + + Assert.That(deserializedAngle, Is.EqualTo(angle)); + } + } +} diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ArraySerializerTest.cs b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ArraySerializerTest.cs new file mode 100644 index 000000000..a490459fc --- /dev/null +++ b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ArraySerializerTest.cs @@ -0,0 +1,34 @@ +using NUnit.Framework; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic; + +// ReSharper disable AccessToStaticMemberViaDerivedType + +namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers +{ + [TestFixture] + [TestOf(typeof(ListSerializers<>))] + public class ArraySerializerTest : TypeSerializerTest + { + [Test] + public void SerializationTest() + { + var list = new[] {"A", "E"}; + var node = Serialization.WriteValueAs(list); + + Assert.That(node.Cast(0).Value, Is.EqualTo("A")); + Assert.That(node.Cast(1).Value, Is.EqualTo("E")); + } + + [Test] + public void DeserializationTest() + { + var list = new[] {"A", "E"}; + var node = new SequenceDataNode("A", "E"); + var deserializedList = Serialization.ReadValue(node); + + Assert.That(deserializedList, Is.EqualTo(list)); + } + } +} diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/Box2SerializerTest.cs b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/Box2SerializerTest.cs new file mode 100644 index 000000000..f6b794353 --- /dev/null +++ b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/Box2SerializerTest.cs @@ -0,0 +1,54 @@ +using NUnit.Framework; +using Robust.Shared.Maths; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.TypeSerializers.Implementations; + +// ReSharper disable AccessToStaticMemberViaDerivedType + +namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers +{ + [TestFixture] + [TestOf(typeof(Box2Serializer))] + public class Box2SerializerTest : TypeSerializerTest + { + [Test] + public void SerializationTest() + { + var left = 1; + var bottom = -2; + var right = -3; + var top = 4; + var str = $"{bottom},{left},{top},{right}"; + var box = new Box2(left, bottom, right, top); + var node = Serialization.WriteValueAs(box); + + Assert.That(node.Value, Is.EqualTo(str)); + } + + [Test] + public void DeserializationTest() + { + var left = 1; + var bottom = -2; + var right = -3; + var top = 4; + var str = $"{bottom},{left},{top},{right}"; + var node = new ValueDataNode(str); + var deserializedBox = Serialization.ReadValueOrThrow(node); + var box = new Box2(left, bottom, right, top); + + Assert.That(deserializedBox, Is.EqualTo(box)); + + Assert.That(deserializedBox.Left, Is.EqualTo(box.Left)); + Assert.That(deserializedBox.Bottom, Is.EqualTo(box.Bottom)); + Assert.That(deserializedBox.Right, Is.EqualTo(box.Right)); + Assert.That(deserializedBox.Top, Is.EqualTo(box.Top)); + + Assert.That(deserializedBox.BottomLeft, Is.EqualTo(box.BottomLeft)); + Assert.That(deserializedBox.BottomRight, Is.EqualTo(box.BottomRight)); + Assert.That(deserializedBox.TopLeft, Is.EqualTo(box.TopLeft)); + Assert.That(deserializedBox.TopRight, Is.EqualTo(box.TopRight)); + } + } +} diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ColorSerializerTest.cs b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ColorSerializerTest.cs new file mode 100644 index 000000000..78402f573 --- /dev/null +++ b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ColorSerializerTest.cs @@ -0,0 +1,44 @@ +using NUnit.Framework; +using Robust.Shared.Maths; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.TypeSerializers.Implementations; + +// ReSharper disable AccessToStaticMemberViaDerivedType + +namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers +{ + [TestFixture] + [TestOf(typeof(ColorSerializer))] + public class ColorSerializerTest : TypeSerializerTest + { + [Test] + public void SerializationTest() + { + byte r = 123; + byte g = 5; + byte b = 40; + byte a = 55; + var color = new Color(r, g, b, a); + var node = Serialization.WriteValueAs(color); + var str = $"#{(r << 24) + (g << 16) + (b << 8) + a:X8}"; + + Assert.That(node.Value, Is.EqualTo(str)); + } + + [Test] + public void DeserializationTest() + { + byte r = 123; + byte g = 5; + byte b = 40; + byte a = 55; + var str = $"#{(r << 24) + (g << 16) + (b << 8) + a:X8}"; + var node = new ValueDataNode(str); + var deserializedColor = Serialization.ReadValueOrThrow(node); + var color = new Color(r, g, b, a); + + Assert.That(deserializedColor.ToString(), Is.EqualTo(color.ToString())); + } + } +} diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs new file mode 100644 index 000000000..307ce8550 --- /dev/null +++ b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs @@ -0,0 +1,60 @@ +using System.IO; +using NUnit.Framework; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.TypeSerializers.Implementations; +using YamlDotNet.RepresentationModel; +using static Robust.Shared.Prototypes.EntityPrototype; +// ReSharper disable AccessToStaticMemberViaDerivedType + +namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers +{ + [TestFixture] + [TestOf(typeof(ComponentRegistrySerializer))] + public class ComponentRegistrySerializerTest : TypeSerializerTest + { + [OneTimeSetUp] + public new void OneTimeSetup() + { + var componentFactory = IoCManager.Resolve(); + componentFactory.RegisterClass(); + } + + [Test] + public void SerializationTest() + { + var component = new TestComponent(); + var registry = new ComponentRegistry {{"Test", component}}; + var node = Serialization.WriteValueAs(registry); + + Assert.That(node.Sequence.Count, Is.EqualTo(1)); + Assert.IsInstanceOf(node[0]); + + var mapping = node.Cast(0); + Assert.That(mapping.Cast("type").Value, Is.EqualTo("Test")); + } + + [Test] + public void DeserializationTest() + { + var str = "- type: Test"; + var yamlStream = new YamlStream(); + yamlStream.Load(new StringReader(str)); + + var mapping = yamlStream.Documents[0].RootNode.ToDataNodeCast(); + + var deserializedRegistry = Serialization.ReadValueOrThrow(mapping); + + Assert.That(deserializedRegistry.Count, Is.EqualTo(1)); + Assert.That(deserializedRegistry.ContainsKey("Test")); + Assert.IsInstanceOf(deserializedRegistry["Test"]); + } + } + + public class TestComponent : Component + { + public override string Name => "Test"; + } +} diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/DictionarySerializerTest.cs b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/DictionarySerializerTest.cs new file mode 100644 index 000000000..cd080d19c --- /dev/null +++ b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/DictionarySerializerTest.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using NUnit.Framework; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic; + +// ReSharper disable AccessToStaticMemberViaDerivedType + +namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers +{ + [TestFixture] + [TestOf(typeof(DictionarySerializer<,>))] + public class DictionarySerializerTest : TypeSerializerTest + { + [Test] + public void SerializationTest() + { + var dictionary = new Dictionary + { + [1] = "A", + [2] = "B", + [3] = "C" + }; + var node = Serialization.WriteValueAs(dictionary); + + Assert.That(node.Cast("1").Value, Is.EqualTo("A")); + Assert.That(node.Cast("2").Value, Is.EqualTo("B")); + Assert.That(node.Cast("3").Value, Is.EqualTo("C")); + } + + [Test] + public void DeserializationTest() + { + var dictionary = new Dictionary + { + [1] = "A", + [2] = "B", + [3] = "C" + }; + var node = new MappingDataNode(); + + node.AddNode("1", new ValueDataNode("A")); + node.AddNode("2", new ValueDataNode("B")); + node.AddNode("3", new ValueDataNode("C")); + + var deserializedDictionary = Serialization.ReadValue>(node); + + Assert.That(deserializedDictionary, Is.EqualTo(dictionary)); + } + } +} diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/HashSetSerializerTest.cs b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/HashSetSerializerTest.cs new file mode 100644 index 000000000..7e9aac717 --- /dev/null +++ b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/HashSetSerializerTest.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using NUnit.Framework; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic; + +// ReSharper disable AccessToStaticMemberViaDerivedType + +namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers +{ + [TestFixture] + [TestOf(typeof(HashSetSerializer<>))] + public class HashSetSerializerTest : TypeSerializerTest + { + [Test] + public void SerializationTest() + { + var list = new HashSet {"A", "E"}; + var node = Serialization.WriteValueAs(list); + + Assert.That(node.Cast(0).Value, Is.EqualTo("A")); + Assert.That(node.Cast(1).Value, Is.EqualTo("E")); + } + + [Test] + public void DeserializationTest() + { + var list = new HashSet {"A", "E"}; + var node = new SequenceDataNode("A", "E"); + var deserializedList = Serialization.ReadValue>(node); + + Assert.That(deserializedList, Is.EqualTo(list)); + } + } +} diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ListSerializerTest.cs b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ListSerializerTest.cs new file mode 100644 index 000000000..1eb0bdeb5 --- /dev/null +++ b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ListSerializerTest.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using NUnit.Framework; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic; + +// ReSharper disable AccessToStaticMemberViaDerivedType + +namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers +{ + [TestFixture] + [TestOf(typeof(ListSerializers<>))] + public class ListSerializerTest : TypeSerializerTest + { + [Test] + public void SerializationTest() + { + var list = new List {"A", "E"}; + var node = Serialization.WriteValueAs(list); + + Assert.That(node.Cast(0).Value, Is.EqualTo("A")); + Assert.That(node.Cast(1).Value, Is.EqualTo("E")); + } + + [Test] + public void DeserializationTest() + { + var list = new List {"A", "E"}; + var node = new SequenceDataNode("A", "E"); + var deserializedList = Serialization.ReadValue>(node); + + Assert.That(deserializedList, Is.EqualTo(list)); + } + } +} diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/RegexSerializerTest.cs b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/RegexSerializerTest.cs new file mode 100644 index 000000000..716bab01f --- /dev/null +++ b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/RegexSerializerTest.cs @@ -0,0 +1,37 @@ +using System.Text.RegularExpressions; +using NUnit.Framework; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.TypeSerializers.Implementations; + +// ReSharper disable AccessToStaticMemberViaDerivedType + +namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers +{ + [TestFixture] + [TestOf(typeof(RegexSerializer))] + public class RegexSerializerTest : TypeSerializerTest + { + [Test] + public void SerializationTest() + { + var str = "[AEIOU]"; + var regex = new Regex(str); + var node = Serialization.WriteValueAs(regex); + + Assert.That(node.Value, Is.EqualTo(str)); + } + + [Test] + public void DeserializationTest() + { + var str = "[AEIOU]"; + var node = new ValueDataNode(str); + var deserializedRegex = Serialization.ReadValueOrThrow(node); + var regex = new Regex(str, RegexOptions.Compiled); + + Assert.That(deserializedRegex.ToString(), Is.EqualTo(regex.ToString())); + Assert.That(deserializedRegex.Options, Is.EqualTo(regex.Options)); + } + } +} diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/TypeSerializerTest.cs b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/TypeSerializerTest.cs new file mode 100644 index 000000000..b95857470 --- /dev/null +++ b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/TypeSerializerTest.cs @@ -0,0 +1,17 @@ +using NUnit.Framework; +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager; + +namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers +{ + public abstract class TypeSerializerTest : RobustUnitTest + { + protected ISerializationManager Serialization => IoCManager.Resolve(); + + [OneTimeSetUp] + public void OneTimeSetup() + { + Serialization.Initialize(); + } + } +} diff --git a/Robust.UnitTesting/Shared/Serialization/YamlConstantSerializer_Test.cs b/Robust.UnitTesting/Shared/Serialization/YamlConstantSerializer_Test.cs deleted file mode 100644 index e938fa479..000000000 --- a/Robust.UnitTesting/Shared/Serialization/YamlConstantSerializer_Test.cs +++ /dev/null @@ -1,70 +0,0 @@ -using NUnit.Framework; -using Robust.Shared.Serialization; -using Robust.UnitTesting.Shared.Serialization.YamlObjectSerializerTests; -using YamlDotNet.RepresentationModel; - -namespace Robust.UnitTesting.Shared.Serialization -{ - [TestFixture] - [TestOf(typeof(YamlConstantSerializer))] - class YamlConstantSerializer_Test : RobustUnitTest - { - public sealed class GenericConstantTag {} - - [Test] - public void SerializeOneConstantTest() - { - // Arrange - var data = (int)GenericConstants.One; - var mapping = new YamlMappingNode(); - var serializer = YamlObjectSerializer.NewWriter(mapping); - - // Act - serializer.DataField(ref data, "generic_constants", 0, WithFormat.Constants()); - - // Assert - var result = YamlObjectSerializer_Test.NodeToYamlText(mapping); - Assert.That(result, Is.EqualTo(SerializedOneConstant)); - } - - [Test] - public void DeserializeOneConstantTest() - { - // Arrange - var data = 0; - var rootNode = YamlObjectSerializer_Test.YamlTextToNode(SerializedOneConstant); - var serializer = YamlObjectSerializer.NewReader(rootNode); - - // Act - serializer.DataField(ref data, "generic_constants", 0, WithFormat.Constants()); - - // Assert - Assert.That(data, Is.EqualTo((int)GenericConstants.One)); - } - - [Test] - public void DeserializeLegacyFormatTest() - { - // Arrange - var data = 0; - var rootNode = YamlObjectSerializer_Test.YamlTextToNode(SerializedThree); - var serializer = YamlObjectSerializer.NewReader(rootNode); - - // Act - serializer.DataField(ref data, "generic_constants", 0, WithFormat.Constants()); - - // Assert - Assert.That(data, Is.EqualTo((int)GenericConstants.Three)); - } - - private const string SerializedOneConstant = "generic_constants: One\n...\n"; - private const string SerializedThree = "generic_constants: 3\n...\n"; - - [ConstantsFor(typeof(GenericConstantTag))] - private enum GenericConstants - { - One = 1, - Three = 3, - } - } -} diff --git a/Robust.UnitTesting/Shared/Serialization/YamlFlagSerializer_Test.cs b/Robust.UnitTesting/Shared/Serialization/YamlFlagSerializer_Test.cs deleted file mode 100644 index 32c8faae1..000000000 --- a/Robust.UnitTesting/Shared/Serialization/YamlFlagSerializer_Test.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System; -using NUnit.Framework; -using Robust.Shared.Serialization; -using Robust.UnitTesting.Shared.Serialization.YamlObjectSerializerTests; -using YamlDotNet.RepresentationModel; - -namespace Robust.UnitTesting.Shared.Serialization -{ - [TestFixture] - [TestOf(typeof(YamlFlagSerializer))] - class YamlFlagSerializer_Test : RobustUnitTest - { - public sealed class GenericFlagTag {} - public sealed class GenericFlagWithZeroTag {} - - [Test] - public void SerializeOneFlagTest() - { - // Arrange - var data = (int)GenericFlags.One; - var mapping = new YamlMappingNode(); - var serializer = YamlObjectSerializer.NewWriter(mapping); - - // Act - serializer.DataField(ref data, "generic_flags", 0, WithFormat.Flags()); - - // Assert - var result = YamlObjectSerializer_Test.NodeToYamlText(mapping); - Assert.That(result, Is.EqualTo(SerializedOneFlag)); - } - - [Test] - public void DeserializeOneFlagTest() - { - // Arrange - var data = 0; - var rootNode = YamlObjectSerializer_Test.YamlTextToNode(SerializedOneFlag); - var serializer = YamlObjectSerializer.NewReader(rootNode); - - // Act - serializer.DataField(ref data, "generic_flags", 0, WithFormat.Flags()); - - // Assert - Assert.That(data, Is.EqualTo((int)GenericFlags.One)); - } - - [Test] - public void SerializeFiveFlagTest() - { - // Arrange - var data = (int)GenericFlags.Five; - var mapping = new YamlMappingNode(); - var serializer = YamlObjectSerializer.NewWriter(mapping); - - // Act - serializer.DataField(ref data, "generic_flags", 0, WithFormat.Flags()); - - // Assert - var result = YamlObjectSerializer_Test.NodeToYamlText(mapping); - Assert.That(result, Is.EqualTo(SerializedOneFourFlag)); - } - - [Test] - public void DeserializeFiveFlagTest() - { - // Arrange - var data = 0; - var rootNode = YamlObjectSerializer_Test.YamlTextToNode(SerializedFiveFlag); - var serializer = YamlObjectSerializer.NewReader(rootNode); - - // Act - serializer.DataField(ref data, "generic_flags", 0, WithFormat.Flags()); - - // Assert - Assert.That(data, Is.EqualTo((int)GenericFlags.Five)); - } - - [Test] - public void SerializeZeroWithoutFlagTest() - { - // Arrange - var data = 0; - var mapping = new YamlMappingNode(); - var serializer = YamlObjectSerializer.NewWriter(mapping); - - // Act - serializer.DataField(ref data, "generic_flags", 0, WithFormat.Flags(), alwaysWrite: true); - - // Assert - var result = YamlObjectSerializer_Test.NodeToYamlText(mapping); - Assert.That(result, Is.EqualTo(SerializedZeroNum)); - } - - [Test] - public void DeserializeZeroWithoutFlagTest() - { - // Arrange - var data = 0; - var rootNode = YamlObjectSerializer_Test.YamlTextToNode(SerializedZeroNum); - var serializer = YamlObjectSerializer.NewReader(rootNode); - - // Act - serializer.DataField(ref data, "generic_flags", 0, WithFormat.Flags()); - - // Assert - Assert.That(data, Is.EqualTo(0)); - } - - private const string SerializedOneFlag = "generic_flags:\n- One\n...\n"; - private const string SerializedOneFourFlag = "generic_flags:\n- One\n- Four\n...\n"; - private const string SerializedFiveFlag = "generic_flags:\n- Five\n...\n"; - private const string SerializedZeroNum = "generic_flags: 0\n...\n"; - - [Test] - public void SerializeZeroWithFlagTest() - { - // Arrange - var data = 0; - var mapping = new YamlMappingNode(); - var serializer = YamlObjectSerializer.NewWriter(mapping); - - // Act - serializer.DataField(ref data, "generic_flags_with_zero", 0, WithFormat.Flags(), alwaysWrite: true); - - // Assert - var result = YamlObjectSerializer_Test.NodeToYamlText(mapping); - Assert.That(result, Is.EqualTo(SerializedZeroFlag)); - } - - [Test] - public void DeserializeZeroWithFlagTest() - { - // Arrange - var data = 0; - var rootNode = YamlObjectSerializer_Test.YamlTextToNode(SerializedZeroFlag); - var serializer = YamlObjectSerializer.NewReader(rootNode); - - // Act - serializer.DataField(ref data, "generic_flags_with_zero", 0, WithFormat.Flags()); - - // Assert - Assert.That(data, Is.EqualTo(0)); - } - - [Test] - public void SerializeNonZeroWithZeroFlagDoesntShowZeroTest() - { - // Arrange - var data = (int)FlagsWithZero.Two; - var mapping = new YamlMappingNode(); - var serializer = YamlObjectSerializer.NewWriter(mapping); - - // Act - serializer.DataField(ref data, "generic_flags_with_zero", 0, WithFormat.Flags()); - - // Assert - var result = YamlObjectSerializer_Test.NodeToYamlText(mapping); - Assert.That(result, Is.EqualTo(SerializedTwoWithZeroFlag)); - } - - private const string SerializedZeroFlag = "generic_flags_with_zero:\n- None\n...\n"; - private const string SerializedTwoWithZeroFlag = "generic_flags_with_zero:\n- Two\n...\n"; - - [Flags] - [FlagsFor(typeof(GenericFlagTag))] - private enum GenericFlags - { - One = 1, - Two = 2, - Four = 4, - Five = 5, - } - - [Flags] - [FlagsFor(typeof(GenericFlagWithZeroTag))] - private enum FlagsWithZero - { - None = 0, - One = 1, - Two = 2, - Four = 4, - Five = 5, - } - } -} diff --git a/Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/ImmutableListSerializationTest.cs b/Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/ImmutableListSerializationTest.cs index 89bf0577d..ac38e6060 100644 --- a/Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/ImmutableListSerializationTest.cs +++ b/Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/ImmutableListSerializationTest.cs @@ -1,29 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; +using System.Collections.Immutable; +using System.IO; using NUnit.Framework; -using Robust.Shared.Serialization; +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Utility; using YamlDotNet.RepresentationModel; -using static Robust.UnitTesting.Shared.Serialization.YamlObjectSerializerTests.YamlObjectSerializer_Test; + // ReSharper disable AccessToStaticMemberViaDerivedType namespace Robust.UnitTesting.Shared.Serialization.YamlObjectSerializerTests { - [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] [TestFixture] - [TestOf(typeof(YamlObjectSerializer))] - public class ImmutableListSerializationTest + public class ImmutableListSerializationTest : RobustUnitTest { + [OneTimeSetUp] + public void Setup() + { + IoCManager.Resolve().Initialize(); + } + [Test] public void SerializeListTest() { // Arrange var data = _serializableList; - var mapping = new YamlMappingNode(); - var serializer = YamlObjectSerializer.NewWriter(mapping); - - // Act - serializer.DataField(ref data, "datalist", ImmutableList.Empty); + var serMan = IoCManager.Resolve(); + var sequence = (SequenceDataNode) serMan.WriteValue(data); + var mapping = new MappingDataNode(); + mapping.AddNode("datalist", sequence); // Assert var result = NodeToYamlText(mapping); @@ -34,12 +39,11 @@ namespace Robust.UnitTesting.Shared.Serialization.YamlObjectSerializerTests public void DeserializeListTest() { // Arrange - ImmutableList data = null!; + var serMan = IoCManager.Resolve(); var rootNode = YamlTextToNode(_serializedListYaml); - var serializer = YamlObjectSerializer.NewReader(rootNode); // Act - serializer.DataField(ref data, "datalist", ImmutableList.Empty); + var data = serMan.ReadValueOrThrow>(new SequenceDataNode((YamlSequenceNode)rootNode.Children[0].Value)); // Assert Assert.That(data, Is.Not.Null); @@ -53,5 +57,41 @@ namespace Robust.UnitTesting.Shared.Serialization.YamlObjectSerializerTests private readonly string _serializedListYaml = "datalist:\n- 1\n- 2\n- 3\n...\n"; private readonly ImmutableList _serializableList = ImmutableList.Create(1, 2, 3); + + // serializes a node tree into text + internal static string NodeToYamlText(DataNode root) + { + var document = new YamlDocument(root.ToYamlNode()); + + using var stream = new MemoryStream(); + using var writer = new StreamWriter(stream) {NewLine = "\n"}; + + var yamlStream = new YamlStream(document); + yamlStream.Save(writer); + writer.Flush(); + return EncodingHelpers.UTF8.GetString(stream.ToArray()); + } + + // deserializes yaml text, loads the first document, and returns the first entity + internal static YamlMappingNode YamlTextToNode(string text) + { + using var stream = new MemoryStream(); + + // create a stream for testing + var writer = new StreamWriter(stream); + writer.Write(text); + writer.Flush(); + stream.Position = 0; + + // deserialize stream + var reader = new StreamReader(stream); + var yamlStream = new YamlStream(); + yamlStream.Load(reader); + + // read first document + var firstDoc = yamlStream.Documents[0]; + + return (YamlMappingNode)firstDoc.RootNode; + } } } diff --git a/Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/TypePropertySerialization_Test.cs b/Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/TypePropertySerialization_Test.cs index aed02baec..e316aa484 100644 --- a/Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/TypePropertySerialization_Test.cs +++ b/Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/TypePropertySerialization_Test.cs @@ -1,16 +1,26 @@ using System.IO; using NUnit.Framework; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Markdown; using YamlDotNet.RepresentationModel; + // ReSharper disable AccessToStaticMemberViaDerivedType namespace Robust.UnitTesting.Shared.Serialization.YamlObjectSerializerTests { [TestFixture] - [TestOf(typeof(YamlObjectSerializer))] public class TypePropertySerialization_Test : RobustUnitTest { + [OneTimeSetUp] + public void Setup() + { + IoCManager.Resolve().Initialize(); + } + [Test] public void SerializeTypePropertiesTest() { @@ -19,24 +29,23 @@ namespace Robust.UnitTesting.Shared.Serialization.YamlObjectSerializerTests TestPropertyOne = "B", TestPropertyTwo = 10 }; - var mapping = new YamlMappingNode(); - var writer = YamlObjectSerializer.NewWriter(mapping); - - writer.DataField(ref type, "test", null!); + var serMan = IoCManager.Resolve(); + var mapping = (MappingDataNode) serMan.WriteValue(type); Assert.IsNotEmpty(mapping.Children); - var testPropertyOne = (YamlScalarNode) mapping["test"]["testPropertyOne"]; - var testPropertyTwo = (YamlScalarNode) mapping["test"]["testPropertyTwo"]; + var testPropertyOne = mapping.GetNode("testPropertyOne") as ValueDataNode; + var testPropertyTwo = mapping.GetNode("testPropertyTwo") as ValueDataNode; - Assert.That(testPropertyOne.Value, Is.EqualTo("B")); - Assert.That(testPropertyTwo.Value, Is.EqualTo("10")); + Assert.NotNull(testPropertyOne); + Assert.NotNull(testPropertyTwo); + Assert.That(testPropertyOne!.Value, Is.EqualTo("B")); + Assert.That(testPropertyTwo!.Value, Is.EqualTo("10")); } [Test] public void DeserializeTypePropertiesTest() { - ITestType? type = null; var yaml = @" - test: !type:testtype2 @@ -57,8 +66,8 @@ namespace Robust.UnitTesting.Shared.Serialization.YamlObjectSerializerTests var mapping = (YamlMappingNode) yamlStream.Documents[0].RootNode[0]; - var reader = YamlObjectSerializer.NewReader(mapping); - reader.DataField(ref type, "test", null); + var serMan = IoCManager.Resolve(); + var type = serMan.ReadValue(mapping["test"].ToDataNode()); Assert.NotNull(type); Assert.IsInstanceOf(type); @@ -71,17 +80,14 @@ namespace Robust.UnitTesting.Shared.Serialization.YamlObjectSerializerTests } [SerializedType("testtype2")] + [DataDefinition] public class TestTypeTwo : ITestType { + [DataField("testPropertyOne")] public string? TestPropertyOne { get; set; } + [DataField("testPropertyTwo")] public int TestPropertyTwo { get; set; } - - void IExposeData.ExposeData(ObjectSerializer serializer) - { - serializer.DataField(this, x => TestPropertyOne, "testPropertyOne", null); - serializer.DataField(this, x => TestPropertyTwo, "testPropertyTwo", 0); - } } [RegisterComponent] @@ -89,13 +95,6 @@ namespace Robust.UnitTesting.Shared.Serialization.YamlObjectSerializerTests { public override string Name => "Test"; - public ITestType? TestType { get; set; } - - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - - serializer.DataField(this, x => x.TestType, "testType", null); - } + [DataField("testType")] public ITestType? TestType { get; set; } } } diff --git a/Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/TypeSerialization_Test.cs b/Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/TypeSerialization_Test.cs index aa3847223..56a884fe3 100644 --- a/Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/TypeSerialization_Test.cs +++ b/Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/TypeSerialization_Test.cs @@ -1,39 +1,47 @@ using System.IO; using NUnit.Framework; +using Robust.Shared.IoC; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Markdown; using YamlDotNet.RepresentationModel; + // ReSharper disable AccessToStaticMemberViaDerivedType namespace Robust.UnitTesting.Shared.Serialization.YamlObjectSerializerTests { [TestFixture] - [TestOf(typeof(YamlObjectSerializer))] public class TypeSerialization_Test : RobustUnitTest { + [OneTimeSetUp] + public void Setup() + { + IoCManager.Resolve().Initialize(); + } + [Test] public void SerializeTypeTest() { - ITestType? type = new TestTypeOne(); - var mapping = new YamlMappingNode(); - var writer = YamlObjectSerializer.NewWriter(mapping); + ITestType type = new TestTypeOne(); + var serMan = IoCManager.Resolve(); + var mapping = serMan.WriteValue(type); - writer.DataField(ref type, "type", null!); + Assert.IsInstanceOf(mapping); - Assert.IsNotEmpty(mapping.Children); - Assert.IsInstanceOf(mapping.Children[0].Key); + var scalar = (MappingDataNode) mapping; - var scalar = (YamlScalarNode) mapping.Children[0].Key; - - Assert.That(scalar.Value, Is.EqualTo("type")); + Assert.That(scalar.Children.Count, Is.EqualTo(0)); + Assert.That(scalar.Tag, Is.EqualTo("!type:TestTypeOne")); } [Test] public void DeserializeTypeTest() { - ITestType? type = null; var yaml = @" test: - !type:testtype1"; + !type:testtype1 + {}"; using var stream = new MemoryStream(); @@ -47,20 +55,19 @@ test: yamlStream.Load(streamReader); var mapping = (YamlMappingNode) yamlStream.Documents[0].RootNode; - - var reader = YamlObjectSerializer.NewReader(mapping); - reader.DataField(ref type, "test", null); + var serMan = IoCManager.Resolve(); + var type = serMan.ReadValue(new MappingDataNode(mapping)["test"]); Assert.NotNull(type); Assert.IsInstanceOf(type); } } - public interface ITestType : IExposeData { } + public interface ITestType { } [SerializedType("testtype1")] + [DataDefinition] public class TestTypeOne : ITestType { - void IExposeData.ExposeData(ObjectSerializer serializer) { } } } diff --git a/Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/YamlObjectSerializer_Test.cs b/Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/YamlObjectSerializer_Test.cs deleted file mode 100644 index 2c88a3df4..000000000 --- a/Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/YamlObjectSerializer_Test.cs +++ /dev/null @@ -1,575 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using NUnit.Framework; -using Robust.Shared.Maths; -using Robust.Shared.Serialization; -using Robust.Shared.Utility; -using YamlDotNet.RepresentationModel; - -// ReSharper disable AccessToStaticMemberViaDerivedType - -namespace Robust.UnitTesting.Shared.Serialization.YamlObjectSerializerTests -{ - [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] - [TestFixture] - [TestOf(typeof(YamlObjectSerializer))] - class YamlObjectSerializer_Test - { - [Test] - public void SerializeListTest() - { - // Arrange - var data = SerializableList; - var mapping = new YamlMappingNode(); - var serializer = YamlObjectSerializer.NewWriter(mapping); - - // Act - serializer.DataField(ref data, "datalist", new List(0)); - - // Assert - var result = NodeToYamlText(mapping); - Assert.That(result, Is.EqualTo(SerializedListYaml)); - } - - [Test] - public void SerializeListAsReadOnlyCollectionTest() - { - // Arrange - IReadOnlyCollection data = SerializableList; - var mapping = new YamlMappingNode(); - var serializer = YamlObjectSerializer.NewWriter(mapping); - - // Act - serializer.DataField(ref data, "datalist", Array.Empty()); - - // Assert - var result = NodeToYamlText(mapping); - Assert.That(result, Is.EqualTo(SerializedListYaml)); - } - - [Test] - public void SerializeListAsReadOnlyListTest() - { - // Arrange - IReadOnlyList data = SerializableList; - var mapping = new YamlMappingNode(); - var serializer = YamlObjectSerializer.NewWriter(mapping); - - // Act - serializer.DataField(ref data, "datalist", Array.Empty()); - - // Assert - var result = NodeToYamlText(mapping); - Assert.That(result, Is.EqualTo(SerializedListYaml)); - } - - [Test] - public void DeserializeListTest() - { - // Arrange - List data = null!; - var rootNode = YamlTextToNode(SerializedListYaml); - var serializer = YamlObjectSerializer.NewReader(rootNode); - - // Act - serializer.DataField(ref data, "datalist", new List(0)); - - // Assert - Assert.That(data, Is.Not.Null); - Assert.That(data.Count, Is.EqualTo(SerializableList.Count)); - for (var i = 0; i < SerializableList.Count; i++) - Assert.That(data[i], Is.EqualTo(SerializableList[i])); - } - - [Test] - public void DeserializeListAsReadOnlyListTest() - { - // Arrange - IReadOnlyList data = null!; - var rootNode = YamlTextToNode(SerializedListYaml); - var serializer = YamlObjectSerializer.NewReader(rootNode); - - // Act - serializer.DataField(ref data, "datalist", Array.Empty()); - - // Assert - Assert.That(data, Is.Not.Null); - Assert.That(data.Count, Is.EqualTo(SerializableList.Count)); - for (var i = 0; i < SerializableList.Count; i++) - Assert.That(data[i], Is.EqualTo(SerializableList[i])); - } - - [Test] - public void DeserializeListAsReadOnlyCollectionTest() - { - // Arrange - IReadOnlyCollection data = null!; - var rootNode = YamlTextToNode(SerializedListYaml); - var serializer = YamlObjectSerializer.NewReader(rootNode); - - // Act - serializer.DataField(ref data, "datalist", Array.Empty()); - - // Assert - Assert.That(data, Is.EquivalentTo(SerializableList)); - } - - - private readonly string SerializedListYaml = "datalist:\n- 1\n- 2\n- 3\n...\n"; - private readonly List SerializableList = new() { 1, 2, 3 }; - - [Test] - public void SerializeDictTest() - { - // Arrange - var data = SerializableDict; - var mapping = new YamlMappingNode(); - var serializer = YamlObjectSerializer.NewWriter(mapping); - - // Act - serializer.DataField(ref data, "datadict", new Dictionary(0)); - - // Assert - var result = NodeToYamlText(mapping); - Assert.That(result, Is.EqualTo(SerializedDictYaml)); - } - - [Test] - public void SerializeDictAsReadOnlyTest() - { - // Arrange - IReadOnlyDictionary data = SerializableDict; - var mapping = new YamlMappingNode(); - var serializer = YamlObjectSerializer.NewWriter(mapping); - - // Act - serializer.DataField(ref data, "datadict", new Dictionary(0)); - - // Assert - var result = NodeToYamlText(mapping); - Assert.That(result, Is.EqualTo(SerializedDictYaml)); - } - - [Test] - public void DeserializeDictTest() - { - Dictionary data = null!; - var rootNode = YamlTextToNode(SerializedDictYaml); - var serializer = YamlObjectSerializer.NewReader(rootNode); - - // Act - serializer.DataField(ref data, "datadict", new Dictionary(0)); - - // Assert - Assert.That(data, Is.Not.Null); - Assert.That(data.Count, Is.EqualTo(SerializableDict.Count)); - foreach (var kvEntry in SerializableDict) - Assert.That(data[kvEntry.Key], Is.EqualTo(kvEntry.Value)); - } - - [Test] - public void DeserializeDictToReadOnlyTest() - { - IReadOnlyDictionary data = null!; - var rootNode = YamlTextToNode(SerializedDictYaml); - var serializer = YamlObjectSerializer.NewReader(rootNode); - - // Act - serializer.DataField(ref data, "datadict", new Dictionary(0)); - - // Assert - Assert.That(data, Is.Not.Null); - Assert.That(data.Count, Is.EqualTo(SerializableDict.Count)); - foreach (var kvEntry in SerializableDict) - Assert.That(data[kvEntry.Key], Is.EqualTo(kvEntry.Value)); - } - - [Test] - public void DeserializeExpressionTest() - { - var dummy = new DummyClass(); - - var rootNode = YamlTextToNode("foo: 5\nbar: \"baz\""); - var serializer = YamlObjectSerializer.NewReader(rootNode); - serializer.CurrentType = typeof(DummyClass); - - serializer.DataField(dummy, d => d.Foo, "foo", 4); - serializer.DataField(dummy, d => d.Bar, "bar", "honk"); - serializer.DataField(dummy, d => d.Baz, "baz", Color.Black); - - Assert.That(dummy.Foo, Is.EqualTo(5)); - Assert.That(dummy.Bar, Is.EqualTo("baz")); - Assert.That(dummy.Baz, Is.EqualTo(Color.Black)); - } - - [Test] - public void SerializeExpressionTest() - { - var dummy = new DummyClass - { - Bar = "honk!", - Baz = Color.Black, - Foo = 5 - }; - - var mapping = new YamlMappingNode(); - var serializer = YamlObjectSerializer.NewWriter(mapping); - serializer.CurrentType = typeof(DummyClass); - - serializer.DataField(dummy, d => d.Foo, "foo", 1); - serializer.DataField(dummy, d => d.Bar, "bar", "*silence*"); - serializer.DataField(dummy, d => d.Baz, "baz", Color.Black); - - Assert.That(mapping, Is.EquivalentTo(new YamlMappingNode {{"bar", "honk!"}, {"foo", "5"}})); - } - - [Test] - public void SerializedEqualDictTest() - { - var dict = new Dictionary - { - ["A"] = "B", - ["C"] = "W", - ["D"] = "G", - ["E"] = "J" - }; - - var dict2 = new Dictionary(dict); - - Assert.That(YamlObjectSerializer.IsSerializedEqual(dict, dict2), Is.True); - } - - [Test] - public void TestSelfSerialize() - { - var mapping = new YamlMappingNode(); - var reader = YamlObjectSerializer.NewWriter(mapping); - - var field = new SelfSerializeTest {Value = 456}; - - reader.DataField(ref field, "foo", default); - - Assert.That(mapping["foo"].AsString(), Is.EqualTo("456")); - } - - [Test] - public void TestSelfDeserialize() - { - var dict = new YamlMappingNode - { - {"foo", "123"} - }; - - var reader = YamlObjectSerializer.NewReader(dict); - - SelfSerializeTest field = default; - - reader.DataField(ref field, "foo", default); - - Assert.That(field.Value, Is.EqualTo(123)); - } - - private readonly string SerializedDictYaml = "datadict:\n val1: 1\n val2: 2\n...\n"; - private readonly Dictionary SerializableDict = new() { { "val1", 1 }, { "val2", 2 } }; - - [Test] - public void SerializeHashSetTest() - { - var mapping = new YamlMappingNode(); - var set = SerializableSet; - - var writer = YamlObjectSerializer.NewWriter(mapping); - - writer.DataField(ref set, "dataSet", new HashSet(0)); - - var result = NodeToYamlText(mapping); - Assert.That(result, Is.EqualTo(SerializedSetYaml)); - } - - [Test] - public void DeserializeHashSetTest() - { - HashSet data = null!; - var rootNode = YamlTextToNode(SerializedSetYaml); - var serializer = YamlObjectSerializer.NewReader(rootNode); - - serializer.DataField(ref data, "dataSet", new HashSet(0)); - - Assert.That(data, Is.Not.Null); - Assert.That(data.Count, Is.EqualTo(SerializableSet.Count)); - for (var i = 0; i < SerializableSet.Count; i++) - Assert.That(data.ElementAt(i), Is.EqualTo(SerializableSet.ElementAt(i))); - } - - [Test] - public void SerializedEqualHashSetTest() - { - var set = new HashSet {"A", "B", "C", "D", "E"}; - var set2 = new HashSet(set); - - Assert.That(YamlObjectSerializer.IsSerializedEqual(set, set2), Is.True); - } - - [Test] - public void SerializedNotEqualHashSetTest() - { - var set = new HashSet {"A"}; - var set2 = new HashSet {"B"}; - - Assert.That(YamlObjectSerializer.IsSerializedEqual(set, set2), Is.False); - } - - private readonly string SerializedSetYaml = "dataSet:\n- 1\n- 2\n- 3\n...\n"; - private readonly HashSet SerializableSet = new() { 1, 2, 3 }; - - [Test] - public void SerializePairTest() - { - var mapping = new YamlMappingNode(); - var pair = SerializablePair; - - var writer = YamlObjectSerializer.NewWriter(mapping); - - writer.DataField(ref pair, "dataPair", new KeyValuePair("val0", 0)); - - var result = NodeToYamlText(mapping); - Assert.That(result, Is.EqualTo(SerializedPairYaml)); - } - - [Test] - public void DeserializePairTest() - { - KeyValuePair data = default; - var rootNode = YamlTextToNode(SerializedPairYaml); - var serializer = YamlObjectSerializer.NewReader(rootNode); - - serializer.DataField(ref data, "dataPair", new KeyValuePair("val0", 0)); - - Assert.That(data, Is.Not.EqualTo(default(KeyValuePair))); - Assert.That(data.Key, Is.EqualTo(SerializablePair.Key)); - Assert.That(data.Value, Is.EqualTo(SerializablePair.Value)); - } - - [Test] - public void SerializeDefaultPairTest() - { - var mapping = new YamlMappingNode(); - var pair = SerializableDefaultPair; - - var writer = YamlObjectSerializer.NewWriter(mapping); - - writer.DataField(ref pair, "dataPair", new KeyValuePair(0, 0)); - - var result = NodeToYamlText(mapping); - Assert.That(result, Is.EqualTo(SerializedDefaultPairYaml)); - } - - [Test] - public void DeserializeDefaultPairTest() - { - KeyValuePair data = default; - var rootNode = YamlTextToNode(SerializedDefaultPairYaml); - var serializer = YamlObjectSerializer.NewReader(rootNode); - - serializer.DataField(ref data, "dataPair", new KeyValuePair(0, 0)); - - Assert.That(data, Is.EqualTo(default(KeyValuePair))); - Assert.That(data.Key, Is.EqualTo(SerializableDefaultPair.Key)); - Assert.That(data.Value, Is.EqualTo(SerializableDefaultPair.Value)); - } - - [Test] - public void DeserializeNoPairTest() - { - KeyValuePair data = default; - var rootNode = YamlTextToNode(SerializedNoPairYaml); - var serializer = YamlObjectSerializer.NewReader(rootNode); - - serializer.DataField(ref data, "dataPair", new KeyValuePair(0, 0)); - - Assert.That(data, Is.EqualTo(default(KeyValuePair))); - Assert.That(data.Key, Is.EqualTo(SerializedNoPair.Key)); - Assert.That(data.Value, Is.EqualTo(SerializedNoPair.Value)); - } - - [Test] - public void SerializedEqualPairTest() - { - var pair = new KeyValuePair("val0", 0); - var pair2 = new KeyValuePair("val0", 0); - - Assert.That(YamlObjectSerializer.IsSerializedEqual(pair, pair2), Is.True); - } - - [Test] - public void SerializedNotEqualPairTest() - { - var pair = new KeyValuePair("val0", 0); - var pair2 = new KeyValuePair("val0", 1); - - Assert.That(YamlObjectSerializer.IsSerializedEqual(pair, pair2), Is.False); - } - - private readonly string SerializedPairYaml = "dataPair:\n val1: 1\n...\n"; - private readonly KeyValuePair SerializablePair = new("val1", 1); - - private readonly string SerializedDefaultPairYaml = "{}\n...\n"; - private readonly KeyValuePair SerializableDefaultPair = new(0, 0); - - private readonly string SerializedNoPairYaml = "dataPair: {}\n...\n"; - private readonly KeyValuePair SerializedNoPair = new(0, 0); - - [Test] - public void NullablePrimitiveSerializeNullTest() - { - var mapping = new YamlMappingNode(); - var reader = YamlObjectSerializer.NewWriter(mapping); - - int? value = null; - - reader.DataField(ref value, "foo", null); - - Assert.That(mapping.Children.Count, Is.Zero); - } - - [Test] - public void NullablePrimitiveSerializeValueTest() - { - var mapping = new YamlMappingNode(); - var reader = YamlObjectSerializer.NewWriter(mapping); - - int? value = 5; - - reader.DataField(ref value, "foo", null); - - Assert.That(mapping["foo"].AsInt(), Is.EqualTo(5)); - } - - [Test] - public void NullablePrimitiveDeserializeNullTest() - { - var mapping = new YamlMappingNode - { - {"foo", null!} - }; - var reader = YamlObjectSerializer.NewReader(mapping); - - int? value = null; - - reader.DataField(ref value, "foo", null); - - Assert.That(value, Is.Null); - } - - [Test] - public void NullablePrimitiveDeserializeEmptyTest() - { - var mapping = new YamlMappingNode - { - {"foo", ""} - }; - var reader = YamlObjectSerializer.NewReader(mapping); - - int? value = null; - - reader.DataField(ref value, "foo", null); - - Assert.That(value, Is.Null); - } - - [Test] - public void NullablePrimitiveDeserializeNothingTest() - { - var mapping = new YamlMappingNode(); - var reader = YamlObjectSerializer.NewReader(mapping); - - int? value = null; - - reader.DataField(ref value, "foo", null); - - Assert.That(value, Is.Null); - } - - [Test] - public void NullablePrimitiveDeserializeValueTest() - { - var mapping = new YamlMappingNode - { - {"foo", "5"} - }; - var reader = YamlObjectSerializer.NewReader(mapping); - - int? value = null; - - reader.DataField(ref value, "foo", null); - - Assert.That(value, Is.EqualTo(5)); - } - - // serializes a node tree into text - internal static string NodeToYamlText(YamlNode root) - { - var document = new YamlDocument(root); - - using (var stream = new MemoryStream()) - { - using (var writer = new StreamWriter(stream)) - { - writer.NewLine = "\n"; - var yamlStream = new YamlStream(document); - yamlStream.Save(writer); - writer.Flush(); - return EncodingHelpers.UTF8.GetString(stream.ToArray()); - } - } - } - - // deserializes yaml text, loads the first document, and returns the first entity - internal static YamlMappingNode YamlTextToNode(string text) - { - using (var stream = new MemoryStream()) - { - // create a stream for testing - var writer = new StreamWriter(stream); - writer.Write(text); - writer.Flush(); - stream.Position = 0; - - // deserialize stream - var reader = new StreamReader(stream); - var yamlStream = new YamlStream(); - yamlStream.Load(reader); - - // read first document - var firstDoc = yamlStream.Documents[0]; - - return (YamlMappingNode)firstDoc.RootNode; - } - } - - private class DummyClass - { - public int Foo { get; set; } - public string Bar { get; set; } = default!; - public Color Baz { get; set; } = Color.Orange; - } - - private struct SelfSerializeTest : ISelfSerialize - { - public int Value; - - public void Deserialize(string value) - { - Value = int.Parse(value, CultureInfo.InvariantCulture); - } - - public string Serialize() - { - return Value.ToString(CultureInfo.InvariantCulture); - } - } - } -}