From 9afa4e4cab3d6b1a4484293075cd7a08052a043f Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 23 May 2021 21:06:23 -0700 Subject: [PATCH] feat: Source generated Serialization/Deserialization (#550) ### Features * Fully abstracts serialization by using compile-time attributes. * Supports serializing the following: - Primitives (integers, strings, etc) - IP Addresses - BigDecimal - DateTime, Delta DateTimes - TimeSpan - Server.Race - Server.Map - Point2D, Point3D, Rect2D, Rect3D - Existing/New `ISerializable` references - Lists/Sets of serializable types - Type with a `Serialize` method and constructor that takes an `IGenericReader` * Supports forward-only migration * Supports existing RunUO deserialization for older versions by changing to the following signature: - `public void OldDeserialize(IGenericReader reader, int version)` - Must remove deserializing the version since this is already done * Supports serializing from private fields or custom made properties. * Types do not require inheriting Item/Mobile. Code gen will fully create `ISerializable` information. - This is not recommended yet, since it requires wiring to `Persistence` which will cause lots of unresolved symbol errors until code gen is built. ### Example ```cs using System.Collections.Generic; namespace Server.Items { [Serializable(1)] public partial class TestItem1 : Item { [SerializableField(1)] [SerializableFieldAttr("[CommandProperty(AccessLevel.Administrator)]")] private List _someProperty; private void Deserialize(IGenericReader reader, int version) { } } } ``` Generates this: ```cs namespace Server.Items { public partial class TestItem1 { #pragma warning disable 0414 private const int _version = 1; #pragma warning restore 0414 [CommandProperty(AccessLevel.Administrator)] public System.Collections.Generic.List SomeProperty { get => _someProperty; set { if (value != _someProperty) { ((ISerializable)this).MarkDirty(); _someProperty = value; } } } public TestItem1(Serial serial) : base(serial) { } public override void Serialize(IGenericWriter writer) { var savePosition = ((Server.ISerializable)this).SavePosition; if (savePosition > -1) { writer.Seek(savePosition, System.IO.SeekOrigin.Begin); return; } writer.WriteEncodedInt(_version); writer.Write(_someProperty); } public override void Deserialize(IGenericReader reader) { var version = reader.ReadEncodedInt(); if (version < 1) { OldDeserialize(reader, version); ((Server.ISerializable)this).MarkDirty(); return; } SomeProperty = reader.ReadEntityList(); } } } ``` And this: ```json { "version": 1, "type": "TestItem1", "properties": [ { "name": "SomeProperty", "type": "System.Collections.Generic.List\u003CServer.Item\u003E", "rule": "ListMigrationRule", "ruleArguments": [ "Server.Item", "SerializableInterfaceMigrationRule" ] } ] } ``` --- .github/workflows/build-test.yml | 2 +- .gitignore | 1 + Directory.Build.props | 2 +- ModernUO.sln | 20 +- .../SerializationGenerator.Tests.csproj | 22 ++ .../Tests/GenerateClassTests.cs | 23 ++ .../EntitySerializationGenerator.cs | 81 ++++++ .../ExampleSerialization.json | 12 + .../SerializationGenerator/IsExternalInit.cs | 4 + .../SerializableEntityGeneration.Class.cs | 262 ++++++++++++++++++ ...zableEntityGeneration.DeserializeMethod.cs | 95 +++++++ ...ializableEntityGeneration.MetadataTypes.cs | 161 +++++++++++ .../SerializableEntityGeneration.Property.cs | 49 ++++ ...SerializableEntityGeneration.SerialCtor.cs | 50 ++++ ...lizableEntityGeneration.SerializeMethod.cs | 68 +++++ .../ISerializableMigrationRule.cs | 46 +++ .../Rules/ArrayMigrationRule.cs | 121 ++++++++ .../Rules/HashSetMigrationRule.cs | 134 +++++++++ .../Rules/KeyValuePairMigrationRule.cs | 179 ++++++++++++ .../Rules/ListMigrationRule.cs | 133 +++++++++ .../Rules/PrimitiveTypeMigrationRule.cs | 159 +++++++++++ .../Rules/PrimitiveUOTypeMigrationRule.cs | 75 +++++ .../SerializableInterfaceMigrationRule.cs | 71 +++++ ...rializationMethodSignatureMigrationRule.cs | 81 ++++++ .../SerializableMetadata.cs | 32 +++ .../SerializableMetadataComparer.cs | 27 ++ .../SerializableMigration.ContentStruct.cs | 53 ++++ .../SerializableMigration.cs | 78 ++++++ .../SerializableMigrationRulesEngine.cs | 78 ++++++ .../SerializableProperty.cs | 34 +++ .../SerializationGenerator.csproj | 25 ++ .../SerializerSyntaxReceiver.cs | 48 ++++ .../SourceGeneration/Helpers.cs | 44 +++ .../SourceGeneration.AccessModifier.cs | 43 +++ .../SourceGeneration.Arguments.cs | 125 +++++++++ .../SourceGeneration.Attribute.cs | 92 ++++++ .../SourceGeneration.Class.cs | 76 +++++ .../SourceGeneration.InstanceModifier.cs | 39 +++ .../SourceGeneration.Method.cs | 60 ++++ .../SourceGeneration.Namespace.cs | 50 ++++ .../SourceGeneration.Property.cs | 107 +++++++ .../Serialization/EnumConversionTests.cs | 22 ++ Projects/Server/Guild.cs | 50 ++-- Projects/Server/IEntity.cs | 13 +- Projects/Server/Items/Item.cs | 34 +-- .../Server/Json/Converters/TypeConverter.cs | 2 +- Projects/Server/Mobiles/Mobile.cs | 75 ++--- Projects/Server/Serialization/BufferWriter.cs | 1 - .../Serialization/DeltaDateTimeAttribute.cs | 27 ++ .../Serialization/GenericPersistence.cs | 1 + .../Server/Serialization/ISerializable.cs | 22 ++ .../Serialization/SerializableAttribute.cs | 27 ++ .../SerializableFieldAttribute.cs | 27 ++ .../SerializableFieldAttributeAttribute.cs | 40 +++ .../SerializablePropertyAttribute.cs | 30 ++ Projects/Server/Server.csproj | 31 +++ Projects/Server/World/World.cs | 1 + Projects/UOContent/Accounting/Account.cs | 43 ++- .../Engines/Factions/Core/Election.cs | 10 +- Projects/UOContent/UOContent.csproj | 23 ++ azure-pipelines.yml | 4 +- 61 files changed, 3203 insertions(+), 142 deletions(-) create mode 100644 Projects/SerializationGenerator.Tests/SerializationGenerator.Tests.csproj create mode 100644 Projects/SerializationGenerator.Tests/Tests/GenerateClassTests.cs create mode 100755 Projects/SerializationGenerator/EntitySerializationGenerator.cs create mode 100644 Projects/SerializationGenerator/ExampleSerialization.json create mode 100644 Projects/SerializationGenerator/IsExternalInit.cs create mode 100644 Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.Class.cs create mode 100644 Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.DeserializeMethod.cs create mode 100644 Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.MetadataTypes.cs create mode 100644 Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.Property.cs create mode 100644 Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.SerialCtor.cs create mode 100644 Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.SerializeMethod.cs create mode 100644 Projects/SerializationGenerator/SerializableMigration/ISerializableMigrationRule.cs create mode 100644 Projects/SerializationGenerator/SerializableMigration/Rules/ArrayMigrationRule.cs create mode 100644 Projects/SerializationGenerator/SerializableMigration/Rules/HashSetMigrationRule.cs create mode 100644 Projects/SerializationGenerator/SerializableMigration/Rules/KeyValuePairMigrationRule.cs create mode 100644 Projects/SerializationGenerator/SerializableMigration/Rules/ListMigrationRule.cs create mode 100644 Projects/SerializationGenerator/SerializableMigration/Rules/PrimitiveTypeMigrationRule.cs create mode 100644 Projects/SerializationGenerator/SerializableMigration/Rules/PrimitiveUOTypeMigrationRule.cs create mode 100644 Projects/SerializationGenerator/SerializableMigration/Rules/SerializableInterfaceMigrationRule.cs create mode 100644 Projects/SerializationGenerator/SerializableMigration/Rules/SerializationMethodSignatureMigrationRule.cs create mode 100644 Projects/SerializationGenerator/SerializableMigration/SerializableMetadata.cs create mode 100644 Projects/SerializationGenerator/SerializableMigration/SerializableMetadataComparer.cs create mode 100644 Projects/SerializationGenerator/SerializableMigration/SerializableMigration.ContentStruct.cs create mode 100644 Projects/SerializationGenerator/SerializableMigration/SerializableMigration.cs create mode 100644 Projects/SerializationGenerator/SerializableMigration/SerializableMigrationRulesEngine.cs create mode 100644 Projects/SerializationGenerator/SerializableMigration/SerializableProperty.cs create mode 100755 Projects/SerializationGenerator/SerializationGenerator.csproj create mode 100755 Projects/SerializationGenerator/SerializerSyntaxReceiver.cs create mode 100644 Projects/SerializationGenerator/SourceGeneration/Helpers.cs create mode 100644 Projects/SerializationGenerator/SourceGeneration/SourceGeneration.AccessModifier.cs create mode 100644 Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Arguments.cs create mode 100644 Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Attribute.cs create mode 100644 Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Class.cs create mode 100644 Projects/SerializationGenerator/SourceGeneration/SourceGeneration.InstanceModifier.cs create mode 100644 Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Method.cs create mode 100644 Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Namespace.cs create mode 100644 Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Property.cs create mode 100644 Projects/Server.Tests/Tests/Serialization/EnumConversionTests.cs create mode 100644 Projects/Server/Serialization/DeltaDateTimeAttribute.cs create mode 100755 Projects/Server/Serialization/SerializableAttribute.cs create mode 100755 Projects/Server/Serialization/SerializableFieldAttribute.cs create mode 100644 Projects/Server/Serialization/SerializableFieldAttributeAttribute.cs create mode 100644 Projects/Server/Serialization/SerializablePropertyAttribute.cs mode change 100644 => 100755 Projects/Server/Server.csproj diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 070760708..c4f097913 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -26,7 +26,7 @@ jobs: - name: Setup .NET 5 uses: actions/setup-dotnet@v1 with: - dotnet-version: 5.0.202 + dotnet-version: 5.0.203 - name: Build run: ./publish.cmd - name: Test diff --git a/.gitignore b/.gitignore index 1a35d8862..f2e5b33e2 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ /Projects/*/obj /Projects/*/bin +/Projects/*/Generated *.log *.user diff --git a/Directory.Build.props b/Directory.Build.props index 2dbfd4c03..1113fce2c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,7 +7,7 @@ net5.0 x64 x64 - 9 + preview true true NU1603 diff --git a/ModernUO.sln b/ModernUO.sln index bfd1dd1c9..adf6fb240 100644 --- a/ModernUO.sln +++ b/ModernUO.sln @@ -12,6 +12,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UOContent.Tests", "Projects EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "Projects\Benchmarks\Benchmarks.csproj", "{38B7E4A1-FDDD-486C-98EE-BA7B13712528}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SerializationGenerator", "Projects\SerializationGenerator\SerializationGenerator.csproj", "{07DDB8CF-F926-44F4-A584-FF2997D2C9D0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SerializationGenerator.Tests", "Projects\SerializationGenerator.Tests\SerializationGenerator.Tests.csproj", "{2BC92375-BB66-4CA5-B39C-36215749FE64}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Analyze|x64 = Analyze|x64 @@ -43,12 +47,24 @@ Global {3C4797F9-603E-44EF-8E8C-9275CC9EA74B}.Debug|x64.Build.0 = Debug|x64 {3C4797F9-603E-44EF-8E8C-9275CC9EA74B}.Release|x64.ActiveCfg = Release|x64 {3C4797F9-603E-44EF-8E8C-9275CC9EA74B}.Release|x64.Build.0 = Release|x64 - {38B7E4A1-FDDD-486C-98EE-BA7B13712528}.Analyze|x64.ActiveCfg = Release|x64 - {38B7E4A1-FDDD-486C-98EE-BA7B13712528}.Analyze|x64.Build.0 = Release|x64 + {38B7E4A1-FDDD-486C-98EE-BA7B13712528}.Analyze|x64.ActiveCfg = Release|x64 + {38B7E4A1-FDDD-486C-98EE-BA7B13712528}.Analyze|x64.Build.0 = Release|x64 {38B7E4A1-FDDD-486C-98EE-BA7B13712528}.Debug|x64.ActiveCfg = Debug|x64 {38B7E4A1-FDDD-486C-98EE-BA7B13712528}.Debug|x64.Build.0 = Debug|x64 {38B7E4A1-FDDD-486C-98EE-BA7B13712528}.Release|x64.ActiveCfg = Release|x64 {38B7E4A1-FDDD-486C-98EE-BA7B13712528}.Release|x64.Build.0 = Release|x64 + {07DDB8CF-F926-44F4-A584-FF2997D2C9D0}.Analyze|x64.ActiveCfg = Analyze|x64 + {07DDB8CF-F926-44F4-A584-FF2997D2C9D0}.Analyze|x64.Build.0 = Analyze|x64 + {07DDB8CF-F926-44F4-A584-FF2997D2C9D0}.Debug|x64.ActiveCfg = Debug|x64 + {07DDB8CF-F926-44F4-A584-FF2997D2C9D0}.Debug|x64.Build.0 = Debug|x64 + {07DDB8CF-F926-44F4-A584-FF2997D2C9D0}.Release|x64.ActiveCfg = Release|x64 + {07DDB8CF-F926-44F4-A584-FF2997D2C9D0}.Release|x64.Build.0 = Release|x64 + {2BC92375-BB66-4CA5-B39C-36215749FE64}.Analyze|x64.ActiveCfg = Analyze|x64 + {2BC92375-BB66-4CA5-B39C-36215749FE64}.Analyze|x64.Build.0 = Analyze|x64 + {2BC92375-BB66-4CA5-B39C-36215749FE64}.Debug|x64.ActiveCfg = Debug|x64 + {2BC92375-BB66-4CA5-B39C-36215749FE64}.Debug|x64.Build.0 = Debug|x64 + {2BC92375-BB66-4CA5-B39C-36215749FE64}.Release|x64.ActiveCfg = Release|x64 + {2BC92375-BB66-4CA5-B39C-36215749FE64}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Projects/SerializationGenerator.Tests/SerializationGenerator.Tests.csproj b/Projects/SerializationGenerator.Tests/SerializationGenerator.Tests.csproj new file mode 100644 index 000000000..597fc3414 --- /dev/null +++ b/Projects/SerializationGenerator.Tests/SerializationGenerator.Tests.csproj @@ -0,0 +1,22 @@ + + + net5.0 + true + true + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/Projects/SerializationGenerator.Tests/Tests/GenerateClassTests.cs b/Projects/SerializationGenerator.Tests/Tests/GenerateClassTests.cs new file mode 100644 index 000000000..03f8db598 --- /dev/null +++ b/Projects/SerializationGenerator.Tests/Tests/GenerateClassTests.cs @@ -0,0 +1,23 @@ +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; +using SerializationGenerator; +using Xunit; + +namespace SerializationGeneratorTests +{ + public class GenerateClassTests + { + [Fact] + public void Test1() + { + var source = new StringBuilder(); + source.GenerateClassStart( + "TestClass", + ImmutableArray.Empty + ); + + Assert.NotEmpty(source.ToString()); + } + } +} diff --git a/Projects/SerializationGenerator/EntitySerializationGenerator.cs b/Projects/SerializationGenerator/EntitySerializationGenerator.cs new file mode 100755 index 000000000..20b6dbee9 --- /dev/null +++ b/Projects/SerializationGenerator/EntitySerializationGenerator.cs @@ -0,0 +1,81 @@ +/************************************************************************* + * ModernUO * + * Copyright (C) 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: EntityJsonGenerator.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace SerializationGenerator +{ + [Generator] + public class EntitySerializationGenerator : ISourceGenerator + { + public void Initialize(GeneratorInitializationContext context) + { +#if DEBUG + if (!Debugger.IsAttached) + { + Debugger.Launch(); + } +#endif + + context.RegisterForPostInitialization(i => + { + SerializerSyntaxReceiver.AttributeTypes.Add("Server.SerializableAttribute"); + SerializerSyntaxReceiver.AttributeTypes.Add("Server.SerializableFieldAttribute"); + }); + + context.RegisterForSyntaxNotifications(() => new SerializerSyntaxReceiver()); + } + + public void Execute(GeneratorExecutionContext context) + { + if (context.SyntaxContextReceiver is not SerializerSyntaxReceiver receiver) + { + return; + } + + var migrationPath = SerializableMigration.GetMigrationPath(context); + var jsonOptions = SerializableMigration.GetJsonSerializerOptions(context.Compilation); + // List of types that _will_ become ISerializable + var serializableList = receiver + .Fields + .GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default) + .Select(g => g.Key as INamedTypeSymbol) + .Where(t => t.WillBeSerializable(context)) + .ToImmutableArray(); + + foreach (IGrouping group in receiver.Fields.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default)) + { + string classSource = SerializableEntityGeneration.GenerateSerializationPartialClass( + group.Key as INamedTypeSymbol, + group.ToList(), + context, + migrationPath, + jsonOptions, + serializableList + ); + + if (classSource != null) + { + context.AddSource($"{group.Key.Name}.Serialization.cs", SourceText.From(classSource, Encoding.UTF8)); + } + } + } + } +} diff --git a/Projects/SerializationGenerator/ExampleSerialization.json b/Projects/SerializationGenerator/ExampleSerialization.json new file mode 100644 index 000000000..1b45b49c5 --- /dev/null +++ b/Projects/SerializationGenerator/ExampleSerialization.json @@ -0,0 +1,12 @@ +{ + "type": "Server.Items.TestItem", + "version": 1, + "properties": [ + { + "name": "SomeProperty", + "type": "Server.Item", + "rule": "SerializableInterfaceMigrationRule", + "ruleArguments": ["Server.Items.Item"] + } + ] +} diff --git a/Projects/SerializationGenerator/IsExternalInit.cs b/Projects/SerializationGenerator/IsExternalInit.cs new file mode 100644 index 000000000..eb2da113f --- /dev/null +++ b/Projects/SerializationGenerator/IsExternalInit.cs @@ -0,0 +1,4 @@ +namespace System.Runtime.CompilerServices +{ + internal static class IsExternalInit {} +} diff --git a/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.Class.cs b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.Class.cs new file mode 100644 index 000000000..6754f5a67 --- /dev/null +++ b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.Class.cs @@ -0,0 +1,262 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SerializableEntityGeneration.Class.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Text.Json; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public static partial class SerializableEntityGeneration + { + public static bool WillBeSerializable(this INamedTypeSymbol classSymbol, GeneratorExecutionContext context) + { + var compilation = context.Compilation; + + var serializableEntityAttribute = + compilation.GetTypeByMetadataName(SERIALIZABLE_ATTRIBUTE); + var serializableInterface = compilation.GetTypeByMetadataName(SERIALIZABLE_INTERFACE); + + if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default)) + { + return false; + } + + if (!classSymbol.ContainsInterface(serializableInterface)) + { + return false; + } + + var versionValue = classSymbol.GetAttributes() + .FirstOrDefault( + attr => attr.AttributeClass?.Equals(serializableEntityAttribute, SymbolEqualityComparer.Default) ?? false + )?.ConstructorArguments.FirstOrDefault().Value; + + return versionValue != null; + } + + public static string GenerateSerializationPartialClass( + INamedTypeSymbol classSymbol, + IList fields, + GeneratorExecutionContext context, + string migrationPath, + JsonSerializerOptions jsonSerializerOptions, + ImmutableArray serializableTypes + ) + { + var compilation = context.Compilation; + + var serializableEntityAttribute = + compilation.GetTypeByMetadataName(SERIALIZABLE_ATTRIBUTE); + var serializableFieldAttribute = + compilation.GetTypeByMetadataName(SERIALIZABLE_FIELD_ATTRIBUTE); + var serializableFieldAttrAttribute = + compilation.GetTypeByMetadataName(SERIALIZABLE_FIELD_ATTR_ATTRIBUTE); + var serializableInterface = compilation.GetTypeByMetadataName(SERIALIZABLE_INTERFACE); + + // This is a class symbol if the containing symbol is the namespace + if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default)) + { + return null; + } + + // If we have a parent that is or derives from ISerializable, then we are in override + var isOverride = classSymbol.BaseType.ContainsInterface(serializableInterface); + + if (!isOverride && !classSymbol.ContainsInterface(serializableInterface)) + { + return null; + } + + var version = classSymbol.GetAttributes() + .FirstOrDefault( + attr => attr.AttributeClass?.Equals(serializableEntityAttribute, SymbolEqualityComparer.Default) ?? false + )?.ConstructorArguments.FirstOrDefault().Value?.ToString(); + + if (version == null) + { + return null; // We don't have the attribute + } + + var namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); + var className = classSymbol.Name; + + StringBuilder source = new StringBuilder(); + + source.GenerateNamespaceStart(namespaceName); + + source.GenerateClassStart( + className, + isOverride ? + ImmutableArray.Empty : + ImmutableArray.Create(serializableInterface) + ); + + source.GenerateClassField( + AccessModifier.Private, + InstanceModifier.Const, + "int", + "_version", + version, + true + ); + source.AppendLine(); + + var serializableProperties = new List(); + + foreach (IFieldSymbol fieldSymbol in fields) + { + var allAttributes = fieldSymbol.GetAttributes(); + + var hasAttribute = allAttributes + .Any( + attr => + SymbolEqualityComparer.Default.Equals(attr.AttributeClass, serializableFieldAttribute) + ); + + if (hasAttribute) + { + foreach (var attr in allAttributes) + { + if (!SymbolEqualityComparer.Default.Equals(attr.AttributeClass, serializableFieldAttrAttribute)) + { + continue; + } + + if (attr.AttributeClass == null) + { + continue; + } + + var ctorArgs = attr.ConstructorArguments; + var attrTypeArg = ctorArgs[0]; + + if (attrTypeArg.Kind == TypedConstantKind.Primitive && attrTypeArg.Value is string attrStr) + { + source.AppendLine($" {attrStr}"); + } + else + { + var attrType = (ITypeSymbol)attrTypeArg.Value; + source.GenerateAttribute(attrType.Name, ctorArgs[1].Values); + } + } + + source.GenerateSerializableProperty(fieldSymbol); + source.AppendLine(); + + var serializableProperty = SerializableMigrationRulesEngine.GenerateSerializableProperty( + compilation, + fieldSymbol.GetPropertyName(), + fieldSymbol.Type, + allAttributes, + serializableTypes + ); + + serializableProperties.Add(serializableProperty); + } + } + + // If we are not inheriting ISerializable, then we need to define some stuff + if (!isOverride) + { + // long ISerializable.SavePosition { get; set; } + source.GenerateAutoProperty( + AccessModifier.None, + "long", + "ISerializable.SavePosition", + AccessModifier.None, + AccessModifier.None + ); + source.AppendLine(); + + // BufferWriter ISerializable.SaveBuffer { get; set; } + source.GenerateAutoProperty( + AccessModifier.None, + "BufferWriter", + "ISerializable.SaveBuffer", + AccessModifier.None, + AccessModifier.None + ); + source.AppendLine(); + } + + // Serial constructor + source.GenerateSerialCtor(context, className, isOverride); + source.AppendLine(); + + var versionValue = int.Parse(version); + List migrations; + + if (versionValue > 0) + { + migrations = SerializableMigration.GetMigrations( + migrationPath, + classSymbol, + versionValue, + jsonSerializerOptions + ); + + for (var i = 0; i < migrations.Count; i++) + { + var migration = migrations[i]; + if (migration.Version < versionValue) + { + source.GenerateMigrationContentStruct(migration); + source.AppendLine(); + } + } + } + else + { + migrations = new List(); + } + + // Serialize Method + source.GenerateSerializeMethod( + compilation, + isOverride, + serializableProperties + ); + source.AppendLine(); + + // Deserialize Method + source.GenerateDeserializeMethod( + compilation, + isOverride, + versionValue, + migrations, + serializableProperties + ); + + source.GenerateClassEnd(); + source.GenerateNamespaceEnd(); + + // Write the migration file + var newMigration = new SerializableMetadata + { + Version = versionValue, + Type = classSymbol.ToDisplayString(), + Properties = serializableProperties + }; + SerializableMigration.WriteMigration(migrationPath, newMigration, jsonSerializerOptions); + + return source.ToString(); + } + } +} diff --git a/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.DeserializeMethod.cs b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.DeserializeMethod.cs new file mode 100644 index 000000000..8ebdc27bf --- /dev/null +++ b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.DeserializeMethod.cs @@ -0,0 +1,95 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SerializableEntityGeneration.DeserializeMethod.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public static partial class SerializableEntityGeneration + { + public static void GenerateDeserializeMethod( + this StringBuilder source, + Compilation compilation, + bool isOverride, + int version, + List migrations, + List properties + ) + { + var genericReaderInterface = compilation.GetTypeByMetadataName(GENERIC_READER_INTERFACE); + + source.GenerateMethodStart( + "Deserialize", + AccessModifier.Public, + isOverride, + "void", + ImmutableArray.Create<(ITypeSymbol, string)>((genericReaderInterface, "reader")) + ); + + const string indent = " "; + + // Version + source.AppendLine($"{indent}var version = reader.ReadEncodedInt();"); + + if (version > 0) + { + var nextVersion = 0; + + for (var i = 0; i < migrations.Count; i++) + { + var migrationVersion = migrations[i].Version; + if (migrationVersion == nextVersion) + { + nextVersion++; + } + + source.AppendLine(); + source.AppendLine($"{indent}if (version == {migrationVersion})"); + source.AppendLine($"{indent}{{"); + source.AppendLine($"{indent} MigrateFrom(new V{migrationVersion}Content(reader));"); + source.AppendLine($"{indent} ((Server.ISerializable)this).MarkDirty();"); + source.AppendLine($"{indent} return;"); + source.AppendLine($"{indent}}}"); + } + + if (nextVersion < version) + { + source.AppendLine(); + source.AppendLine($"{indent}if (version < _version)"); + source.AppendLine($"{indent}{{"); + source.AppendLine($"{indent} Deserialize(reader, version);"); + source.AppendLine($"{indent} ((Server.ISerializable)this).MarkDirty();"); + source.AppendLine($"{indent} return;"); + source.AppendLine($"{indent}}}"); + } + } + + foreach (var property in properties) + { + source.AppendLine(); + SerializableMigrationRulesEngine.Rules[property.Rule].GenerateDeserializationMethod( + source, + indent, + property + ); + } + + source.GenerateMethodEnd(); + } + } +} diff --git a/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.MetadataTypes.cs b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.MetadataTypes.cs new file mode 100644 index 000000000..840cd14de --- /dev/null +++ b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.MetadataTypes.cs @@ -0,0 +1,161 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SerializableEntityGeneration.MetadataTypes.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public static partial class SerializableEntityGeneration + { + public const string LIST_CLASS = "System.Collections.Generic.List`1"; + public const string HASHSET_CLASS = "System.Collections.Generic.HashSet`1"; + public const string IP_CLASS = "System.Net.IPAddress"; + public const string KEYVALUEPAIR_STRUCT = "System.Collections.Generic.KeyValuePair"; + + public const string SERIALIZABLE_ATTRIBUTE = "Server.SerializableAttribute"; + public const string SERIALIZABLE_FIELD_ATTRIBUTE = "Server.SerializableFieldAttribute"; + public const string SERIALIZABLE_FIELD_ATTR_ATTRIBUTE = "Server.SerializableFieldAttrAttribute"; + public const string SERIALIZABLE_INTERFACE = "Server.ISerializable"; + public const string GENERIC_WRITER_INTERFACE = "Server.IGenericWriter"; + public const string GENERIC_READER_INTERFACE = "Server.IGenericReader"; + public const string DELTA_DATE_TIME_ATTRIBUTE = "Server.DeltaDateTimeAttribute"; + public const string POINT2D_STRUCT = "Server.Point2D"; + public const string POINT3D_STRUCT = "Server.Point3D"; + public const string RECTANGLE2D_STRUCT = "Server.Rectangle2D"; + public const string RECTANGLE3D_STRUCT = "Server.Rectangle3D"; + public const string RACE_CLASS = "Server.Race"; + public const string MAP_CLASS = "Server.Map"; + + public static bool IsDeltaDateTime(this AttributeData attr, Compilation compilation) => + attr?.IsAttribute(compilation.GetTypeByMetadataName(DELTA_DATE_TIME_ATTRIBUTE)) == true; + + public static bool IsAttribute(this AttributeData attr, ISymbol symbol) => + attr?.AttributeClass?.Equals(symbol, SymbolEqualityComparer.Default) == true; + + public static bool IsEnum(this ITypeSymbol symbol) => + symbol.SpecialType == SpecialType.System_Enum || symbol.TypeKind == TypeKind.Enum; + + public static bool HasSerializableInterface( + this ITypeSymbol symbol, + Compilation compilation, + ImmutableArray serializableTypes + ) => + symbol.ContainsInterface(compilation.GetTypeByMetadataName(SERIALIZABLE_INTERFACE)) || + serializableTypes.Contains(symbol); + + public static bool Contains(this ImmutableArray symbols, ITypeSymbol symbol) => + symbol is INamedTypeSymbol namedSymbol && + symbols.Contains(namedSymbol, SymbolEqualityComparer.Default); + + public static bool HasGenericReaderCtor(this INamedTypeSymbol symbol, Compilation compilation, out bool requiresParent) + { + var genericReaderInterface = compilation.GetTypeByMetadataName(GENERIC_READER_INTERFACE); + var genericCtor = symbol.Constructors.FirstOrDefault( + m => !m.IsStatic && + m.MethodKind == MethodKind.Constructor && + m.Parameters.Length <= 2 && + m.Parameters[0].Equals(genericReaderInterface, SymbolEqualityComparer.Default) + ); + + requiresParent = genericCtor?.Parameters.Length == 2 && genericCtor.Parameters[1].Equals(symbol, SymbolEqualityComparer.Default); + return genericCtor != null; + } + + public static bool HasPublicSerializeMethod( + this ITypeSymbol symbol, + Compilation compilation, + ImmutableArray serializableTypes + ) + { + if (symbol.HasSerializableInterface(compilation, serializableTypes)) + { + return true; + } + + var genericWriterInterface = compilation.GetTypeByMetadataName(GENERIC_WRITER_INTERFACE); + + return symbol.GetAllMethods("Serialize") + .Any( + m => !m.IsStatic && + m.ReturnsVoid && + m.Parameters.Length == 1 && + m.Parameters[0].Equals(genericWriterInterface, SymbolEqualityComparer.Default) && + m.DeclaredAccessibility == Accessibility.Public + ); + } + + public static bool IsPoint2D(this ISymbol symbol, Compilation compilation) => + symbol.Equals( + compilation.GetTypeByMetadataName(POINT2D_STRUCT), + SymbolEqualityComparer.Default + ); + + public static bool IsPoint3D(this ISymbol symbol, Compilation compilation) => + symbol.Equals( + compilation.GetTypeByMetadataName(POINT3D_STRUCT), + SymbolEqualityComparer.Default + ); + + public static bool IsRectangle2D(this ISymbol symbol, Compilation compilation) => + symbol.Equals( + compilation.GetTypeByMetadataName(RECTANGLE2D_STRUCT), + SymbolEqualityComparer.Default + ); + + public static bool IsRectangle3D(this ISymbol symbol, Compilation compilation) => + symbol.Equals( + compilation.GetTypeByMetadataName(RECTANGLE3D_STRUCT), + SymbolEqualityComparer.Default + ); + + public static bool IsIpAddress(this ISymbol symbol, Compilation compilation) => + symbol.Equals( + compilation.GetTypeByMetadataName(IP_CLASS), + SymbolEqualityComparer.Default + ); + + public static bool IsRace(this ISymbol symbol, Compilation compilation) => + symbol.Equals( + compilation.GetTypeByMetadataName(RACE_CLASS), + SymbolEqualityComparer.Default + ); + + public static bool IsMap(this ISymbol symbol, Compilation compilation) => + symbol.Equals( + compilation.GetTypeByMetadataName(MAP_CLASS), + SymbolEqualityComparer.Default + ); + + public static bool IsKeyValuePair(this ISymbol symbol, Compilation compilation) => + (symbol as INamedTypeSymbol)?.ConstructedFrom.Equals( + compilation.GetTypeByMetadataName(KEYVALUEPAIR_STRUCT), + SymbolEqualityComparer.Default + ) == true; + + public static bool IsList(this ISymbol symbol, Compilation compilation) => + (symbol as INamedTypeSymbol)?.ConstructedFrom.Equals( + compilation.GetTypeByMetadataName(LIST_CLASS), + SymbolEqualityComparer.Default + ) == true; + + public static bool IsHashSet(this ISymbol symbol, Compilation compilation) => + (symbol as INamedTypeSymbol)?.ConstructedFrom.Equals( + compilation.GetTypeByMetadataName(HASHSET_CLASS), + SymbolEqualityComparer.Default + ) == true; + } +} diff --git a/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.Property.cs b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.Property.cs new file mode 100644 index 000000000..7699c4158 --- /dev/null +++ b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.Property.cs @@ -0,0 +1,49 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SerializableEntityGeneration.Property.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.Text; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public static partial class SerializableEntityGeneration + { + public static void GenerateSerializableProperty( + this StringBuilder source, + IFieldSymbol fieldSymbol + ) + { + var fieldName = fieldSymbol.Name; + + source.GeneratePropertyStart(AccessModifier.Public, fieldSymbol); + + // Getter + source.GeneratePropertyGetterReturnsField(fieldSymbol); + + // Setter + source.GeneratePropertySetterStart(false); + source.AppendLine( + $@" if (value != {fieldName}) + {{ + ((ISerializable)this).MarkDirty(); + {fieldName} = value; + }}" + ); + source.GeneratePropertyGetSetEnd(false); + + source.GeneratePropertyEnd(); + } + } +} diff --git a/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.SerialCtor.cs b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.SerialCtor.cs new file mode 100644 index 000000000..7823d3e9a --- /dev/null +++ b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.SerialCtor.cs @@ -0,0 +1,50 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SerializableEntityGeneration.SerialCtor.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public static partial class SerializableEntityGeneration + { + private static readonly ImmutableArray _baseParameters = new[] { "serial" }.ToImmutableArray(); + public static void GenerateSerialCtor( + this StringBuilder source, + GeneratorExecutionContext context, + string className, + bool isOverride + ) + { + var serialType = (ITypeSymbol)context.Compilation.GetTypeByMetadataName("Server.Serial"); + + source.GenerateConstructorStart( + className, + AccessModifier.Public, + new []{ (serialType, "serial") }.ToImmutableArray(), + isOverride ? _baseParameters : ImmutableArray.Empty + ); + + if (!isOverride) + { + source.Append(@$" Serial = serial; + SetTypeRef(typeof({className}));"); + } + + source.GenerateMethodEnd(); + } + } +} diff --git a/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.SerializeMethod.cs b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.SerializeMethod.cs new file mode 100644 index 000000000..d752eec3c --- /dev/null +++ b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.SerializeMethod.cs @@ -0,0 +1,68 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SerializableEntityGeneration.SerializeMethod.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public static partial class SerializableEntityGeneration + { + public static void GenerateSerializeMethod( + this StringBuilder source, + Compilation compilation, + bool isOverride, + List properties + ) + { + var genericWriterInterface = compilation.GetTypeByMetadataName(GENERIC_WRITER_INTERFACE); + + source.GenerateMethodStart( + "Serialize", + AccessModifier.Public, + isOverride, + "void", + ImmutableArray.Create<(ITypeSymbol, string)>((genericWriterInterface, "writer")) + ); + + const string indent = " "; + + source.AppendLine($"{indent}var savePosition = ((Server.ISerializable)this).SavePosition;"); + source.AppendLine(@$"{indent}if (savePosition > -1) +{indent}{{ +{indent} writer.Seek(savePosition, System.IO.SeekOrigin.Begin); +{indent} return; +{indent}}}"); + + // Version + source.AppendLine(); + source.AppendLine($"{indent}writer.WriteEncodedInt(_version);"); + + foreach (var property in properties) + { + source.AppendLine(); + SerializableMigrationRulesEngine.Rules[property.Rule].GenerateSerializationMethod( + source, + indent, + property + ); + } + + source.GenerateMethodEnd(); + } + } +} diff --git a/Projects/SerializationGenerator/SerializableMigration/ISerializableMigrationRule.cs b/Projects/SerializationGenerator/SerializableMigration/ISerializableMigrationRule.cs new file mode 100644 index 000000000..4c5582331 --- /dev/null +++ b/Projects/SerializationGenerator/SerializableMigration/ISerializableMigrationRule.cs @@ -0,0 +1,46 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SerializableMigrationRule.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public interface ISerializableMigrationRule + { + string RuleName { get; } + + bool GenerateRuleState( + Compilation compilation, + ISymbol symbol, + ImmutableArray attributes, + ImmutableArray serializableTypes, + out string[] ruleArguments + ); + + void GenerateDeserializationMethod( + StringBuilder source, + string indent, + SerializableProperty property + ); + + void GenerateSerializationMethod( + StringBuilder source, + string indent, + SerializableProperty property + ); + } +} diff --git a/Projects/SerializationGenerator/SerializableMigration/Rules/ArrayMigrationRule.cs b/Projects/SerializationGenerator/SerializableMigration/Rules/ArrayMigrationRule.cs new file mode 100644 index 000000000..1fffeb5cb --- /dev/null +++ b/Projects/SerializationGenerator/SerializableMigration/Rules/ArrayMigrationRule.cs @@ -0,0 +1,121 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: ArrayMigrationRule.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public class ArrayMigrationRule : ISerializableMigrationRule + { + public string RuleName => nameof(ArrayMigrationRule); + + public bool GenerateRuleState( + Compilation compilation, + ISymbol symbol, + ImmutableArray attributes, + ImmutableArray serializableTypes, + out string[] ruleArguments + ) + { + if (symbol is not IArrayTypeSymbol arrayTypeSymbol) + { + ruleArguments = null; + return false; + } + + var serializableArrayType = SerializableMigrationRulesEngine.GenerateSerializableProperty( + compilation, + "ArrayEntry", + arrayTypeSymbol.ElementType, + attributes, + serializableTypes + ); + + var length = serializableArrayType.RuleArguments.Length; + ruleArguments = new string[length + 2]; + ruleArguments[0] = arrayTypeSymbol.ElementType.ToDisplayString(); + ruleArguments[1] = serializableArrayType.Rule; + Array.Copy(serializableArrayType.RuleArguments, 0, ruleArguments, 2, length); + + return true; + } + + public void GenerateDeserializationMethod(StringBuilder source, string indent, SerializableProperty property) + { + const string expectedRule = nameof(ArrayMigrationRule); + var ruleName = property.Rule; + if (expectedRule != ruleName) + { + throw new ArgumentException($"Invalid rule applied to property {ruleName}. Expecting {expectedRule}, but received {ruleName}."); + } + var ruleArguments = property.RuleArguments; + var arrayElementRule = SerializableMigrationRulesEngine.Rules[ruleArguments[1]]; + var arrayElementRuleArguments = new string[ruleArguments.Length - 2]; + Array.Copy(ruleArguments, 2, arrayElementRuleArguments, 0, ruleArguments.Length - 2); + + var propertyIndex = $"{property.Name}Index"; + source.AppendLine($"{indent}{property.Name} = new {ruleArguments[0]}[reader.ReadEncodedInt()];"); + source.AppendLine($"{indent}for (var {propertyIndex} = 0; {propertyIndex} < {property.Name}.Length; {propertyIndex}++)"); + source.AppendLine($"{indent}{{"); + + var serializableArrayElement = new SerializableProperty + { + Name = $"{property.Name}[{propertyIndex}]", + Type = ruleArguments[0], + Rule = arrayElementRule.RuleName, + RuleArguments = arrayElementRuleArguments + }; + + arrayElementRule.GenerateDeserializationMethod(source, $"{indent} ", serializableArrayElement); + + source.AppendLine($"{indent}}}"); + } + + public void GenerateSerializationMethod(StringBuilder source, string indent, SerializableProperty property) + { + const string expectedRule = nameof(ArrayMigrationRule); + var ruleName = property.Rule; + if (expectedRule != ruleName) + { + throw new ArgumentException($"Invalid rule applied to property {ruleName}. Expecting {expectedRule}, but received {ruleName}."); + } + + var ruleArguments = property.RuleArguments; + var arrayElementRule = SerializableMigrationRulesEngine.Rules[ruleArguments[1]]; + var arrayElementRuleArguments = new string[ruleArguments.Length - 2]; + Array.Copy(ruleArguments, 2, arrayElementRuleArguments, 0, ruleArguments.Length - 2); + + var propertyIndex = $"{property.Name}Index"; + source.AppendLine($"{indent}writer.WriteEncodedInt({property.Name}.Length);"); + source.AppendLine($"{indent}for (var {propertyIndex} = 0; {propertyIndex} < {property.Name}.Length; {propertyIndex}++)"); + source.AppendLine($"{indent}{{"); + + var serializableArrayElement = new SerializableProperty + { + Name = $"{property.Name}[{propertyIndex}]", + Type = ruleArguments[0], + Rule = arrayElementRule.RuleName, + RuleArguments = arrayElementRuleArguments + }; + + arrayElementRule.GenerateSerializationMethod(source, $"{indent} ", serializableArrayElement); + + source.AppendLine($"{indent}}}"); + } + } +} diff --git a/Projects/SerializationGenerator/SerializableMigration/Rules/HashSetMigrationRule.cs b/Projects/SerializationGenerator/SerializableMigration/Rules/HashSetMigrationRule.cs new file mode 100644 index 000000000..c8416db7e --- /dev/null +++ b/Projects/SerializationGenerator/SerializableMigration/Rules/HashSetMigrationRule.cs @@ -0,0 +1,134 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: HashSetMigrationRule.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public class HashSetMigrationRule : ISerializableMigrationRule + { + public string RuleName => nameof(HashSetMigrationRule); + + public bool GenerateRuleState( + Compilation compilation, + ISymbol symbol, + ImmutableArray attributes, + ImmutableArray serializableTypes, + out string[] ruleArguments + ) + { + if (symbol is not INamedTypeSymbol namedTypeSymbol || !symbol.IsHashSet(compilation)) + { + ruleArguments = null; + return false; + } + + var setTypeSymbol = namedTypeSymbol.TypeArguments[0]; + + var serializableSetType = SerializableMigrationRulesEngine.GenerateSerializableProperty( + compilation, + "SetEntry", + setTypeSymbol, + attributes, + serializableTypes + ); + + var length = serializableSetType.RuleArguments.Length; + ruleArguments = new string[length + 2]; + ruleArguments[0] = setTypeSymbol.ToDisplayString(); + ruleArguments[1] = serializableSetType.Rule; + Array.Copy(serializableSetType.RuleArguments, 0, ruleArguments, 2, length); + + return true; + } + + public void GenerateDeserializationMethod(StringBuilder source, string indent, SerializableProperty property) + { + const string expectedRule = nameof(HashSetMigrationRule); + var ruleName = property.Rule; + if (expectedRule != ruleName) + { + throw new ArgumentException($"Invalid rule applied to property {ruleName}. Expecting {expectedRule}, but received {ruleName}."); + } + + var ruleArguments = property.RuleArguments; + var setElementRule = SerializableMigrationRulesEngine.Rules[ruleArguments[1]]; + var setElementRuleArguments = new string[ruleArguments.Length - 2]; + Array.Copy(ruleArguments, 2, setElementRuleArguments, 0, ruleArguments.Length - 2); + + var propertyName = property.Name; + var propertyVarPrefix = $"{char.ToLower(propertyName[0])}{propertyName.Substring(1, propertyName.Length - 1)}"; + var propertyIndex = $"{propertyVarPrefix}Index"; + var propertyEntry = $"{propertyVarPrefix}Entry"; + var propertyCount = $"{propertyVarPrefix}Count"; + + source.AppendLine($"{indent}{ruleArguments[0]} {propertyEntry};"); + source.AppendLine($"{indent}var {propertyCount} = reader.ReadEncodedInt();"); + source.AppendLine($"{indent}{property.Name} = new System.Collections.Generic.HashSet<{ruleArguments[0]}>({propertyCount});"); + source.AppendLine($"{indent}for (var {propertyIndex} = 0; i < {propertyCount}; {propertyIndex}++)"); + source.AppendLine($"{indent}{{"); + + var serializableSetElement = new SerializableProperty + { + Name = propertyEntry, + Type = ruleArguments[0], + Rule = setElementRule.RuleName, + RuleArguments = setElementRuleArguments + }; + + setElementRule.GenerateDeserializationMethod(source, $"{indent} ", serializableSetElement); + source.AppendLine($"{indent} {property.Name}.Add({propertyEntry});"); + + source.AppendLine($"{indent}}}"); + } + + public void GenerateSerializationMethod(StringBuilder source, string indent, SerializableProperty property) + { + const string expectedRule = nameof(HashSetMigrationRule); + var ruleName = property.Rule; + if (expectedRule != ruleName) + { + throw new ArgumentException($"Invalid rule applied to property {ruleName}. Expecting {expectedRule}, but received {ruleName}."); + } + + var ruleArguments = property.RuleArguments; + var setElementRule = SerializableMigrationRulesEngine.Rules[ruleArguments[1]]; + var setElementRuleArguments = new string[ruleArguments.Length - 2]; + Array.Copy(ruleArguments, 2, setElementRuleArguments, 0, ruleArguments.Length - 2); + + var propertyName = property.Name; + var propertyVarPrefix = $"{char.ToLower(propertyName[0])}{propertyName.Substring(1, propertyName.Length - 1)}"; + var propertyEntry = $"{propertyVarPrefix}Entry"; + source.AppendLine($"{indent}writer.WriteEncodedInt({property.Name}.Count);"); + source.AppendLine($"{indent}foreach (var {propertyEntry} in {property.Name});"); + source.AppendLine($"{indent}{{"); + + var serializableSetElement = new SerializableProperty + { + Name = propertyEntry, + Type = ruleArguments[0], + Rule = setElementRule.RuleName, + RuleArguments = setElementRuleArguments + }; + + setElementRule.GenerateSerializationMethod(source, $"{indent} ", serializableSetElement); + + source.AppendLine($"{indent}}}"); + } + } +} diff --git a/Projects/SerializationGenerator/SerializableMigration/Rules/KeyValuePairMigrationRule.cs b/Projects/SerializationGenerator/SerializableMigration/Rules/KeyValuePairMigrationRule.cs new file mode 100644 index 000000000..d78a69d92 --- /dev/null +++ b/Projects/SerializationGenerator/SerializableMigration/Rules/KeyValuePairMigrationRule.cs @@ -0,0 +1,179 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: KeyValuePairMigrationRule.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public class KeyValuePairMigrationRule : ISerializableMigrationRule + { + public string RuleName => nameof(KeyValuePairMigrationRule); + + public bool GenerateRuleState( + Compilation compilation, + ISymbol symbol, + ImmutableArray attributes, + ImmutableArray serializableTypes, + out string[] ruleArguments + ) + { + if (symbol is not INamedTypeSymbol namedTypeSymbol || !symbol.IsKeyValuePair(compilation)) + { + ruleArguments = null; + return false; + } + + var typeArguments = namedTypeSymbol.TypeArguments; + + var keySerializedProperty = SerializableMigrationRulesEngine.GenerateSerializableProperty( + compilation, + "key", + typeArguments[0], + attributes, + serializableTypes + ); + + var valueSerializedProperty = SerializableMigrationRulesEngine.GenerateSerializableProperty( + compilation, + "value", + typeArguments[1], + attributes, + serializableTypes + ); + + // Key + ruleArguments = new string[5 + keySerializedProperty.RuleArguments.Length + valueSerializedProperty.RuleArguments.Length]; + ruleArguments[0] = typeArguments[0].ToDisplayString(); + ruleArguments[1] = keySerializedProperty.Rule; + ruleArguments[2] = keySerializedProperty.RuleArguments.Length.ToString(); + Array.Copy(keySerializedProperty.RuleArguments, 0, ruleArguments, 2, keySerializedProperty.RuleArguments.Length); + + // Value + var valueIndex = 3 + keySerializedProperty.RuleArguments.Length; + ruleArguments[valueIndex++] = typeArguments[1].ToDisplayString(); + ruleArguments[valueIndex++] = valueSerializedProperty.Rule; + Array.Copy(valueSerializedProperty.RuleArguments, 0, ruleArguments, valueIndex, valueSerializedProperty.RuleArguments.Length); + + return true; + } + + public void GenerateDeserializationMethod(StringBuilder source, string indent, SerializableProperty property) + { + const string expectedRule = nameof(KeyValuePairMigrationRule); + var ruleName = property.Rule; + if (expectedRule != ruleName) + { + throw new ArgumentException($"Invalid rule applied to property {ruleName}. Expecting {expectedRule}, but received {ruleName}."); + } + + var ruleArguments = property.RuleArguments; + var keyType = ruleArguments[0]; + var keyRule = SerializableMigrationRulesEngine.Rules[ruleArguments[1]]; + var keyRuleArguments = new string[int.Parse(ruleArguments[2])]; + Array.Copy(ruleArguments, 3, keyRuleArguments, 0, keyRuleArguments.Length); + + var serializableKeyProperty = new SerializableProperty + { + Name = "key", + Type = keyType, + Rule = keyRule.RuleName, + RuleArguments = keyRuleArguments + }; + + keyRule.GenerateDeserializationMethod( + source, + indent, + serializableKeyProperty + ); + + var valueIndex = 3 + keyRuleArguments.Length; + var valueType = ruleArguments[valueIndex++]; + var valueRule = SerializableMigrationRulesEngine.Rules[ruleArguments[valueIndex++]]; + var valueRuleArguments = new string[ruleArguments.Length - valueIndex]; + Array.Copy(ruleArguments, valueIndex, valueRuleArguments, 0, valueRuleArguments.Length); + + var serializableValueProperty = new SerializableProperty + { + Name = "value", + Type = valueType, + Rule = valueRule.RuleName, + RuleArguments = valueRuleArguments + }; + + keyRule.GenerateDeserializationMethod( + source, + indent, + serializableValueProperty + ); + + source.AppendLine( + $"{indent}{property.Name} = new {SerializableEntityGeneration.KEYVALUEPAIR_STRUCT}<{keyType}, {valueType}>(key, value);" + ); + } + + public void GenerateSerializationMethod(StringBuilder source, string indent, SerializableProperty property) + { + const string expectedRule = nameof(KeyValuePairMigrationRule); + var ruleName = property.Rule; + if (expectedRule != ruleName) + { + throw new ArgumentException($"Invalid rule applied to property {ruleName}. Expecting {expectedRule}, but received {ruleName}."); + } + + var ruleArguments = property.RuleArguments; + var keyType = ruleArguments[0]; + var keyRule = SerializableMigrationRulesEngine.Rules[ruleArguments[1]]; + var keyRuleArguments = new string[int.Parse(ruleArguments[2])]; + Array.Copy(ruleArguments, 3, keyRuleArguments, 0, keyRuleArguments.Length); + + var serializableKeyProperty = new SerializableProperty + { + Name = $"{property.Name}.Key", + Type = keyType, + Rule = keyRule.RuleName, + RuleArguments = keyRuleArguments + }; + + keyRule.GenerateSerializationMethod( + source, + indent, + serializableKeyProperty + ); + + var valueIndex = 3 + keyRuleArguments.Length; + var valueType = ruleArguments[valueIndex++]; + var valueRule = SerializableMigrationRulesEngine.Rules[ruleArguments[valueIndex++]]; + var valueRuleArguments = new string[ruleArguments.Length - valueIndex]; + Array.Copy(ruleArguments, valueIndex, valueRuleArguments, 0, valueRuleArguments.Length); + + var serializableValueProperty = new SerializableProperty + { + Name = $"{property.Name}.Value", + Type = valueType, + Rule = valueRule.RuleName, + RuleArguments = valueRuleArguments + }; + + keyRule.GenerateSerializationMethod( + source, + indent, + serializableValueProperty + ); + } + } +} diff --git a/Projects/SerializationGenerator/SerializableMigration/Rules/ListMigrationRule.cs b/Projects/SerializationGenerator/SerializableMigration/Rules/ListMigrationRule.cs new file mode 100644 index 000000000..80bea3562 --- /dev/null +++ b/Projects/SerializationGenerator/SerializableMigration/Rules/ListMigrationRule.cs @@ -0,0 +1,133 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: ListMigrationRule.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public class ListMigrationRule : ISerializableMigrationRule + { + public string RuleName => nameof(ListMigrationRule); + + public bool GenerateRuleState( + Compilation compilation, + ISymbol symbol, + ImmutableArray attributes, + ImmutableArray serializableTypes, + out string[] ruleArguments + ) + { + if (symbol is not INamedTypeSymbol namedTypeSymbol || !symbol.IsList(compilation)) + { + ruleArguments = null; + return false; + } + + var listTypeSymbol = namedTypeSymbol.TypeArguments[0]; + + var serializableListType = SerializableMigrationRulesEngine.GenerateSerializableProperty( + compilation, + "ListEntry", + listTypeSymbol, + attributes, + serializableTypes + ); + + var length = serializableListType.RuleArguments.Length; + ruleArguments = new string[length + 2]; + ruleArguments[0] = listTypeSymbol.ToDisplayString(); + ruleArguments[1] = serializableListType.Rule; + Array.Copy(serializableListType.RuleArguments, 0, ruleArguments, 2, length); + + return true; + } + + public void GenerateDeserializationMethod(StringBuilder source, string indent, SerializableProperty property) + { + const string expectedRule = nameof(ListMigrationRule); + var ruleName = property.Rule; + if (expectedRule != ruleName) + { + throw new ArgumentException($"Invalid rule applied to property {ruleName}. Expecting {expectedRule}, but received {ruleName}."); + } + + var ruleArguments = property.RuleArguments; + var listElementRule = SerializableMigrationRulesEngine.Rules[ruleArguments[1]]; + var listElementRuleArguments = new string[ruleArguments.Length - 2]; + Array.Copy(ruleArguments, 2, listElementRuleArguments, 0, ruleArguments.Length - 2); + + var propertyName = property.Name; + var propertyVarPrefix = $"{char.ToLower(propertyName[0])}{propertyName.Substring(1, propertyName.Length - 1)}"; + var propertyIndex = $"{propertyVarPrefix}Index"; + var propertyEntry = $"{propertyVarPrefix}Entry"; + var propertyCount = $"{propertyVarPrefix}Count"; + + source.AppendLine($"{indent}{ruleArguments[0]} {propertyEntry};"); + source.AppendLine($"{indent}var {propertyCount} = reader.ReadEncodedInt();"); + source.AppendLine($"{indent}{propertyName} = new System.Collections.Generic.List<{ruleArguments[0]}>({propertyCount});"); + source.AppendLine($"{indent}for (var {propertyIndex} = 0; {propertyIndex} < {propertyCount}; {propertyIndex}++)"); + source.AppendLine($"{indent}{{"); + + var serializableListElement = new SerializableProperty + { + Name = propertyEntry, + Type = ruleArguments[0], + Rule = listElementRule.RuleName, + RuleArguments = listElementRuleArguments + }; + + listElementRule.GenerateDeserializationMethod(source, $"{indent} ", serializableListElement); + source.AppendLine($"{indent} {propertyName}.Add({propertyEntry});"); + + source.AppendLine($"{indent}}}"); + } + + public void GenerateSerializationMethod(StringBuilder source, string indent, SerializableProperty property) + { + const string expectedRule = nameof(ListMigrationRule); + var ruleName = property.Rule; + if (expectedRule != ruleName) + { + throw new ArgumentException($"Invalid rule applied to property {ruleName}. Expecting {expectedRule}, but received {ruleName}."); + } + + var ruleArguments = property.RuleArguments; + var listElementRule = SerializableMigrationRulesEngine.Rules[ruleArguments[1]]; + var listElementRuleArguments = new string[ruleArguments.Length - 2]; + Array.Copy(ruleArguments, 2, listElementRuleArguments, 0, ruleArguments.Length - 2); + + var propertyName = property.Name; + var propertyEntry = $"{char.ToLower(propertyName[0])}{propertyName.Substring(1, propertyName.Length - 1)}Entry"; + source.AppendLine($"{indent}writer.WriteEncodedInt({propertyName}.Count);"); + source.AppendLine($"{indent}foreach (var {propertyEntry} in {propertyName})"); + source.AppendLine($"{indent}{{"); + + var serializableListElement = new SerializableProperty + { + Name = propertyEntry, + Type = ruleArguments[0], + Rule = listElementRule.RuleName, + RuleArguments = listElementRuleArguments + }; + + listElementRule.GenerateSerializationMethod(source, $"{indent} ", serializableListElement); + + source.AppendLine($"{indent}}}"); + } + } +} diff --git a/Projects/SerializationGenerator/SerializableMigration/Rules/PrimitiveTypeMigrationRule.cs b/Projects/SerializationGenerator/SerializableMigration/Rules/PrimitiveTypeMigrationRule.cs new file mode 100644 index 000000000..7b0d1054e --- /dev/null +++ b/Projects/SerializationGenerator/SerializableMigration/Rules/PrimitiveTypeMigrationRule.cs @@ -0,0 +1,159 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: PrimitiveTypeMigrationRule.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public class PrimitiveTypeMigrationRule : ISerializableMigrationRule + { + public string RuleName => nameof(PrimitiveTypeMigrationRule); + + public bool GenerateRuleState( + Compilation compilation, + ISymbol symbol, + ImmutableArray attributes, + ImmutableArray serializableTypes, + out string[] ruleArguments + ) + { + if (symbol.IsIpAddress(compilation)) + { + ruleArguments = new[] { "IPAddress" }; + return true; + } + + if (symbol is not ITypeSymbol typeSymbol) + { + ruleArguments = null; + return false; + } + + if ( + typeSymbol.SpecialType is + SpecialType.System_Boolean or + SpecialType.System_SByte or + SpecialType.System_Int16 or + SpecialType.System_Int32 or + SpecialType.System_Int64 or + SpecialType.System_Byte or + SpecialType.System_UInt16 or + SpecialType.System_UInt32 or + SpecialType.System_UInt64 or + SpecialType.System_Single or + SpecialType.System_Double or + SpecialType.System_String or + SpecialType.System_Decimal or + SpecialType.System_DateTime + ) + { + ruleArguments = new[] { typeSymbol.SpecialType.ToString() }; + return true; + } + + if (typeSymbol.SpecialType == SpecialType.System_DateTime) + { + ruleArguments = attributes.Any(a => a.IsDeltaDateTime(compilation)) + ? new[] { typeSymbol.SpecialType.ToString(), "DeltaTime" } + : new[] { typeSymbol.SpecialType.ToString() }; + + return true; + } + + ruleArguments = null; + return false; + } + + public void GenerateDeserializationMethod(StringBuilder source, string indent, SerializableProperty property) + { + const string expectedRule = nameof(PrimitiveTypeMigrationRule); + var ruleName = property.Rule; + if (expectedRule != ruleName) + { + throw new ArgumentException($"Invalid rule applied to property {ruleName}. Expecting {expectedRule}, but received {ruleName}."); + } + + var propertyName = property.Name; + var ruleType = property.RuleArguments[0]; + string readMethod; + + if (ruleType == "IPAddress") + { + readMethod = "ReadIPAddress"; + } + else + { + if (!Enum.TryParse(ruleType, out var specialType)) + { + throw new ArgumentException($"Invalid rule state for property {propertyName} ({ruleType})"); + } + + readMethod = specialType switch + { + SpecialType.System_Boolean => "ReadBool", + SpecialType.System_SByte => "ReadSByte", + SpecialType.System_Int16 => "ReadShort", + SpecialType.System_Int32 => "ReadInt", + SpecialType.System_Int64 => "ReadLong", + SpecialType.System_Byte => "ReadByte", + SpecialType.System_UInt16 => "ReadUShort", + SpecialType.System_UInt32 => "ReadUInt", + SpecialType.System_UInt64 => "ReadULong", + SpecialType.System_Single => "ReadFloat", + SpecialType.System_Double => "ReadDouble", + SpecialType.System_String => "ReadString", + SpecialType.System_Decimal => "ReadDecimal", + SpecialType.System_DateTime => property.RuleArguments.Length >= 2 && + property.RuleArguments[1] == "DeltaTime" ? + "ReadDeltaTime" : + "ReadDateTime" + }; + } + + source.AppendLine($"{indent}{propertyName} = reader.{readMethod}()"); + } + + public void GenerateSerializationMethod(StringBuilder source, string indent, SerializableProperty property) + { + const string expectedRule = nameof(PrimitiveTypeMigrationRule); + var ruleName = property.Rule; + if (expectedRule != ruleName) + { + throw new ArgumentException($"Invalid rule applied to property {ruleName}. Expecting {expectedRule}, but received {ruleName}."); + } + + var propertyName = property.Name; + var ruleType = property.RuleArguments[0]; + + if (!Enum.TryParse(ruleType, out var specialType)) + { + throw new ArgumentException($"Invalid rule state for property {propertyName} ({ruleType})"); + } + + if (specialType == SpecialType.System_DateTime && property.RuleArguments[1] == "DeltaTime") + { + source.AppendLine($"{indent}writer.WriteDeltaTime({propertyName});"); + } + else + { + source.AppendLine($"{indent}writer.Write({propertyName});"); + } + } + } +} diff --git a/Projects/SerializationGenerator/SerializableMigration/Rules/PrimitiveUOTypeMigrationRule.cs b/Projects/SerializationGenerator/SerializableMigration/Rules/PrimitiveUOTypeMigrationRule.cs new file mode 100644 index 000000000..35c7a67fb --- /dev/null +++ b/Projects/SerializationGenerator/SerializableMigration/Rules/PrimitiveUOTypeMigrationRule.cs @@ -0,0 +1,75 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: PrimitiveUOTypeMigrationRule.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public class PrimitiveUOTypeMigrationRule : ISerializableMigrationRule + { + public string RuleName => nameof(PrimitiveUOTypeMigrationRule); + + public bool GenerateRuleState( + Compilation compilation, + ISymbol symbol, + ImmutableArray attributes, + ImmutableArray serializableTypes, + out string[] ruleArguments + ) + { + ruleArguments = symbol switch + { + _ when symbol.IsPoint2D(compilation) => new[] { "Point2D" }, + _ when symbol.IsPoint3D(compilation) => new[] { "Point3D" }, + _ when symbol.IsRectangle2D(compilation) => new[] { "Rect2D" }, + _ when symbol.IsRectangle3D(compilation) => new[] { "Rect3D" }, + _ when symbol.IsRace(compilation) => new[] { "Race" }, + _ when symbol.IsMap(compilation) => new[] { "Map" }, + _ => null + }; + + return ruleArguments != null; + } + + public void GenerateDeserializationMethod(StringBuilder source, string indent, SerializableProperty property) + { + const string expectedRule = nameof(PrimitiveUOTypeMigrationRule); + var ruleName = property.Rule; + if (expectedRule != ruleName) + { + throw new ArgumentException($"Invalid rule applied to property {ruleName}. Expecting {expectedRule}, but received {ruleName}."); + } + + var propertyName = property.Name; + source.AppendLine($"{indent}{propertyName} = reader.Read{property.RuleArguments[0]}()"); + } + + public void GenerateSerializationMethod(StringBuilder source, string indent, SerializableProperty property) + { + const string expectedRule = nameof(PrimitiveUOTypeMigrationRule); + var ruleName = property.Rule; + if (expectedRule != ruleName) + { + throw new ArgumentException($"Invalid rule applied to property {ruleName}. Expecting {expectedRule}, but received {ruleName}."); + } + + var propertyName = property.Name; + source.AppendLine($"{indent}writer.Write({propertyName});"); + } + } +} diff --git a/Projects/SerializationGenerator/SerializableMigration/Rules/SerializableInterfaceMigrationRule.cs b/Projects/SerializationGenerator/SerializableMigration/Rules/SerializableInterfaceMigrationRule.cs new file mode 100644 index 000000000..1b6aa30a4 --- /dev/null +++ b/Projects/SerializationGenerator/SerializableMigration/Rules/SerializableInterfaceMigrationRule.cs @@ -0,0 +1,71 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SerializableInterfaceMigrationRule.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public class SerializableInterfaceMigrationRule : ISerializableMigrationRule + { + public string RuleName => nameof(SerializableInterfaceMigrationRule); + + public bool GenerateRuleState( + Compilation compilation, + ISymbol symbol, + ImmutableArray attributes, + ImmutableArray serializableTypes, + out string[] ruleArguments + ) + { + if (symbol is ITypeSymbol typeSymbol && typeSymbol.HasSerializableInterface(compilation, serializableTypes)) + { + ruleArguments = Array.Empty(); + return true; + } + + ruleArguments = null; + return false; + } + + public void GenerateDeserializationMethod(StringBuilder source, string indent, SerializableProperty property) + { + const string expectedRule = nameof(SerializableInterfaceMigrationRule); + var ruleName = property.Rule; + if (expectedRule != ruleName) + { + throw new ArgumentException($"Invalid rule applied to property {ruleName}. Expecting {expectedRule}, but received {ruleName}."); + } + + var propertyName = property.Name; + source.AppendLine($"{indent}{propertyName} = reader.ReadEntity<{property.Type}>();"); + } + + public void GenerateSerializationMethod(StringBuilder source, string indent, SerializableProperty property) + { + const string expectedRule = nameof(SerializableInterfaceMigrationRule); + var ruleName = property.Rule; + if (expectedRule != ruleName) + { + throw new ArgumentException($"Invalid rule applied to property {ruleName}. Expecting {expectedRule}, but received {ruleName}."); + } + + var propertyName = property.Name; + source.AppendLine($"{indent}writer.Write({propertyName});"); + } + } +} diff --git a/Projects/SerializationGenerator/SerializableMigration/Rules/SerializationMethodSignatureMigrationRule.cs b/Projects/SerializationGenerator/SerializableMigration/Rules/SerializationMethodSignatureMigrationRule.cs new file mode 100644 index 000000000..2b6b139fc --- /dev/null +++ b/Projects/SerializationGenerator/SerializableMigration/Rules/SerializationMethodSignatureMigrationRule.cs @@ -0,0 +1,81 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SerializationMethodSignatureMigrationRule.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public class SerializationMethodSignatureMigrationRule : ISerializableMigrationRule + { + public string RuleName => nameof(SerializationMethodSignatureMigrationRule); + + public bool GenerateRuleState( + Compilation compilation, + ISymbol symbol, + ImmutableArray attributes, + ImmutableArray serializableTypes, + out string[] ruleArguments + ) + { + if ((symbol as ITypeSymbol)?.HasPublicSerializeMethod(compilation, serializableTypes) != true) + { + ruleArguments = null; + return false; + } + + if (symbol is not INamedTypeSymbol namedTypeSymbol || + !namedTypeSymbol.HasGenericReaderCtor(compilation, out var requiresParent)) + { + ruleArguments = null; + return false; + } + + ruleArguments = requiresParent ? new[] { "DeserializationRequiresParent" } : Array.Empty(); + return true; + } + + public void GenerateDeserializationMethod(StringBuilder source, string indent, SerializableProperty property) + { + const string expectedRule = nameof(SerializationMethodSignatureMigrationRule); + var ruleName = property.Rule; + if (expectedRule != ruleName) + { + throw new ArgumentException($"Invalid rule applied to property {ruleName}. Expecting {expectedRule}, but received {ruleName}."); + } + + var propertyName = property.Name; + var argument = property.RuleArguments.Length >= 1 && + property.RuleArguments[0] == "DeserializationRequiresParent" ? ", this" : ""; + + source.AppendLine($"{indent}{propertyName} = new {property.Type}(reader{argument})"); + } + + public void GenerateSerializationMethod(StringBuilder source, string indent, SerializableProperty property) + { + const string expectedRule = nameof(SerializationMethodSignatureMigrationRule); + var ruleName = property.Rule; + if (expectedRule != ruleName) + { + throw new ArgumentException($"Invalid rule applied to property {ruleName}. Expecting {expectedRule}, but received {ruleName}."); + } + + var propertyName = property.Name; + source.AppendLine($"{indent}{propertyName}.Serialize(writer);"); + } + } +} diff --git a/Projects/SerializationGenerator/SerializableMigration/SerializableMetadata.cs b/Projects/SerializationGenerator/SerializableMigration/SerializableMetadata.cs new file mode 100644 index 000000000..d827cec5a --- /dev/null +++ b/Projects/SerializationGenerator/SerializableMigration/SerializableMetadata.cs @@ -0,0 +1,32 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SerializableMigration.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace SerializationGenerator +{ + public class SerializableMetadata + { + [JsonPropertyName("version")] + public int Version { get; set; } + + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("properties")] + public List Properties { get; set; } + } +} diff --git a/Projects/SerializationGenerator/SerializableMigration/SerializableMetadataComparer.cs b/Projects/SerializationGenerator/SerializableMigration/SerializableMetadataComparer.cs new file mode 100644 index 000000000..713fd92ba --- /dev/null +++ b/Projects/SerializationGenerator/SerializableMigration/SerializableMetadataComparer.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace SerializationGenerator +{ + public class SerializableMetadataComparer : IComparer + { + public int Compare(SerializableMetadata x, SerializableMetadata y) + { + if (ReferenceEquals(x, y)) + { + return 0; + } + + if (ReferenceEquals(null, y)) + { + return 1; + } + + if (ReferenceEquals(null, x)) + { + return -1; + } + + return x.Version.CompareTo(y.Version); + } + } +} diff --git a/Projects/SerializationGenerator/SerializableMigration/SerializableMigration.ContentStruct.cs b/Projects/SerializationGenerator/SerializableMigration/SerializableMigration.ContentStruct.cs new file mode 100644 index 000000000..2e41f0be0 --- /dev/null +++ b/Projects/SerializationGenerator/SerializableMigration/SerializableMigration.ContentStruct.cs @@ -0,0 +1,53 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SerializableMigration.ContentStruct.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.Text; + +namespace SerializationGenerator +{ + public static partial class SerializableMigration + { + public static void GenerateMigrationContentStruct( + this StringBuilder source, + SerializableMetadata migration + ) + { + const string indent = " "; + + source.AppendLine($"{indent}ref struct V{migration.Version}Content"); + source.AppendLine($"{indent}{{"); + foreach (var serializableProperty in migration.Properties) + { + source.AppendLine($"{indent} internal readonly {serializableProperty.Type} {serializableProperty.Name};"); + } + + source.AppendLine($"{indent} internal V{migration.Version}Content(IGenericReader reader)"); + source.AppendLine($"{indent} {{"); + + foreach (var serializableProperty in migration.Properties) + { + SerializableMigrationRulesEngine.Rules[serializableProperty.Rule].GenerateDeserializationMethod( + source, + $"{indent} ", + serializableProperty + ); + } + + source.AppendLine($"{indent} }}"); + + source.AppendLine($"{indent}}}"); + } + } +} diff --git a/Projects/SerializationGenerator/SerializableMigration/SerializableMigration.cs b/Projects/SerializationGenerator/SerializableMigration/SerializableMigration.cs new file mode 100644 index 000000000..5171d654a --- /dev/null +++ b/Projects/SerializationGenerator/SerializableMigration/SerializableMigration.cs @@ -0,0 +1,78 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SerializableMigration.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public static partial class SerializableMigration + { + public static JsonSerializerOptions GetJsonSerializerOptions(Compilation compilation) => + new() + { + WriteIndented = true, + AllowTrailingCommas = true, + IgnoreNullValues = true, + ReadCommentHandling = JsonCommentHandling.Skip + }; + + public static string GetMigrationPath(GeneratorExecutionContext context) + { + context.AnalyzerConfigOptions.GlobalOptions.TryGetValue( + "build_property.SerializableMigrationPath", + out var migrationPath + ); + + return migrationPath; + } + + public static List GetMigrations( + string migrationPath, + INamedTypeSymbol typeSymbol, + int version, + JsonSerializerOptions options + ) + { + var typeName = typeSymbol.ToDisplayString(); + + var migrations = new SortedSet(new SerializableMetadataComparer()); + var migrationFiles = Directory.GetFiles(migrationPath, $"{typeName}.v*.json"); + + foreach (var migrationFile in migrationFiles) + { + var text = File.ReadAllText(migrationFile, Encoding.UTF8); + var migration = JsonSerializer.Deserialize(text, options); + if (typeName == migration!.Type && version > migration.Version) + { + migrations.Add(migration); + } + } + + return migrations.ToList(); + } + + public static void WriteMigration(string migrationPath, SerializableMetadata metadata, JsonSerializerOptions options) + { + Directory.CreateDirectory(migrationPath); + var filePath = Path.Combine(migrationPath, $"{metadata.Type}.v{metadata.Version}.json"); + File.WriteAllText(filePath, JsonSerializer.Serialize(metadata, options)); + } + } +} diff --git a/Projects/SerializationGenerator/SerializableMigration/SerializableMigrationRulesEngine.cs b/Projects/SerializationGenerator/SerializableMigration/SerializableMigrationRulesEngine.cs new file mode 100644 index 000000000..6ddcd7bbe --- /dev/null +++ b/Projects/SerializationGenerator/SerializableMigration/SerializableMigrationRulesEngine.cs @@ -0,0 +1,78 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SerializableMigrationRulesEngine.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public static class SerializableMigrationRulesEngine + { + public static readonly Dictionary Rules = new(); + + static SerializableMigrationRulesEngine() + { + var rules = new ISerializableMigrationRule[] + { + new ArrayMigrationRule(), + new HashSetMigrationRule(), + new KeyValuePairMigrationRule(), + new ListMigrationRule(), + new PrimitiveTypeMigrationRule(), + new PrimitiveUOTypeMigrationRule(), + new SerializableInterfaceMigrationRule(), + new SerializationMethodSignatureMigrationRule() + }; + + foreach (var rule in rules) + { + Rules.Add(rule.RuleName, rule); + } + } + + public static SerializableProperty GenerateSerializableProperty( + Compilation compilation, + string propertyName, + ISymbol propertyType, + ImmutableArray attributes, + ImmutableArray serializableTypes + ) + { + foreach (var rule in Rules.Values) + { + if (rule.GenerateRuleState( + compilation, + propertyType, + attributes, + serializableTypes, + out var ruleArguments + )) + { + return new SerializableProperty + { + Name = propertyName, + Type = propertyType.ToDisplayString(), + Rule = rule.RuleName, + RuleArguments = ruleArguments + }; + } + } + + throw new Exception($"No rule found for property {propertyName} of type {propertyType} ({Rules.Count})"); + } + } +} diff --git a/Projects/SerializationGenerator/SerializableMigration/SerializableProperty.cs b/Projects/SerializationGenerator/SerializableMigration/SerializableProperty.cs new file mode 100644 index 000000000..99b19cd48 --- /dev/null +++ b/Projects/SerializationGenerator/SerializableMigration/SerializableProperty.cs @@ -0,0 +1,34 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SerializableProperty.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.Text.Json.Serialization; + +namespace SerializationGenerator +{ + public class SerializableProperty + { + [JsonPropertyName("name")] + public string Name { get; init; } + + [JsonPropertyName("type")] + public string Type { get; init; } + + [JsonPropertyName("rule")] + public string Rule { get; init; } + + [JsonPropertyName("ruleArguments")] + public string[] RuleArguments { get; init; } + } +} diff --git a/Projects/SerializationGenerator/SerializationGenerator.csproj b/Projects/SerializationGenerator/SerializationGenerator.csproj new file mode 100755 index 000000000..9186dc8dd --- /dev/null +++ b/Projects/SerializationGenerator/SerializationGenerator.csproj @@ -0,0 +1,25 @@ + + + netstandard2.0 + preview + analyzers + + + + + + + + + + + $(GetTargetPathDependsOn);GetDependencyTargetPaths + + + + + + + + + diff --git a/Projects/SerializationGenerator/SerializerSyntaxReceiver.cs b/Projects/SerializationGenerator/SerializerSyntaxReceiver.cs new file mode 100755 index 000000000..7bc0f9bba --- /dev/null +++ b/Projects/SerializationGenerator/SerializerSyntaxReceiver.cs @@ -0,0 +1,48 @@ +/************************************************************************* + * ModernUO * + * Copyright (C) 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SyntaxReceiver.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace SerializationGenerator +{ + public class SerializerSyntaxReceiver : ISyntaxContextReceiver + { + public List Fields { get; } = new(); + + public static HashSet AttributeTypes { get; } = new(); + + public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + { + if (context.Node is FieldDeclarationSyntax { AttributeLists: { Count: > 0 } } fieldDeclarationSyntax) + { + foreach (VariableDeclaratorSyntax variable in fieldDeclarationSyntax.Declaration.Variables) + { + if (context.SemanticModel.GetDeclaredSymbol(variable) is not IFieldSymbol fieldSymbol) + { + return; + } + + if (fieldSymbol.GetAttributes().Any(ad => AttributeTypes.Contains(ad.AttributeClass?.ToDisplayString()))) + { + Fields.Add(fieldSymbol); + } + } + } + } + } +} diff --git a/Projects/SerializationGenerator/SourceGeneration/Helpers.cs b/Projects/SerializationGenerator/SourceGeneration/Helpers.cs new file mode 100644 index 000000000..8bc0dd4fe --- /dev/null +++ b/Projects/SerializationGenerator/SourceGeneration/Helpers.cs @@ -0,0 +1,44 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: Helpers.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public static class Helpers + { + public static bool ContainsInterface(this ITypeSymbol symbol, ISymbol interfaceSymbol) => + symbol.Interfaces.Any(i => i.ConstructedFrom.Equals(interfaceSymbol, SymbolEqualityComparer.Default)) || + symbol.AllInterfaces.Any(i => i.ConstructedFrom.Equals(interfaceSymbol, SymbolEqualityComparer.Default)); + + public static ImmutableArray GetAllMethods(this ITypeSymbol symbol, string name) + { + var methods = symbol.GetMembers(name).OfType().ToImmutableArray(); + if (symbol.ContainingSymbol is not ITypeSymbol typeSymbol) + { + return methods; + } + + var list = new List(); + list.AddRange(methods.ToList()); + list.AddRange(GetAllMethods(typeSymbol, name).ToList()); + + return list.ToImmutableArray(); + } + } +} diff --git a/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.AccessModifier.cs b/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.AccessModifier.cs new file mode 100644 index 000000000..ed769c7dc --- /dev/null +++ b/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.AccessModifier.cs @@ -0,0 +1,43 @@ +/************************************************************************* + * ModernUO * + * Copyright (C) 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SourceGeneration.AccessModifier.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +namespace SerializationGenerator +{ + public enum AccessModifier + { + None, + Public, + Private, + Protected, + Internal, + ProtectedInternal, + PrivateProtected + } + + public static partial class SourceGeneration + { + public static string ToFriendlyString(this AccessModifier modifier) => + modifier switch + { + AccessModifier.Public => "public", + AccessModifier.Private => "private", + AccessModifier.Protected => "protected", + AccessModifier.Internal => "internal", + AccessModifier.ProtectedInternal => "protected internal", + AccessModifier.PrivateProtected => "private protected", + _ => "" + }; + } +} diff --git a/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Arguments.cs b/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Arguments.cs new file mode 100644 index 000000000..0d194d65f --- /dev/null +++ b/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Arguments.cs @@ -0,0 +1,125 @@ +/************************************************************************* + * ModernUO * + * Copyright (C) 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SourceGeneration.Arguments.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public static partial class SourceGeneration + { + public static void GetTypesFromTypedConstant(TypedConstant arg, List list) + { + if (arg.Kind == TypedConstantKind.Type) + { + list.Add((ITypeSymbol)arg.Value); + } + else if (arg.Kind == TypedConstantKind.Array) + { + for (var i = 0; i < arg.Values.Length; i++) + { + GetTypesFromTypedConstant(arg.Values[i], list); + } + } + } + + public static void GenerateSignatureArguments(this StringBuilder source, ImmutableArray<(ITypeSymbol, string)> parameters) + { + for (var i = 0; i < parameters.Length; i++) + { + var (t, v) = parameters[i]; + source.AppendFormat("{0} {1}", t.Name, v); + if (i < parameters.Length - 1) + { + source.Append(", "); + } + } + } + + public static void GenerateNamedArgument(this StringBuilder source, KeyValuePair namedArg) + { + source.AppendFormat("{0} = ", namedArg.Key); + source.GenerateTypedConstant(namedArg.Value); + } + + public static void GenerateTypedConstants(this StringBuilder source, ImmutableArray args) + { + source.Append("new []{"); + for (var i = 0; i < args.Length; i++) + { + source.GenerateTypedConstant(args[i]); + if (i < args.Length - 1) + { + source.Append(", "); + } + } + source.Append('}'); + } + + public static void GenerateTypedConstant(this StringBuilder source, TypedConstant arg) + { + if (arg.IsNull) + { + source.Append("null"); + return; + } + + switch (arg.Kind) + { + default: + { + return; + } + case TypedConstantKind.Primitive: + { + + if (arg.Value is string str) + { + source.AppendFormat("\"{0}\"", str); + } + else + { + source.Append(arg.Value); + } + break; + } + case TypedConstantKind.Enum: + { + if (arg.Type == null || arg.Value == null) + { + source.Append("null"); + } + else + { + source.AppendFormat("({0}){1}", arg.Type.ToDisplayString(), arg.Value); + } + break; + } + case TypedConstantKind.Type: + { + source.AppendFormat("typeof({0})", ((ITypeSymbol)arg.Value)?.Name); + break; + } + case TypedConstantKind.Array: + { + source.GenerateTypedConstants(arg.Values); + break; + } + } + } + } +} diff --git a/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Attribute.cs b/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Attribute.cs new file mode 100644 index 000000000..98e312beb --- /dev/null +++ b/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Attribute.cs @@ -0,0 +1,92 @@ +/************************************************************************* + * ModernUO * + * Copyright (C) 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SourceGeneration.Attribute.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public static partial class SourceGeneration + { + public static void GenerateAttribute(this StringBuilder source, string attrClassName, ImmutableArray args) + { + source.Append($" [{attrClassName}"); + var hasArgs = args.Length > 0; + + if (hasArgs) + { + source.Append("("); + } + + for (var i = 0; i < args.Length; i++) + { + var arg = args[i]; + source.GenerateTypedConstant(arg); + if (i < args.Length - 1) + { + source.Append(", "); + } + } + + if (hasArgs) + { + source.Append(")"); + } + + source.AppendLine("]"); + } + + public static void GenerateAttribute(this StringBuilder source, AttributeData attr) + { + source.Append($" [{attr.AttributeClass?.Name}"); + var ctorArgs = attr.ConstructorArguments; + var namedArgs = attr.NamedArguments; + var hasArgs = ctorArgs.Length + namedArgs.Length > 0; + + if (hasArgs) + { + source.Append("("); + } + + for (var i = 0; i < ctorArgs.Length; i++) + { + var arg = ctorArgs[i]; + source.GenerateTypedConstant(arg); + if (i < ctorArgs.Length - 1) + { + source.Append(", "); + } + } + + for (var i = 0; i < namedArgs.Length; i++) + { + var arg = namedArgs[i]; + source.GenerateNamedArgument(arg); + if (i < namedArgs.Length - 1) + { + source.Append(", "); + } + } + + if (hasArgs) + { + source.Append(")"); + } + + source.AppendLine("]"); + } + } +} diff --git a/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Class.cs b/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Class.cs new file mode 100644 index 000000000..109be6529 --- /dev/null +++ b/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Class.cs @@ -0,0 +1,76 @@ +/************************************************************************* + * ModernUO * + * Copyright (C) 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SourceGeneration.Class.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public static partial class SourceGeneration + { + public static void GenerateClassStart(this StringBuilder source, string className, ImmutableArray interfaces) + { + source.Append($" public partial class {className}"); + if (!interfaces.IsEmpty) + { + source.Append(" : "); + for (var i = 0; i < interfaces.Length; i++) + { + source.Append(interfaces[i].ToDisplayString()); + if (i < interfaces.Length - 1) + { + source.Append(", "); + } + } + } + + source.AppendLine(@" + {"); + } + + public static void GenerateClassEnd(this StringBuilder source) + { + source.AppendLine(" }"); + } + + // TODO: Generalize this to any field using dynamic indentation + public static void GenerateClassField( + this StringBuilder source, + AccessModifier accessors, + InstanceModifier instance, + string type, + string variableName, + string value, + bool unusedPragma = false + ) + { + if (unusedPragma) + { + source.AppendLine("#pragma warning disable 0414"); // assigned, but never used + } + + var instanceStr = instance == InstanceModifier.None ? "" : $"{instance.ToFriendlyString()} "; + var accessorStr = accessors == AccessModifier.None ? "" : $"{accessors.ToFriendlyString()} "; + var valueStr = value == null ? "" : $" = {value}"; + source.AppendLine($" {accessorStr}{instanceStr}{type} {variableName}{valueStr};"); + + if (unusedPragma) + { + source.AppendLine("#pragma warning restore 0414"); + } + } + } +} diff --git a/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.InstanceModifier.cs b/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.InstanceModifier.cs new file mode 100644 index 000000000..2fdfef6da --- /dev/null +++ b/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.InstanceModifier.cs @@ -0,0 +1,39 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SourceGeneration.InstanceModifier.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +namespace SerializationGenerator +{ + public enum InstanceModifier + { + None, + Const, + ReadOnly, + Static, + StaticReadOnly + } + + public static partial class SourceGeneration + { + public static string ToFriendlyString(this InstanceModifier modifier) => + modifier switch + { + InstanceModifier.Const => "const", + InstanceModifier.ReadOnly => "readonly", + InstanceModifier.Static => "static", + InstanceModifier.StaticReadOnly => "static readonly", + _ => "" + }; + } +} diff --git a/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Method.cs b/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Method.cs new file mode 100644 index 000000000..f6863fc19 --- /dev/null +++ b/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Method.cs @@ -0,0 +1,60 @@ +/************************************************************************* + * ModernUO * + * Copyright (C) 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SourceGeneration.Method.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public static partial class SourceGeneration + { + public static void GenerateMethodStart(this StringBuilder source, string methodName, AccessModifier accessors, bool isOverride, string returnType, ImmutableArray<(ITypeSymbol, string)> parameters) + { + source.Append($" {accessors.ToFriendlyString()}{(isOverride ? " override" : "")} {returnType} {methodName}("); + source.GenerateSignatureArguments(parameters); + source.AppendLine(@") + {"); + } + + public static void GenerateMethodEnd(this StringBuilder source) => source.AppendLine(@" }"); + + public static void GenerateConstructorStart( + this StringBuilder source, string className, AccessModifier accessors, ImmutableArray<(ITypeSymbol, string)> parameters, + ImmutableArray baseParameters, bool isOverload = false + ) + { + source.Append($" {accessors.ToFriendlyString()} {className}("); + source.GenerateSignatureArguments(parameters); + source.Append(')'); + bool hasBaseParams = baseParameters.Length > 0; + if (hasBaseParams) + { + source.AppendFormat(" : {0}(", isOverload ? "this" : "base"); + for (int i = 0; i < baseParameters.Length; i++) + { + source.Append(baseParameters[i]); + if (i < baseParameters.Length - 1) + { + source.Append(','); + } + } + source.Append(')'); + } + + source.AppendLine("\n {"); + } + } +} diff --git a/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Namespace.cs b/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Namespace.cs new file mode 100644 index 000000000..5f25e614b --- /dev/null +++ b/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Namespace.cs @@ -0,0 +1,50 @@ +/************************************************************************* + * ModernUO * + * Copyright (C) 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SourceGeneration.Namespace.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public static partial class SourceGeneration + { + public static void GenerateUsings(this StringBuilder source, IImmutableList typesUsed) + { + var enumerable = typesUsed + .Select(t => t.ContainingNamespace.Name) + .Distinct() + .OrderByDescending(t => t); + + foreach (var t in enumerable) + { + source.Insert(0, $"using {t}{Environment.NewLine}"); + } + } + + public static void GenerateNamespaceStart(this StringBuilder source, string namespaceName) + { + source.AppendLine($@"namespace {namespaceName} +{{"); + } + + public static void GenerateNamespaceEnd(this StringBuilder source) + { + source.AppendLine("}"); + } + } +} diff --git a/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Property.cs b/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Property.cs new file mode 100644 index 000000000..e803944a5 --- /dev/null +++ b/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Property.cs @@ -0,0 +1,107 @@ +/************************************************************************* + * ModernUO * + * Copyright (C) 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SourceGeneration.Property.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; +using System.Text; +using Humanizer; +using Microsoft.CodeAnalysis; + +namespace SerializationGenerator +{ + public static partial class SourceGeneration + { + public static string GetPropertyName(this IFieldSymbol fieldSymbol) + { + var fieldName = fieldSymbol.Name; + + var propertyName = fieldName; + + if (propertyName.StartsWith("m_", StringComparison.OrdinalIgnoreCase)) + { + propertyName = propertyName.Substring(2); + } + else if (propertyName.StartsWith("_", StringComparison.OrdinalIgnoreCase)) + { + propertyName = propertyName.Substring(1); + } + + return propertyName.Dehumanize(); + } + + public static void GeneratePropertyStart( + this StringBuilder source, + AccessModifier accessors, + IFieldSymbol fieldSymbol + ) + { + var propertyName = fieldSymbol.GetPropertyName(); + + source.AppendLine($@" {accessors.ToFriendlyString()} {fieldSymbol.Type} {propertyName} + {{"); + } + + public static void GenerateAutoProperty( + this StringBuilder source, + AccessModifier accessors, + string type, + string propertyName, + AccessModifier? getAccessor, + AccessModifier? setAccessor, + bool useInit = false + ) + { + if (getAccessor == null && setAccessor == null) + { + throw new ArgumentNullException($"Must specify a {nameof(getAccessor)} or {nameof(setAccessor)} parameter"); + } + + var getter = getAccessor == null ? + "" : + $"{(getAccessor != AccessModifier.None ? $"{getAccessor.Value.ToFriendlyString()} " : "")}get;"; + + var getterSpace = getAccessor != null ? " " : ""; + var setOrInit = useInit ? "init;" : "set;"; + + var setterAccessor = setAccessor != AccessModifier.None ? $"{setAccessor?.ToFriendlyString() ?? ""} " : ""; + var setter = setterAccessor == "" ? "" : $"{getterSpace}{setterAccessor}{setOrInit}"; + + var propertyAccessor = accessors == AccessModifier.None ? "" : $"{accessors.ToFriendlyString()} "; + + source.AppendLine($"{propertyAccessor}{type} {propertyName} {{ {getter}{setter} }}"); + } + + public static void GeneratePropertyEnd(this StringBuilder source) => source.AppendLine(" }"); + + public static void GeneratePropertyGetterReturnsField(this StringBuilder source, IFieldSymbol fieldSymbol) => + source.AppendLine($" get => {fieldSymbol.Name};"); + + public static void GeneratePropertyGetterStart(this StringBuilder source, bool useExpression) => + source.AppendLine($" get{(useExpression ? " => " : "\n {")}"); + + public static void GeneratePropertyGetSetEnd(this StringBuilder source, bool useExpression) + { + if (!useExpression) + { + source.AppendLine(" }"); + } + } + + public static void GeneratePropertySetterSetsValue(this StringBuilder source, IFieldSymbol fieldSymbol) => + source.AppendLine($" set => {fieldSymbol.Name} = value;"); + + public static void GeneratePropertySetterStart(this StringBuilder source, bool useExpression, bool useInit = false) => + source.AppendLine($" {(useInit ? "init" : "set")}{(useExpression ? " => " : "\n {")}"); + } +} diff --git a/Projects/Server.Tests/Tests/Serialization/EnumConversionTests.cs b/Projects/Server.Tests/Tests/Serialization/EnumConversionTests.cs new file mode 100644 index 000000000..4e95890ad --- /dev/null +++ b/Projects/Server.Tests/Tests/Serialization/EnumConversionTests.cs @@ -0,0 +1,22 @@ +using System; +using Xunit; + +namespace Server.Tests +{ + public class EnumConversionTests + { + [Fact] + public void TestToEnum() + { + var e = ReadEnum(); + + Assert.Equal(TileFlag.Container, e); + } + + private unsafe T ReadEnum() where T : unmanaged, Enum + { + var num = (long)TileFlag.Container; + return *(T*)# + } + } +} diff --git a/Projects/Server/Guild.cs b/Projects/Server/Guild.cs index daa22153f..1b07f9047 100644 --- a/Projects/Server/Guild.cs +++ b/Projects/Server/Guild.cs @@ -1,3 +1,19 @@ +/************************************************************************* + * ModernUO * + * Copyright (C) 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: Guild.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; using System.Collections.Generic; namespace Server.Guilds @@ -11,31 +27,27 @@ namespace Server.Guilds public abstract class BaseGuild : ISerializable { - protected BaseGuild(Serial serial) - { - Serial = serial; - - var ourType = GetType(); - TypeRef = World.GuildTypes.IndexOf(ourType); - - if (TypeRef == -1) - { - World.GuildTypes.Add(ourType); - TypeRef = World.GuildTypes.Count - 1; - } - } - protected BaseGuild() { Serial = World.NewGuild; World.AddGuild(this); - var ourType = GetType(); - TypeRef = World.GuildTypes.IndexOf(ourType); + SetTypeRef(GetType()); + } + + protected BaseGuild(Serial serial) + { + Serial = serial; + SetTypeRef(GetType()); + } + + public void SetTypeRef(Type type) + { + TypeRef = World.GuildTypes.IndexOf(type); if (TypeRef == -1) { - World.GuildTypes.Add(ourType); + World.GuildTypes.Add(type); TypeRef = World.GuildTypes.Count - 1; } } @@ -51,9 +63,11 @@ namespace Server.Guilds [CommandProperty(AccessLevel.Counselor)] public Serial Serial { get; } + long ISerializable.SavePosition { get; set; } + BufferWriter ISerializable.SaveBuffer { get; set; } - public int TypeRef { get; } + public int TypeRef { get; private set; } public abstract void Serialize(IGenericWriter writer); public abstract void Deserialize(IGenericReader reader); diff --git a/Projects/Server/IEntity.cs b/Projects/Server/IEntity.cs index 8a22e4950..f30269b17 100644 --- a/Projects/Server/IEntity.cs +++ b/Projects/Server/IEntity.cs @@ -13,15 +13,14 @@ * along with this program. If not, see . * *************************************************************************/ +using System; + namespace Server { public interface IEntity : IPoint3D, ISerializable { - // Serial Serial { get; } Point3D Location { get; } Map Map { get; } - bool Deleted { get; } - void Delete(); void MoveToWorld(Point3D location, Map map); void ProcessDelta(); @@ -45,6 +44,12 @@ namespace Server Deleted = false; } + public void SetTypeRef(Type type) + { + } + + long ISerializable.SavePosition { get; set; } + BufferWriter ISerializable.SaveBuffer { get; set; } public int TypeRef { get; } = -1; @@ -61,7 +66,7 @@ namespace Server public Map Map { get; private set; } - public virtual void MoveToWorld(Point3D newLocation, Map map) + public void MoveToWorld(Point3D newLocation, Map map) { Location = newLocation; Map = map; diff --git a/Projects/Server/Items/Item.cs b/Projects/Server/Items/Item.cs index e21077d3b..6f9f2e258 100644 --- a/Projects/Server/Items/Item.cs +++ b/Projects/Server/Items/Item.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Runtime.CompilerServices; using Server.ContextMenus; using Server.Items; @@ -216,9 +215,6 @@ namespace Server private ObjectPropertyList m_PropertyList; - // Position in the save buffer where serialization ends. -1 if dirty - private int _savePosition = -1; - [Constructible] public Item(int itemID = 0) { @@ -234,27 +230,22 @@ namespace Server SetLastMoved(); World.AddEntity(this); - - var ourType = GetType(); - TypeRef = World.ItemTypes.IndexOf(ourType); - - if (TypeRef == -1) - { - World.ItemTypes.Add(ourType); - TypeRef = World.ItemTypes.Count - 1; - } + SetTypeRef(GetType()); } public Item(Serial serial) { Serial = serial; + SetTypeRef(GetType()); + } - var ourType = GetType(); - TypeRef = World.ItemTypes.IndexOf(ourType); + public void SetTypeRef(Type type) + { + TypeRef = World.ItemTypes.IndexOf(type); if (TypeRef == -1) { - World.ItemTypes.Add(ourType); + World.ItemTypes.Add(type); TypeRef = World.ItemTypes.Count - 1; } } @@ -792,22 +783,17 @@ namespace Server AddNameProperties(list); } + long ISerializable.SavePosition { get; set; } + BufferWriter ISerializable.SaveBuffer { get; set; } [CommandProperty(AccessLevel.Counselor)] public Serial Serial { get; } - public int TypeRef { get; } + public int TypeRef { get; private set; } public virtual void Serialize(IGenericWriter writer) { - // The item is clean, so let's skip - if (_savePosition > -1) - { - writer.Seek(_savePosition, SeekOrigin.Begin); - return; - } - writer.Write(9); // version var flags = SaveFlag.None; diff --git a/Projects/Server/Json/Converters/TypeConverter.cs b/Projects/Server/Json/Converters/TypeConverter.cs index 1409dc16f..d21531317 100644 --- a/Projects/Server/Json/Converters/TypeConverter.cs +++ b/Projects/Server/Json/Converters/TypeConverter.cs @@ -35,7 +35,7 @@ namespace Server.Json Console.WriteLine("Invalid type {0} deserialized", typeName); } - return AssemblyHandler.FindTypeByName(reader.GetString()); + return type; } public override void Write(Utf8JsonWriter writer, Type value, JsonSerializerOptions options) => diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index ab862a642..f603f63d2 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Runtime.CompilerServices; -using System.Runtime.Serialization; using Microsoft.Toolkit.HighPerformance; using Server.Accounting; using Server.Buffers; @@ -379,22 +377,6 @@ namespace Server Cured } - [Serializable] - public class MobileNotConnectedException : Exception - { - public MobileNotConnectedException(Mobile source, string message) - : base(message) => - Source = source.ToString(); - - public MobileNotConnectedException(Mobile source, string message, Exception innerException) - : base(message, innerException) => - Source = source.ToString(); - - protected MobileNotConnectedException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } - } - public delegate bool SkillCheckTargetHandler( Mobile from, SkillName skill, object target, double minSkill, double maxSkill @@ -413,8 +395,7 @@ namespace Server public delegate bool AllowHarmfulHandler(Mobile from, Mobile target); public delegate Container CreateCorpseHandler( - Mobile from, HairInfo hair, FacialHairInfo facialhair, - List initialContent, List equippedItems + Mobile from, HairInfo hair, FacialHairInfo facialhair, List initialContent, List equippedItems ); public delegate int AOSStatusHandler(Mobile from, int index); @@ -570,8 +551,17 @@ namespace Server private bool m_YellowHealthbar; - // Position in the save buffer where serialization ends. -1 if dirty - private int _savePosition = -1; + public Mobile() + { + m_Region = Map.Internal.DefaultRegion; + Serial = World.NewMobile; + + DefaultMobileInit(); + + World.AddEntity(this); + + SetTypeRef(GetType()); + } public Mobile(Serial serial) { @@ -582,31 +572,16 @@ namespace Server NextSkillTime = Core.TickCount; DamageEntries = new List(); - var ourType = GetType(); - TypeRef = World.MobileTypes.IndexOf(ourType); - - if (TypeRef == -1) - { - World.MobileTypes.Add(ourType); - TypeRef = World.MobileTypes.Count - 1; - } + SetTypeRef(GetType()); } - public Mobile() + public void SetTypeRef(Type type) { - m_Region = Map.Internal.DefaultRegion; - Serial = World.NewMobile; - - DefaultMobileInit(); - - World.AddEntity(this); - - var ourType = GetType(); - TypeRef = World.MobileTypes.IndexOf(ourType); + TypeRef = World.MobileTypes.IndexOf(type); if (TypeRef == -1) { - World.MobileTypes.Add(ourType); + World.MobileTypes.Add(type); TypeRef = World.MobileTypes.Count - 1; } } @@ -2547,22 +2522,17 @@ namespace Server AddNameProperties(list); } + long ISerializable.SavePosition { get; set; } + BufferWriter ISerializable.SaveBuffer { get; set; } [CommandProperty(AccessLevel.Counselor)] public Serial Serial { get; } - public int TypeRef { get; } + public int TypeRef { get; private set; } public virtual void Serialize(IGenericWriter writer) { - // The item is clean, so let's skip - if (_savePosition > -1) - { - writer.Seek(_savePosition, SeekOrigin.Begin); - return; - } - writer.Write(32); // version writer.WriteDeltaTime(LastStrGain); @@ -8373,7 +8343,7 @@ namespace Server public void Yell(int number, string args = "") => PublicOverheadMessage(MessageType.Yell, YellHue, number, args); - public bool SendHuePicker(HuePicker p, bool throwOnOffline = false) + public bool SendHuePicker(HuePicker p) { if (m_NetState != null) { @@ -8381,11 +8351,6 @@ namespace Server return true; } - if (throwOnOffline) - { - throw new MobileNotConnectedException(this, "Hue picker could not be sent."); - } - return false; } diff --git a/Projects/Server/Serialization/BufferWriter.cs b/Projects/Server/Serialization/BufferWriter.cs index aa2973e4e..9503fd884 100644 --- a/Projects/Server/Serialization/BufferWriter.cs +++ b/Projects/Server/Serialization/BufferWriter.cs @@ -16,7 +16,6 @@ using System; using System.Diagnostics; using System.IO; -using System.Net; using System.Runtime.CompilerServices; using System.Text; using Server.Text; diff --git a/Projects/Server/Serialization/DeltaDateTimeAttribute.cs b/Projects/Server/Serialization/DeltaDateTimeAttribute.cs new file mode 100644 index 000000000..aa77bdacb --- /dev/null +++ b/Projects/Server/Serialization/DeltaDateTimeAttribute.cs @@ -0,0 +1,27 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: DeltaDateTimeAttribute.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; + +namespace Server +{ + /// + /// Hints to the source generator that a serializable DateTime field or property is for delta time (duration) + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public class DeltaDateTimeAttribute : Attribute + { + } +} diff --git a/Projects/Server/Serialization/GenericPersistence.cs b/Projects/Server/Serialization/GenericPersistence.cs index 2c75194a8..2912664b7 100644 --- a/Projects/Server/Serialization/GenericPersistence.cs +++ b/Projects/Server/Serialization/GenericPersistence.cs @@ -53,6 +53,7 @@ namespace Server void Deserialize(string savePath) { var path = Path.Combine(savePath, name); + AssemblyHandler.EnsureDirectory(path); string binPath = Path.Combine(path, $"{name}.bin"); diff --git a/Projects/Server/Serialization/ISerializable.cs b/Projects/Server/Serialization/ISerializable.cs index 665159386..274735f65 100644 --- a/Projects/Server/Serialization/ISerializable.cs +++ b/Projects/Server/Serialization/ISerializable.cs @@ -13,12 +13,14 @@ * along with this program. If not, see . * *************************************************************************/ +using System; using System.IO; namespace Server { public interface ISerializable { + long SavePosition { get; protected set; } BufferWriter SaveBuffer { get; protected internal set; } int TypeRef { get; } Serial Serial { get; } @@ -27,6 +29,13 @@ namespace Server void Delete(); bool Deleted { get; } + void MarkDirty() + { + SavePosition = -1; + } + + void SetTypeRef(Type type); + public void InitializeSaveBuffer(byte[] buffer) { SaveBuffer = new BufferWriter(buffer, true); @@ -35,8 +44,21 @@ namespace Server public void Serialize() { SaveBuffer ??= new BufferWriter(true); + + // Clean, don't bother serializing + if (SavePosition > -1) + { + SaveBuffer.Seek(SavePosition, SeekOrigin.Begin); + return; + } + SaveBuffer.Seek(0, SeekOrigin.Begin); Serialize(SaveBuffer); + + if (World.DirtyTrackingEnabled) + { + SavePosition = SaveBuffer.Position; + } } } } diff --git a/Projects/Server/Serialization/SerializableAttribute.cs b/Projects/Server/Serialization/SerializableAttribute.cs new file mode 100755 index 000000000..a1b1654f7 --- /dev/null +++ b/Projects/Server/Serialization/SerializableAttribute.cs @@ -0,0 +1,27 @@ +/************************************************************************* + * ModernUO * + * Copyright (C) 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SerializableEntityAttribute.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; + +namespace Server +{ + [AttributeUsage(AttributeTargets.Class)] + public sealed class SerializableAttribute : Attribute + { + public int Version { get; } + + public SerializableAttribute(int version) => Version = version; + } +} diff --git a/Projects/Server/Serialization/SerializableFieldAttribute.cs b/Projects/Server/Serialization/SerializableFieldAttribute.cs new file mode 100755 index 000000000..aeb7861ae --- /dev/null +++ b/Projects/Server/Serialization/SerializableFieldAttribute.cs @@ -0,0 +1,27 @@ +/************************************************************************* + * ModernUO * + * Copyright (C) 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SerializableFieldAttribute.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; + +namespace Server +{ + [AttributeUsage(AttributeTargets.Field)] + public sealed class SerializableFieldAttribute : Attribute + { + public int Order { get; } + + public SerializableFieldAttribute(int order) => Order = order; + } +} diff --git a/Projects/Server/Serialization/SerializableFieldAttributeAttribute.cs b/Projects/Server/Serialization/SerializableFieldAttributeAttribute.cs new file mode 100644 index 000000000..6e6142a82 --- /dev/null +++ b/Projects/Server/Serialization/SerializableFieldAttributeAttribute.cs @@ -0,0 +1,40 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SerializableFieldAttributeAttribute.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; + +namespace Server +{ + [AttributeUsage(AttributeTargets.Field)] + public sealed class SerializableFieldAttrAttribute : Attribute + { + public string AttributeString { get; } + public Type AttributeType { get; } + public object[] Arguments { get; } + + public SerializableFieldAttrAttribute(string attrString) => AttributeString = attrString; + + public SerializableFieldAttrAttribute(Type type, params object[] args) + { + if (typeof(Attribute).IsAssignableFrom(type)) + { + throw new ArgumentException($"Argument {nameof(type)} must be an attribute."); + } + + AttributeType = type; + Arguments = args; + } + } +} diff --git a/Projects/Server/Serialization/SerializablePropertyAttribute.cs b/Projects/Server/Serialization/SerializablePropertyAttribute.cs new file mode 100644 index 000000000..cbac334f6 --- /dev/null +++ b/Projects/Server/Serialization/SerializablePropertyAttribute.cs @@ -0,0 +1,30 @@ +/************************************************************************* + * ModernUO * + * Copyright (C) 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SerializablePropertyAttribute.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; + +namespace Server +{ + /// + /// Marks a property as serializable. Requires a call to ISerializable.MarkDirty() + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class SerializablePropertyAttribute : Attribute + { + public int Order { get; } + + public SerializablePropertyAttribute(int order) => Order = order; + } +} diff --git a/Projects/Server/Server.csproj b/Projects/Server/Server.csproj old mode 100644 new mode 100755 index 52f5d6936..289e87dde --- a/Projects/Server/Server.csproj +++ b/Projects/Server/Server.csproj @@ -10,6 +10,8 @@ ..\..\Distribution ..\..\Distribution 0.0.0 + true + Generated @@ -30,6 +32,7 @@ + @@ -38,4 +41,32 @@ + + + TargetFramework=netstandard2.0 + Analyzer + false + all + + + + + + + + + + + + + + + + + + + + + .\Migrations\ + diff --git a/Projects/Server/World/World.cs b/Projects/Server/World/World.cs index 2fcfd2c79..f4bb661fa 100644 --- a/Projects/Server/World/World.cs +++ b/Projects/Server/World/World.cs @@ -48,6 +48,7 @@ namespace Server private static string _tempSavePath; // Path to the temporary folder for the save private static string _savePath; // Path to "Saves" folder + public const bool DirtyTrackingEnabled = false; public const uint ItemOffset = 0x40000000; public const uint MaxItemSerial = 0x7FFFFFFF; public const uint MaxMobileSerial = ItemOffset - 1; diff --git a/Projects/UOContent/Accounting/Account.cs b/Projects/UOContent/Accounting/Account.cs index a413ed3af..ff3b803db 100644 --- a/Projects/UOContent/Accounting/Account.cs +++ b/Projects/UOContent/Accounting/Account.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.IO; using System.Net; +using System.Runtime.CompilerServices; using System.Xml; using Server.Accounting.Security; using Server.Misc; @@ -10,7 +12,7 @@ using Server.Network; namespace Server.Accounting { - public class Account : IAccount, IComparable + public partial class Account : IAccount, IComparable { public static readonly TimeSpan YoungDuration = TimeSpan.FromHours(40.0); public static readonly TimeSpan InactiveDuration = TimeSpan.FromDays(180.0); @@ -23,7 +25,6 @@ namespace Server.Accounting private List m_Tags; private TimeSpan m_TotalGameTime; private Timer m_YoungTimer; - private BufferWriter _saveBuffer; public Account(string username, string password) : this(Accounts.NewAccount) { @@ -48,28 +49,13 @@ namespace Server.Accounting { Serial = serial; - var ourType = GetType(); - TypeRef = Accounts.Types.IndexOf(ourType); - - if (TypeRef == -1) - { - Accounts.Types.Add(ourType); - TypeRef = Accounts.Types.Count - 1; - } + SetTypeRef(GetType()); } public Account(XmlElement node) { Serial = Accounts.NewAccount; - - var ourType = GetType(); - TypeRef = Accounts.Types.IndexOf(ourType); - - if (TypeRef == -1) - { - Accounts.Types.Add(ourType); - TypeRef = Accounts.Types.Count - 1; - } + SetTypeRef(GetType()); Username = Utility.GetText(node["username"], "empty"); @@ -141,6 +127,17 @@ namespace Server.Accounting Accounts.Add(this); } + public void SetTypeRef(Type type) + { + TypeRef = Accounts.Types.IndexOf(type); + + if (TypeRef == -1) + { + Accounts.Types.Add(type); + TypeRef = Accounts.Types.Count - 1; + } + } + /// /// Object detailing information about the hardware of the last person to log into this account /// @@ -270,11 +267,9 @@ namespace Server.Accounting } } - BufferWriter ISerializable.SaveBuffer - { - get => _saveBuffer; - set => _saveBuffer = value; - } + long ISerializable.SavePosition { get; set; } + + BufferWriter ISerializable.SaveBuffer { get; set; } public int TypeRef { get; private set; } diff --git a/Projects/UOContent/Engines/Factions/Core/Election.cs b/Projects/UOContent/Engines/Factions/Core/Election.cs index 251085a69..12927be3a 100644 --- a/Projects/UOContent/Engines/Factions/Core/Election.cs +++ b/Projects/UOContent/Engines/Factions/Core/Election.cs @@ -275,7 +275,6 @@ namespace Server.Factions if (Faction.Election != this) { m_Timer?.Stop(); - m_Timer = null; return; @@ -447,14 +446,7 @@ namespace Server.Factions gameTime = mobile.GameTime; } - var kp = 0; - - var pl = PlayerState.Find(From); - - if (pl != null) - { - kp = pl.KillPoints; - } + var kp = PlayerState.Find(From)?.KillPoints ?? 0; var sk = From.Skills.Total; diff --git a/Projects/UOContent/UOContent.csproj b/Projects/UOContent/UOContent.csproj index b7a0c86cb..6bf1e220d 100644 --- a/Projects/UOContent/UOContent.csproj +++ b/Projects/UOContent/UOContent.csproj @@ -6,6 +6,8 @@ ModernUO Content ..\..\Distribution\Assemblies ..\..\Distribution\Assemblies + true + Generated @@ -26,6 +28,7 @@ + @@ -36,4 +39,24 @@ + + + + + + + + + + + + + + + + + + + .\Migrations\ + diff --git a/azure-pipelines.yml b/azure-pipelines.yml index df3397068..f6be18dd7 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -18,7 +18,7 @@ jobs: displayName: 'Install .NET 5' inputs: packageType: sdk - version: 5.0.202 + version: 5.0.203 - task: NuGetAuthenticate@0 - script: ./publish.cmd Release win displayName: 'Build' @@ -62,7 +62,7 @@ jobs: displayName: 'Install .NET 5' inputs: packageType: sdk - version: 5.0.202 + version: 5.0.203 - task: NuGetAuthenticate@0 - script: ./publish.cmd Release $(os) displayName: 'Build'