From cab8fa82a26a101389af9521b5093f5a3f43117f Mon Sep 17 00:00:00 2001 From: clodanSPT Date: Mon, 2 Jun 2025 12:15:44 +0100 Subject: [PATCH] Json extension data fody (#340) * Added a new Fody plugin to add to every model class the JsonExtensionData attribute * retargeted fody plugin to netstandard for msbuild runtime * Fixed runtime issue * Fixed property check for new extension data properties --------- Co-authored-by: Alex --- .../AssemblyProcessor.cs | 30 +++++++++ .../JsonExtensionData.Fody/CecilExtensions.cs | 36 ++++++++++ .../JsonExtensionData.Fody/ConfigReader.cs | 34 ++++++++++ .../JsonExtensionData.Fody/Extensions.cs | 10 +++ .../JsonExtensionData.Fody.csproj | 15 +++++ .../JsonExtensionData.Fody.xcf | 11 +++ .../JsonExtensionData.Fody/ModuleWeaver.cs | 24 +++++++ .../JsonExtensionData.Fody/TypeProcessor.cs | 67 +++++++++++++++++++ .../Controllers/LauncherController.cs | 4 +- .../Controllers/LauncherV2Controller.cs | 4 +- .../SPTarkov.Server.Core/FodyWeavers.xml | 3 + .../SPTarkov.Server.Core/FodyWeavers.xsd | 9 +++ .../Models/Eft/Common/Tables/BotBase.cs | 14 ---- .../Models/Eft/Common/Tables/Item.cs | 16 ----- .../Models/Eft/Common/Tables/TemplateItem.cs | 3 - .../Models/Spt/Config/BotConfig.cs | 7 -- .../SPTarkov.Server.Core.csproj | 6 +- server-csharp.sln | 16 +++++ 18 files changed, 262 insertions(+), 47 deletions(-) create mode 100644 Libraries/FodyWeavers/JsonExtensionData.Fody/AssemblyProcessor.cs create mode 100644 Libraries/FodyWeavers/JsonExtensionData.Fody/CecilExtensions.cs create mode 100644 Libraries/FodyWeavers/JsonExtensionData.Fody/ConfigReader.cs create mode 100644 Libraries/FodyWeavers/JsonExtensionData.Fody/Extensions.cs create mode 100644 Libraries/FodyWeavers/JsonExtensionData.Fody/JsonExtensionData.Fody.csproj create mode 100644 Libraries/FodyWeavers/JsonExtensionData.Fody/JsonExtensionData.Fody.xcf create mode 100644 Libraries/FodyWeavers/JsonExtensionData.Fody/ModuleWeaver.cs create mode 100644 Libraries/FodyWeavers/JsonExtensionData.Fody/TypeProcessor.cs diff --git a/Libraries/FodyWeavers/JsonExtensionData.Fody/AssemblyProcessor.cs b/Libraries/FodyWeavers/JsonExtensionData.Fody/AssemblyProcessor.cs new file mode 100644 index 00000000..0466eb1f --- /dev/null +++ b/Libraries/FodyWeavers/JsonExtensionData.Fody/AssemblyProcessor.cs @@ -0,0 +1,30 @@ +using System.Linq; +using Mono.Cecil; + +namespace JsonExtensionData.Fody; + +public partial class ModuleWeaver +{ + public void ProcessAssembly() + { + foreach (var type in allClasses) + { + if (!ShouldInclude(type)) + { + continue; + } + + if (ShouldIncludeType(type)) + { + ProcessType(type); + } + } + } + + public bool ShouldIncludeType(TypeDefinition type) + { + return IncludeNamespacesRegex.Any(r => r.IsMatch(type.Namespace)); + } + + static bool ShouldInclude(TypeDefinition type) => !type.IsSealed; +} diff --git a/Libraries/FodyWeavers/JsonExtensionData.Fody/CecilExtensions.cs b/Libraries/FodyWeavers/JsonExtensionData.Fody/CecilExtensions.cs new file mode 100644 index 00000000..f7245f36 --- /dev/null +++ b/Libraries/FodyWeavers/JsonExtensionData.Fody/CecilExtensions.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Linq; +using Mono.Cecil; + +namespace JsonExtensionData.Fody; + +public static class CecilExtensions +{ + public static List GetAllClasses(this ModuleDefinition moduleDefinition) + { + var definitions = new List(); + //First is always module so we will skip that; + GetTypes(moduleDefinition.Types.Skip(1), definitions); + return definitions; + } + + static void GetTypes(IEnumerable typeDefinitions, List definitions) + { + foreach (var typeDefinition in typeDefinitions) + { + GetTypes(typeDefinition.NestedTypes, definitions); + + if (typeDefinition.IsInterface) + { + continue; + } + + if (typeDefinition.IsEnum) + { + continue; + } + + definitions.Add(typeDefinition); + } + } +} diff --git a/Libraries/FodyWeavers/JsonExtensionData.Fody/ConfigReader.cs b/Libraries/FodyWeavers/JsonExtensionData.Fody/ConfigReader.cs new file mode 100644 index 00000000..91462406 --- /dev/null +++ b/Libraries/FodyWeavers/JsonExtensionData.Fody/ConfigReader.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace JsonExtensionData.Fody; + +public partial class ModuleWeaver +{ + public List IncludeNamespacesRegex = new(); + + public void ReadConfig() + { + ReadExcludes(); + } + + void ReadExcludes() + { + var includeNamespacesElement = Config.Element("IncludeNamespacesRegex"); + if (includeNamespacesElement != null) + { + foreach (var item in includeNamespacesElement.Value + .Split( + [ + "\r\n", + "\n" + ], + StringSplitOptions.RemoveEmptyEntries) + .NonEmpty()) + { + IncludeNamespacesRegex.Add(new Regex(item)); + } + } + } +} diff --git a/Libraries/FodyWeavers/JsonExtensionData.Fody/Extensions.cs b/Libraries/FodyWeavers/JsonExtensionData.Fody/Extensions.cs new file mode 100644 index 00000000..883b2bbc --- /dev/null +++ b/Libraries/FodyWeavers/JsonExtensionData.Fody/Extensions.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Linq; + +namespace JsonExtensionData.Fody; + +public static class Extensions +{ + public static IEnumerable NonEmpty(this IEnumerable list) => + list.Select(_ => _.Trim()).Where(_ => _ != string.Empty); +} diff --git a/Libraries/FodyWeavers/JsonExtensionData.Fody/JsonExtensionData.Fody.csproj b/Libraries/FodyWeavers/JsonExtensionData.Fody/JsonExtensionData.Fody.csproj new file mode 100644 index 00000000..91463433 --- /dev/null +++ b/Libraries/FodyWeavers/JsonExtensionData.Fody/JsonExtensionData.Fody.csproj @@ -0,0 +1,15 @@ + + + + + + netstandard2.0 + + + + + diff --git a/Libraries/FodyWeavers/JsonExtensionData.Fody/JsonExtensionData.Fody.xcf b/Libraries/FodyWeavers/JsonExtensionData.Fody/JsonExtensionData.Fody.xcf new file mode 100644 index 00000000..6971a73b --- /dev/null +++ b/Libraries/FodyWeavers/JsonExtensionData.Fody/JsonExtensionData.Fody.xcf @@ -0,0 +1,11 @@ + + + + + Namespaces to use for adding JsonExtensionData properties and attribute + + + diff --git a/Libraries/FodyWeavers/JsonExtensionData.Fody/ModuleWeaver.cs b/Libraries/FodyWeavers/JsonExtensionData.Fody/ModuleWeaver.cs new file mode 100644 index 00000000..c309f377 --- /dev/null +++ b/Libraries/FodyWeavers/JsonExtensionData.Fody/ModuleWeaver.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Fody; +using Mono.Cecil; + +namespace JsonExtensionData.Fody; + +public partial class ModuleWeaver : BaseModuleWeaver +{ + List allClasses; + + public override void Execute() + { + allClasses = ModuleDefinition.GetAllClasses(); + ReadConfig(); + ProcessAssembly(); + } + + public override IEnumerable GetAssembliesForScanning() + { + return []; + } + + public override bool ShouldCleanReference => true; +} diff --git a/Libraries/FodyWeavers/JsonExtensionData.Fody/TypeProcessor.cs b/Libraries/FodyWeavers/JsonExtensionData.Fody/TypeProcessor.cs new file mode 100644 index 00000000..28a0b52e --- /dev/null +++ b/Libraries/FodyWeavers/JsonExtensionData.Fody/TypeProcessor.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace JsonExtensionData.Fody; + +public partial class ModuleWeaver +{ + private TypeReference? _dictionaryStringObjectReference; + private MethodReference? _jsonExtensionDataAttributeReference; + private MethodReference? _jsonIgnoreAttributeReference; + public void ProcessType(TypeDefinition typeDefinition) + { + _dictionaryStringObjectReference ??= ModuleDefinition.ImportReference(typeof(Dictionary)); + if (_jsonExtensionDataAttributeReference is null) + { + var jsonConstructorReference = ModuleDefinition.AssemblyResolver + .Resolve(AssemblyNameReference.Parse("System.Text.Json")).MainModule + .GetType("System.Text.Json.Serialization.JsonExtensionDataAttribute").Methods + .First(m => m.IsConstructor && !m.HasParameters); + _jsonExtensionDataAttributeReference = ModuleDefinition.ImportReference(jsonConstructorReference); + } + if (_jsonIgnoreAttributeReference is null) + { + var jsonIgnoreConstructorReference = ModuleDefinition.AssemblyResolver + .Resolve(AssemblyNameReference.Parse("System.Text.Json")).MainModule + .GetType("System.Text.Json.Serialization.JsonIgnoreAttribute").Methods + .First(m => m.IsConstructor && !m.HasParameters); + _jsonIgnoreAttributeReference = ModuleDefinition.ImportReference(jsonIgnoreConstructorReference); + } + var propertyDefinition = new PropertyDefinition("ExtensionData", PropertyAttributes.None, _dictionaryStringObjectReference); + propertyDefinition.CustomAttributes.Add(new CustomAttribute(_jsonExtensionDataAttributeReference)); + + // Add backing field + var field = new FieldDefinition("_extensionData", + FieldAttributes.Private, + _dictionaryStringObjectReference); + field.CustomAttributes.Add(new CustomAttribute(_jsonIgnoreAttributeReference)); + typeDefinition.Fields.Add(field); + + // Add getter + var get = new MethodDefinition("get_ExtensionData", + MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, + _dictionaryStringObjectReference); + get.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0)); + get.Body.Instructions.Add(Instruction.Create(OpCodes.Ldfld, field)); + get.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); + propertyDefinition.GetMethod = get; + typeDefinition.Methods.Add(get); + + // Add setter + var set = new MethodDefinition("set_ExtensionData", + MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, + ModuleDefinition.TypeSystem.Void); + set.Parameters.Add(new ParameterDefinition("value", ParameterAttributes.None, _dictionaryStringObjectReference)); + + set.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0)); + set.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_1)); + set.Body.Instructions.Add(Instruction.Create(OpCodes.Stfld, field)); + set.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); + + propertyDefinition.SetMethod = set; + typeDefinition.Methods.Add(set); + typeDefinition.Properties.Add(propertyDefinition); + } +} diff --git a/Libraries/SPTarkov.Server.Core/Controllers/LauncherController.cs b/Libraries/SPTarkov.Server.Core/Controllers/LauncherController.cs index 3e14e649..efaa2c73 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/LauncherController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/LauncherController.cs @@ -62,12 +62,12 @@ public class LauncherController( { var result = new Dictionary(); var dbProfiles = _databaseService.GetProfiles(); - foreach (var templatesProperty in typeof(ProfileTemplates).GetProperties().Where(p => p.CanWrite)) + foreach (var templatesProperty in typeof(ProfileTemplates).GetProperties().Where(p => p.CanWrite && p.Name != "ExtensionData")) { var propertyValue = templatesProperty.GetValue(dbProfiles); if (propertyValue == null) { - _logger.Warning(_localisationService.GetText("launcher-missing_property", templatesProperty)); + _logger.Warning(_localisationService.GetText("launcher-missing_property", templatesProperty.Name)); continue; } diff --git a/Libraries/SPTarkov.Server.Core/Controllers/LauncherV2Controller.cs b/Libraries/SPTarkov.Server.Core/Controllers/LauncherV2Controller.cs index 67f42dd4..a0612b21 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/LauncherV2Controller.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/LauncherV2Controller.cs @@ -48,12 +48,12 @@ public class LauncherV2Controller( var result = new Dictionary(); var dbProfiles = _databaseService.GetProfiles(); - foreach (var templatesProperty in typeof(ProfileTemplates).GetProperties().Where(p => p.CanWrite)) + foreach (var templatesProperty in typeof(ProfileTemplates).GetProperties().Where(p => p.CanWrite && p.Name != "ExtensionData")) { var propertyValue = templatesProperty.GetValue(dbProfiles); if (propertyValue == null) { - _logger.Warning(_localisationService.GetText("launcher-missing_property", templatesProperty)); + _logger.Warning(_localisationService.GetText("launcher-missing_property", templatesProperty.Name)); continue; } diff --git a/Libraries/SPTarkov.Server.Core/FodyWeavers.xml b/Libraries/SPTarkov.Server.Core/FodyWeavers.xml index c6787ffe..e389f2f3 100644 --- a/Libraries/SPTarkov.Server.Core/FodyWeavers.xml +++ b/Libraries/SPTarkov.Server.Core/FodyWeavers.xml @@ -1,4 +1,7 @@  + + SPTarkov\.Server\.Core\.Models.* + diff --git a/Libraries/SPTarkov.Server.Core/FodyWeavers.xsd b/Libraries/SPTarkov.Server.Core/FodyWeavers.xsd index 0eeb31cf..7cba32d5 100644 --- a/Libraries/SPTarkov.Server.Core/FodyWeavers.xsd +++ b/Libraries/SPTarkov.Server.Core/FodyWeavers.xsd @@ -5,6 +5,15 @@ + + + + + Namespaces to use for adding JsonExtensionData properties and attribute + + + + diff --git a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/BotBase.cs b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/BotBase.cs index d54dc6c7..c839acac 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/BotBase.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/BotBase.cs @@ -1217,13 +1217,6 @@ public record Victim get; set; } - - [JsonExtensionData] - public Dictionary OtherProperties - { - get; - set; - } } public record SessionCounters @@ -1335,13 +1328,6 @@ public record Aggressor get; set; } - - [JsonExtensionData] - public Dictionary OtherProperties - { - get; - set; - } } public record DamageHistory diff --git a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/Item.cs b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/Item.cs index afdf5836..c53667df 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/Item.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/Item.cs @@ -88,14 +88,6 @@ public record Item get; set; } -#if !DEBUG - [JsonExtensionData] - public Dictionary? ExtensionData - { - get; - set; - } -#endif } public record HideoutItem @@ -384,14 +376,6 @@ public record Upd get; set; } -#if !DEBUG - [JsonExtensionData] - public Dictionary? ExtensionData - { - get; - set; - } -#endif } public record LockableKeyComponent diff --git a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/TemplateItem.cs b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/TemplateItem.cs index d8937327..6320e85c 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/TemplateItem.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/TemplateItem.cs @@ -3517,9 +3517,6 @@ public record Props get; set; } - - //[JsonExtensionData] - //public Dictionary OtherProperties { get; set; } } public record WeaponRecoilSettings diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BotConfig.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BotConfig.cs index 217fa40d..4a360677 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BotConfig.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BotConfig.cs @@ -547,13 +547,6 @@ public record PresetBatch get; set; } - - [JsonExtensionData] - public IDictionary AdditionalData - { - get; - set; - } } public record WalletLootSettings diff --git a/Libraries/SPTarkov.Server.Core/SPTarkov.Server.Core.csproj b/Libraries/SPTarkov.Server.Core/SPTarkov.Server.Core.csproj index 6ede0af3..a8775ae3 100644 --- a/Libraries/SPTarkov.Server.Core/SPTarkov.Server.Core.csproj +++ b/Libraries/SPTarkov.Server.Core/SPTarkov.Server.Core.csproj @@ -22,18 +22,18 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive - + all + diff --git a/server-csharp.sln b/server-csharp.sln index 06d36fce..6475a611 100644 --- a/server-csharp.sln +++ b/server-csharp.sln @@ -6,6 +6,9 @@ MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SPTarkov.Server", "SPTarkov.Server\SPTarkov.Server.csproj", "{1F5ED9C6-8B1F-4776-85AB-B387CBBC5557}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SPTarkov.Server.Core", "Libraries\SPTarkov.Server.Core\SPTarkov.Server.Core.csproj", "{AC8643DC-8779-4B4A-BBDA-2D4CC466F765}" + ProjectSection(ProjectDependencies) = postProject + {905FBA04-D73A-4A46-930B-1B0C3A7C4EB8} = {905FBA04-D73A-4A46-930B-1B0C3A7C4EB8} + EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{6C0681F9-4013-4579-82DA-0A9297984FD3}" EndProject @@ -27,6 +30,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "Benchmarks\Be EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SPTarkov.Reflection", "Libraries\SPTarkov.Reflection\SPTarkov.Reflection.csproj", "{9073A593-A2F5-471E-9678-B896A7226FD4}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Fody", "Fody", "{61D0AD50-5B86-41E6-8B19-F11944AC3EE4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "JsonExtensionData", "JsonExtensionData", "{2938742B-34FA-47F2-A50B-E0470FB1D807}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonExtensionData.Fody", "Libraries\FodyWeavers\JsonExtensionData.Fody\JsonExtensionData.Fody.csproj", "{905FBA04-D73A-4A46-930B-1B0C3A7C4EB8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -73,6 +82,10 @@ Global {9073A593-A2F5-471E-9678-B896A7226FD4}.Debug|Any CPU.Build.0 = Debug|Any CPU {9073A593-A2F5-471E-9678-B896A7226FD4}.Release|Any CPU.ActiveCfg = Release|Any CPU {9073A593-A2F5-471E-9678-B896A7226FD4}.Release|Any CPU.Build.0 = Release|Any CPU + {905FBA04-D73A-4A46-930B-1B0C3A7C4EB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {905FBA04-D73A-4A46-930B-1B0C3A7C4EB8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {905FBA04-D73A-4A46-930B-1B0C3A7C4EB8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {905FBA04-D73A-4A46-930B-1B0C3A7C4EB8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -85,5 +98,8 @@ Global {4B973AC0-0C60-4853-9AF7-7CB69127473E} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF} {C24B1FEB-F8AC-434E-998D-5DA4D1687295} = {587959C2-5AFA-4B77-B327-566610F9A289} {9073A593-A2F5-471E-9678-B896A7226FD4} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF} + {61D0AD50-5B86-41E6-8B19-F11944AC3EE4} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF} + {2938742B-34FA-47F2-A50B-E0470FB1D807} = {61D0AD50-5B86-41E6-8B19-F11944AC3EE4} + {905FBA04-D73A-4A46-930B-1B0C3A7C4EB8} = {2938742B-34FA-47F2-A50B-E0470FB1D807} EndGlobalSection EndGlobal