diff --git a/Libraries/SPTarkov.Reflection/Patching/AbstractPrepatch.cs b/Libraries/SPTarkov.Reflection/Patching/AbstractPrepatch.cs deleted file mode 100644 index ff444633a..000000000 --- a/Libraries/SPTarkov.Reflection/Patching/AbstractPrepatch.cs +++ /dev/null @@ -1,102 +0,0 @@ -using Mono.Cecil; -using Mono.Cecil.Rocks; - -namespace SPTarkov.Reflection.Patching; - -public abstract class AbstractPrepatch(ModuleDefinition serverCoreModule) : IPrepatch -{ - /// - /// - /// - public abstract string ModGuid { get; } - - /// - /// - /// - public abstract bool IsActive { get; } - - /// - /// - /// - public abstract void Patch(); - - /// - /// Gets a type definition from the server core module - /// - /// Type to get the typedef for - /// The typedef for the provided runtime type - /// Thrown if the type does not exist in the module definition - protected TypeDefinition GetTypeDefinition() - { - var result = serverCoreModule.GetAllTypes().FirstOrDefault(t => t.FullName == typeof(T).FullName); - return result ?? throw new PatchException($"Could not a TypeDefinition for type: {typeof(T).FullName}"); - } - - /// - /// Gets a field definition from the server core module - /// - /// Name of the field - /// Declaring type of the field - /// The field definition for the provided type and name - /// Thrown if the field does not exist in the type definition - protected FieldDefinition GetField(string name) - { - var typeDef = GetTypeDefinition(); - return typeDef.Fields.FirstOrDefault(f => f.Name == name) - ?? throw new PatchException($"Could not locate a FieldDefinition for type: `{typeof(T).FullName}` and name `{name}`"); - } - - /// - /// Gets a property definition from the server core module - /// - /// Name of the property - /// Declaring type of the property - /// The property definition for the provided type and name - /// Thrown if the property does not exist in the type definition - protected PropertyDefinition GetProperty(string name) - { - var typeDef = GetTypeDefinition(); - return typeDef.Properties.FirstOrDefault(p => p.Name == name) - ?? throw new PatchException($"Could not locate a PropertyDefinition for type: `{typeof(T).FullName}` and name `{name}`"); - } - - /// - /// Gets a method definition from the server core module - /// - /// Name of the method - /// Declaring type of the method - /// The method definition for the provided type and name - /// Thrown if the method does not exist in the type definition - protected MethodDefinition GetMethod(string name) - { - var typeDef = GetTypeDefinition(); - return typeDef.Methods.FirstOrDefault(m => m.Name == name) - ?? throw new PatchException($"Could not locate a MethodDefinition for type: `{typeof(T).FullName}` and name `{name}`"); - } - - /// - /// Adds a new constant to an enum - /// - /// Name of the constant field, must be unique - /// Number of the constant, must be unique - /// Type of the enum to add the constant to - protected void AddNewEnumConstant(string name, int constant) - where T : Enum - { - var typeDef = GetTypeDefinition(); - - var newEnum = new FieldDefinition( - name, - Mono.Cecil.FieldAttributes.Public - | Mono.Cecil.FieldAttributes.Static - | Mono.Cecil.FieldAttributes.Literal - | Mono.Cecil.FieldAttributes.HasDefault, - typeDef - ) - { - Constant = constant, - }; - - typeDef.Fields.Add(newEnum); - } -} diff --git a/Libraries/SPTarkov.Reflection/Patching/IPrepatch.cs b/Libraries/SPTarkov.Reflection/Patching/IPrepatch.cs deleted file mode 100644 index 82805c76d..000000000 --- a/Libraries/SPTarkov.Reflection/Patching/IPrepatch.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace SPTarkov.Reflection.Patching; - -public interface IPrepatch -{ - /// - /// Guid of the mod this prepatch belongs to - /// - public string ModGuid { get; } - - /// - /// Is this patch active? - /// - public bool IsActive { get; } - - /// - /// prepatch method called by the mod loader - /// - void Patch(); -} diff --git a/Libraries/SPTarkov.Server.Core/Callbacks/ModdedTraderCustomizationCallbacks.cs b/Libraries/SPTarkov.Server.Core/Callbacks/ModLoaderCallbacks.cs similarity index 60% rename from Libraries/SPTarkov.Server.Core/Callbacks/ModdedTraderCustomizationCallbacks.cs rename to Libraries/SPTarkov.Server.Core/Callbacks/ModLoaderCallbacks.cs index 682c2cb0b..e0fbe4dd8 100644 --- a/Libraries/SPTarkov.Server.Core/Callbacks/ModdedTraderCustomizationCallbacks.cs +++ b/Libraries/SPTarkov.Server.Core/Callbacks/ModLoaderCallbacks.cs @@ -2,16 +2,27 @@ using SPTarkov.Server.Core.Controllers; using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common; +using SPTarkov.Server.Core.Models.Spt.Mod; using SPTarkov.Server.Core.Utils; namespace SPTarkov.Server.Core.Callbacks; [Injectable] -public class ModdedTraderCustomizationCallbacks( +public class ModLoaderCallbacks( + ClientEnumDefinitions clientEnumDefinitions, ModdedTraderCustomizationController moddedTraderCustomizationController, HttpResponseUtil httpResponseUtil ) { + /// + /// Handle /singleplayer/customEnumEntries + /// + /// + public ValueTask GetCustomEnumEntries(string url, EmptyRequestData _, MongoId sessionID) + { + return new ValueTask(httpResponseUtil.NoBody(clientEnumDefinitions.Entries.Values.SelectMany(e => e))); + } + /// /// Handle /singleplayer/moddedTraders /// diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/EnumEntryDefinition.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/EnumEntryDefinition.cs new file mode 100644 index 000000000..cbab9e44e --- /dev/null +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/EnumEntryDefinition.cs @@ -0,0 +1,81 @@ +using System.Text.Json.Serialization; + +namespace SPTarkov.Server.Core.Models.Spt.Mod; + +/// +/// Provides enum prepatch data needed to apply custom enum constants +/// +public sealed record EnumEntryDefinition +{ + /// + /// Type of enum, full namespace must be provided. E.g. `EFT.EBuffId`

+ /// When targeting nested types they should be denoted with `+` E.g. `EFT.InventoryLogic.Weapon+EFireMode` + ///
+ [JsonPropertyName("enumType")] + public required string EnumType { get; set; } + + /// + /// The name to give your new constant. + /// This will be validated and pre-patcher will provide feedback if this name is already taken. + /// + [JsonPropertyName("constantName")] + public required string ConstantName { get; set; } + + /// + /// Value to give your new constant. + /// This will be validated and pre-patcher will provide feedback if this constant is already taken. + /// This value is a long, however it will be converted to the proper type on application. E.g. int, sbyte, etc + /// + [JsonPropertyName("constantValue")] + public required long ConstantValue { get; set; } + + /// + /// CLIENT ONLY

+ /// Denotes the string to give the `JsonEnumName` attribute constructor should one exist. + /// E.g. `EFT.EBuffId` + ///
+ [JsonPropertyName("jsonEnumName")] + public string? JsonEnumName { get; set; } +} + +public sealed class ClientEnumDefinitions +{ + public IReadOnlyDictionary> Entries + { + get { return _entries; } + } + + private readonly Dictionary> _entries = []; + + /// + /// Adds a singular definition + /// + /// Mod guid that is adding this definition + /// Definition to add + public void Add(string modGuid, EnumEntryDefinition definition) + { + if (!_entries.TryGetValue(modGuid, out var value)) + { + _entries[modGuid] = [definition]; + return; + } + + value.Add(definition); + } + + /// + /// Adds multiple definitions + /// + /// Mod guid that is adding these definitions + /// Definitions to add + public void AddRange(string modGuid, IEnumerable definitions) + { + if (!_entries.TryGetValue(modGuid, out var value)) + { + _entries[modGuid] = [.. definitions]; + return; + } + + value.AddRange(definitions); + } +} diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/IModMetadata.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/IModMetadata.cs index fe74dc9a1..28f8416d0 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/IModMetadata.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/IModMetadata.cs @@ -73,7 +73,8 @@ public interface IModMetadata Range SptVersion { get; init; } /// - /// Indicates whether this mod includes a pre-patcher. If you don't know what this means, leave it false. + /// Indicates whether this mod includes enum prepatch definitions. Definitions must be stored as one JSON array in + /// user/patchers/{ModGuid}. If you don't know what this means, leave it false. /// bool HasPrepatcher { get; init; } diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/SptMod.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/SptMod.cs index 73e390a98..92c0edcf2 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/SptMod.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/SptMod.cs @@ -13,7 +13,4 @@ public class SptMod [JsonPropertyName("assemblies")] public required IEnumerable Assemblies { get; init; } - - [JsonPropertyName("patcherAssembly")] - public Assembly? PatcherAssembly { get; set; } } diff --git a/Libraries/SPTarkov.Server.Core/Routers/Static/ModdedTraderCustomizationRouter.cs b/Libraries/SPTarkov.Server.Core/Routers/Static/ModLoaderRouter.cs similarity index 53% rename from Libraries/SPTarkov.Server.Core/Routers/Static/ModdedTraderCustomizationRouter.cs rename to Libraries/SPTarkov.Server.Core/Routers/Static/ModLoaderRouter.cs index 4349426f3..dd1e6b8e9 100644 --- a/Libraries/SPTarkov.Server.Core/Routers/Static/ModdedTraderCustomizationRouter.cs +++ b/Libraries/SPTarkov.Server.Core/Routers/Static/ModLoaderRouter.cs @@ -7,14 +7,19 @@ namespace SPTarkov.Server.Core.Routers.Static; [Injectable(TypePriority = OnLoadOrder.Routers)] -public class ModdedTraderCustomizationRouter(JsonUtil jsonUtil, ModdedTraderCustomizationCallbacks moddedTraderCustomizationCallbacks) +public class ModLoaderRouter(JsonUtil jsonUtil, ModLoaderCallbacks modLoaderCallbacks) : StaticRouter( jsonUtil, [ + new RouteAction( + "/singleplayer/customEnumEntries", + async (url, info, sessionID, output, cancellationToken) => + await modLoaderCallbacks.GetCustomEnumEntries(url, info, sessionID) + ), new RouteAction( "/singleplayer/moddedTraders", async (url, info, sessionID, output, cancellationToken) => - await moddedTraderCustomizationCallbacks.GetCustomizationTraders(url, info, sessionID) + await modLoaderCallbacks.GetCustomizationTraders(url, info, sessionID) ), ] ) { } diff --git a/SPTarkov.Server/Modding/EnumPatcher.cs b/SPTarkov.Server/Modding/EnumPatcher.cs new file mode 100644 index 000000000..c6f3c9626 --- /dev/null +++ b/SPTarkov.Server/Modding/EnumPatcher.cs @@ -0,0 +1,83 @@ +using Mono.Cecil; +using Mono.Cecil.Rocks; +using SPTarkov.Reflection.Patching; +using SPTarkov.Server.Core.Models.Spt.Mod; +using SPTarkov.Server.Exceptions; + +namespace SPTarkov.Server.Modding; + +internal static class EnumPatcher +{ + public static void Patch(ModuleDefinition module, IReadOnlyCollection entries) + { + foreach (var entry in entries) + { + ApplyEntry(module, entry); + } + } + + private static void ApplyEntry(ModuleDefinition module, EnumEntryDefinition entry) + { + if (string.IsNullOrWhiteSpace(entry.EnumType)) + { + throw new ModLoaderException("An enum prepatch entry has no enumType."); + } + + var cecilTypeName = entry.EnumType.Replace('+', '/'); + var enumType = module.GetAllTypes().FirstOrDefault(type => string.Equals(type.FullName, cecilTypeName, StringComparison.Ordinal)); + + if (enumType is null || !enumType.IsEnum) + { + throw new ModLoaderException($"Could not find enum type `{entry.EnumType}` in SPTarkov.Server.Core.dll."); + } + + if (enumType.Fields.Any(field => string.Equals(field.Name, entry.ConstantName, StringComparison.Ordinal))) + { + throw new ModLoaderException($"Enum `{entry.EnumType}` already contains an entry named `{entry.ConstantName}`."); + } + + if (enumType.Fields.Any(field => field.HasConstant && Convert.ToDecimal(field.Constant) == entry.ConstantValue)) + { + throw new ModLoaderException($"Enum `{entry.EnumType}` already contains the value {entry.ConstantValue}."); + } + + enumType.Fields.Add( + new FieldDefinition( + entry.ConstantName, + FieldAttributes.Public | FieldAttributes.Static | FieldAttributes.Literal | FieldAttributes.HasDefault, + enumType + ) + { + Constant = ConvertConstant(enumType, entry), + } + ); + } + + private static object ConvertConstant(TypeDefinition enumType, EnumEntryDefinition entry) + { + var underlyingType = enumType.Fields.First(field => field.Name == "value__").FieldType.MetadataType; + + try + { + return underlyingType switch + { + MetadataType.SByte => checked((sbyte)entry.ConstantValue), + MetadataType.Byte => checked((byte)entry.ConstantValue), + MetadataType.Int16 => checked((short)entry.ConstantValue), + MetadataType.UInt16 => checked((ushort)entry.ConstantValue), + MetadataType.Int32 => checked((int)entry.ConstantValue), + MetadataType.UInt32 => checked((uint)entry.ConstantValue), + MetadataType.Int64 => entry.ConstantValue, + MetadataType.UInt64 => checked((ulong)entry.ConstantValue), + _ => throw new ModLoaderException($"Enum `{entry.EnumType}` has an unsupported underlying type `{underlyingType}`."), + }; + } + catch (OverflowException exception) + { + throw new ModLoaderException( + $"Value {entry.ConstantValue} does not fit enum `{entry.EnumType}` underlying type `{underlyingType}`.", + exception + ); + } + } +} diff --git a/SPTarkov.Server/Modding/ModLoader.cs b/SPTarkov.Server/Modding/ModLoader.cs index 8f83700ae..788e6d454 100644 --- a/SPTarkov.Server/Modding/ModLoader.cs +++ b/SPTarkov.Server/Modding/ModLoader.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Runtime.Loader; +using System.Text.Json; using Mono.Cecil; using Mono.Cecil.Cil; using SPTarkov.Common.Models.Logging; @@ -12,7 +13,7 @@ namespace SPTarkov.Server.Modding; public sealed class ModLoader(ISptLogger logger, ModValidator modValidator) { private List _loadedMods = []; - private readonly List _prepatches = []; + private readonly List _enumPrepatches = []; private ModuleDefinition? _serverCoreModule; private MemoryStream? _serverCoreModuleStream; @@ -39,7 +40,7 @@ public async Task RunModLoader(string[] args) // Load all prepatches without loading metadata, preventing a stale copy of the patched assembly await LoadPrepatchesForPrepatchPass(); - if (_prepatches.Count > 0) + if (_enumPrepatches.Count > 0) { // Clean the console a bit ClearConsole(); @@ -85,7 +86,7 @@ private async Task LoadPrepatchesForPrepatchPass() foreach (var patcherDirectory in Directory.GetDirectories(PatcherPath)) { - LoadModPatchers(Path.GetFileName(patcherDirectory), patcherDirectory); + LoadEnumPrepatch(Path.GetFileName(patcherDirectory), patcherDirectory); } } @@ -144,7 +145,7 @@ private async Task LoadMods(bool isPrepatchedProcess) /// Patched assembly and it's symbols, or null if any prepatch failed. private async Task ApplyPrepatchesInMemory(IReadOnlyCollection? validRuntimeMods = null) { - var success = RunActivePrepatches(validRuntimeMods); + var success = RunEnumPrepatches(validRuntimeMods); try { @@ -190,10 +191,10 @@ private async Task LoadMods(bool isPrepatchedProcess) } /// - /// Runs every active, valid prepatch against the loaded Core module in a deterministic order. + /// Runs every valid enum prepatch against the loaded Core module in a deterministic order. /// /// True if all prepatches succeeded. - private bool RunActivePrepatches(IReadOnlyCollection? validRuntimeMods) + private bool RunEnumPrepatches(IReadOnlyCollection? validRuntimeMods) { if (_serverCoreModule is null) { @@ -201,18 +202,18 @@ private bool RunActivePrepatches(IReadOnlyCollection? validRuntimeMods) } var validModGuids = validRuntimeMods?.Select(mod => mod.ModMetadata.ModGuid).ToHashSet(StringComparer.OrdinalIgnoreCase); - var activePrepatches = _prepatches - .Where(prepatch => prepatch.IsActive && (validModGuids is null || validModGuids.Contains(prepatch.ModGuid))) + var activePrepatches = _enumPrepatches + .Where(prepatch => validModGuids is null || validModGuids.Contains(prepatch.ModGuid)) .OrderBy(prepatch => prepatch.ModGuid, StringComparer.OrdinalIgnoreCase) - .ThenBy(prepatch => prepatch.GetType().FullName, StringComparer.OrdinalIgnoreCase); + .ThenBy(prepatch => prepatch.DefinitionPath, StringComparer.OrdinalIgnoreCase); foreach (var prepatch in activePrepatches) { - logger.Info($"Applying prepatch: {prepatch.GetType().FullName}"); + logger.Info($"Applying enum prepatch definitions: {prepatch.DefinitionPath}"); var succeeded = false; try { - prepatch.Patch(); + EnumPatcher.Patch(_serverCoreModule, prepatch.Entries); succeeded = true; } catch (Exception e) @@ -304,7 +305,7 @@ private SptMod LoadMod(string path) if (result.ModMetadata.HasPrepatcher) { - LoadModPatchers(result.ModMetadata.ModGuid, Path.Combine(PatcherPath, result.ModMetadata.ModGuid), result); + LoadEnumPrepatch(result.ModMetadata.ModGuid, Path.Combine(PatcherPath, result.ModMetadata.ModGuid)); } return result; @@ -357,7 +358,7 @@ private IModMetadata LoadModMetadata(IEnumerable assemblies, string pa return result; } - private void LoadModPatchers(string modGuid, string path, SptMod? mod = null) + private void LoadEnumPrepatch(string modGuid, string path) { if (!Directory.Exists(path)) { @@ -366,32 +367,45 @@ private void LoadModPatchers(string modGuid, string path, SptMod? mod = null) ); } - var patcherPath = - Directory.GetFiles(path, "*.dll", SearchOption.TopDirectoryOnly).FirstOrDefault() - ?? throw new ModLoaderException( - $"Failed to locate a patcher for mod: `{modGuid}`. If you did not intend to ship a patcher. Disable `HasPatcher` in your IModMetadata implementation." + var definitionFiles = Directory + .EnumerateFiles(path, "*", SearchOption.TopDirectoryOnly) + .Where(file => string.Equals(Path.GetExtension(file), ".json", StringComparison.OrdinalIgnoreCase)) + .OrderBy(file => file, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + if (definitionFiles.Length == 0) + { + throw new ModLoaderException( + $"Failed to locate an enum prepatch JSON file for mod: `{modGuid}`. If you did not intend to ship a prepatcher, disable `HasPrepatcher` in your IModMetadata implementation." ); + } - // Load into the loader's own context so the patcher's AbstractPrepatch matches ours - var loadContext = AssemblyLoadContext.GetLoadContext(typeof(ModLoader).Assembly) ?? AssemblyLoadContext.Default; - var patcherAssembly = loadContext.LoadFromAssemblyPath(Path.GetFullPath(patcherPath)); - if (mod is not null) + if (definitionFiles.Length > 1) { - mod.PatcherAssembly = patcherAssembly; + throw new ModLoaderException( + $"Found multiple enum prepatch JSON files for mod: `{modGuid}`. Expected exactly one file in: `{Path.GetFullPath(path)}`" + ); } - var patcherTypes = mod?.PatcherAssembly.GetTypes() ?? patcherAssembly.GetTypes(); - var prepatchTypes = patcherTypes.Where(type => !type.IsAbstract && typeof(AbstractPrepatch).IsAssignableFrom(type)); + var definitionPath = definitionFiles[0]; + List? entries; - if (!prepatchTypes.Any()) + try + { + using var definitionStream = File.OpenRead(definitionPath); + entries = JsonSerializer.Deserialize>(definitionStream); + } + catch (Exception exception) when (exception is JsonException or NotSupportedException or IOException or UnauthorizedAccessException) { - throw new ModLoaderException($"Patcher at path: `{patcherPath}` has no patcher entry point(s) of type `AbstractPrepatch`"); + throw new ModLoaderException($"Failed to load enum prepatch definitions from: `{Path.GetFullPath(definitionPath)}`", exception); } - foreach (var prepatchType in prepatchTypes) + if (entries is null || entries.Count == 0) { - _prepatches.Add((AbstractPrepatch)Activator.CreateInstance(prepatchType, args: [_serverCoreModule])!); + throw new ModLoaderException($"Enum prepatch file contains no definitions: `{Path.GetFullPath(definitionPath)}`"); } + + _enumPrepatches.Add(new EnumPrepatch(modGuid, Path.GetFullPath(definitionPath), entries)); } private async Task TryLoadServerCoreBytes() @@ -431,3 +445,5 @@ private async Task TryLoadServerCoreBytes() public sealed record ModLoaderRunResult(bool ShouldStartServer, List ValidRuntimeMods); public sealed record PatchedCoreAssembly(byte[] Assembly, byte[]? Symbols); + +internal sealed record EnumPrepatch(string ModGuid, string DefinitionPath, IReadOnlyList Entries); diff --git a/SPTarkov.Server/Program.cs b/SPTarkov.Server/Program.cs index 6caa70994..f75eb46d0 100644 --- a/SPTarkov.Server/Program.cs +++ b/SPTarkov.Server/Program.cs @@ -218,6 +218,7 @@ await dbImporter.LoadDatabaseAsync(shouldVerify, cTSource.Token) if (ProgramStatics.MODS()) { diHandler.AddInjectableTypesFromAssemblies(loadedMods.SelectMany(a => a.Assemblies)); + builder.Services.AddSingleton(new ClientEnumDefinitions()); diHandler.AddInjectableTypesFromTypeAssembly(typeof(SPTStartupHostedService)); } else diff --git a/Testing/TestMod2/TestMod2.cs b/Testing/TestMod2/TestMod2.cs index 3baa617d1..9b20ba817 100644 --- a/Testing/TestMod2/TestMod2.cs +++ b/Testing/TestMod2/TestMod2.cs @@ -29,7 +29,7 @@ public sealed class TestMod2Metadata : IModMetadata, IModBlazorMetadata } [Injectable(TypePriority = OnLoadOrder.PostLoad + 1)] -public class TestMod2(ISptLogger logger) : IOnLoad +public class TestMod2(ISptLogger logger, ClientEnumDefinitions clientEnumDefinitions) : IOnLoad { private const string InjectedName = "TestSkillEntry"; @@ -49,6 +49,17 @@ public async Task OnLoadAsync(CancellationToken cancellationToken) logger.Warning($"Prepatch NOT applied: {InjectedName} missing from SkillTypes"); } + clientEnumDefinitions.Add( + "com.sp-tarkov.test-mod2", + new EnumEntryDefinition + { + EnumType = "EFT.EBuffId", + ConstantName = "NewSkill", + ConstantValue = 10000, + JsonEnumName = "NewSkill", + } + ); + await Task.CompletedTask; } } diff --git a/Testing/TestMod2/TestMod2.csproj b/Testing/TestMod2/TestMod2.csproj index 17e300738..df458ab59 100644 --- a/Testing/TestMod2/TestMod2.csproj +++ b/Testing/TestMod2/TestMod2.csproj @@ -15,16 +15,10 @@ - - - + @@ -33,6 +27,10 @@ SourceFiles="@(OutputDLL);" DestinationFolder="$(MSBuildProjectDirectory)\..\..\SPTarkov.Server\bin\$(Configuration)\net10.0\user\mods\TestMod2" /> + ("TestSkillEntry", 100); - - if (serverCoreModule.Assembly.CustomAttributes.Any(IsTestPrepatchMarker)) - { - return; - } - - var constructor = typeof(AssemblyMetadataAttribute).GetConstructor([typeof(string), typeof(string)]); - var metadataAttribute = new CustomAttribute(serverCoreModule.ImportReference(constructor)); - metadataAttribute.ConstructorArguments.Add(new CustomAttributeArgument(serverCoreModule.TypeSystem.String, MetadataKey)); - metadataAttribute.ConstructorArguments.Add(new CustomAttributeArgument(serverCoreModule.TypeSystem.String, MetadataValue)); - - serverCoreModule.Assembly.CustomAttributes.Add(metadataAttribute); - } - - private static bool IsTestPrepatchMarker(CustomAttribute attribute) - { - return attribute.AttributeType.FullName == typeof(AssemblyMetadataAttribute).FullName - && attribute.ConstructorArguments.Count == 2 - && string.Equals(attribute.ConstructorArguments[0].Value as string, MetadataKey, StringComparison.Ordinal); - } -} diff --git a/Testing/TestPrepatch/TestPrepatch.csproj b/Testing/TestPrepatch/TestPrepatch.csproj deleted file mode 100644 index 03b57435d..000000000 --- a/Testing/TestPrepatch/TestPrepatch.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - enable - TestPrepatch - Library - - - - - - - - - - - - diff --git a/server-csharp.slnx b/server-csharp.slnx index 1b6a93db8..bd7a0850e 100644 --- a/server-csharp.slnx +++ b/server-csharp.slnx @@ -14,10 +14,7 @@ - - - - +