diff --git a/.github/ISSUE_TEMPLATE/1-bug-report.yml b/.github/ISSUE_TEMPLATE/1-bug-report.yml new file mode 100644 index 00000000..a9c358fd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-bug-report.yml @@ -0,0 +1,101 @@ +name: "Bug Report" +description: "Report a bug in the SPT project." +labels: ["triage"] +body: + - type: markdown + attributes: + value: | + ## Thank you for taking the time to fill out a bug report! + + Please note the following requirements: + + - You must be able to replicate the issue with a fresh profile, running no mods. If you can't, we can't fix it. + - If you are using a profile from an older version, please make a fresh profile and replicate the issue before submitting. + - You must upload all the required log files, even if you think they are useless. + - Failure to comply with any of the above requirements will result in your issue being closed without notice. + - type: dropdown + id: version + attributes: + label: SPT Version + description: What version of SPT are you using? + options: + - "4.0" + validations: + required: true + - type: dropdown + id: projects + attributes: + label: "Project Type" + description: "If known, which part of the project is involved in this bug report?" + options: + - "Server" + - "Modules" + - "Launcher" + multiple: true + validations: + required: false + - type: textarea + id: result_expected + attributes: + label: "Expected Result" + description: "What you expect to happen?" + validations: + required: true + - type: textarea + id: result_actual + attributes: + label: "Actual Result" + description: "What actually happened?" + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: "Steps To Reproduce" + description: "Describe in point form the steps we can take to reproduce the issue on our end." + validations: + required: true + - type: textarea + id: log_server + attributes: + label: "Server Log" + description: "Upload a copy of your *entire* server log: `/user/logs/spt/spt.txt`." + placeholder: "Attach the log file. Do not paste the contents." + validations: + required: true + - type: textarea + id: log_bepinex + attributes: + label: "BepinEx Log" + description: "Upload a copy of your *entire* BepinEx log: `/BepinEx/LogOutput.log`." + placeholder: "Attach the log file. Do not paste the contents." + validations: + required: true + - type: textarea + id: log_client + attributes: + label: "Client Log" + description: "Upload a copy of your *entire* client log: `/Logs/log__/_ traces.log`." + placeholder: "Attach the log file. Do not paste the contents." + validations: + required: true + - type: textarea + id: profile + attributes: + label: "Player Profile" + description: "If helpful, upload a copy of your *entire* player profile: `/user/profiles/.json`." + placeholder: "Attach the profile file. Do not paste the contents." + validations: + required: false + - type: textarea + id: screenshots + attributes: + label: "Screenshots" + description: "If helpful, upload any screenshots or videos you think would help us identify the issue." + validations: + required: false + - type: markdown + attributes: + value: | + ## BEFORE YOU SUBMIT + Ensure that your logs are attached. **No logs = Issue deleted** diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..740c7c21 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Discord Community + url: https://discord.com/invite/Xn9msqQZan + about: Find community support through our Discord server. + - name: Github Discussions + url: https://github.com/orgs/sp-tarkov/discussions + about: Please duscuss the project on the Github Discussions board. diff --git a/.github/README.md b/.github/README.md deleted file mode 100644 index 0fd84114..00000000 --- a/.github/README.md +++ /dev/null @@ -1,90 +0,0 @@ -# Single Player Tarkov - Server Project - -This is the Server project for the Single Player Tarkov mod for Escape From Tarkov. It can be run locally to replicate responses to the modified Escape From Tarkov client - - -# Table of Contents - -- [Features](#features) -- [Installation](#installation) - - [Requirements](#requirements) - - [Initial Setup](#initial-setup) -- [Development](#development) - - [Commands](#commands) - - [Debugging](#debugging) - - [Mod Debugging](#mod-debugging) -- [Contributing](#contributing) - - [Branches](#branchs) - - [Pull Request Guidelines](#pull-request-guidelines) - - [Tests](#tests) -- [License](#license) - -## Features - -For a full list of features, please see [FEATURES.md](FEATURES.md) - -## Installation - -### Requirements - -This project has been built in [Visual Studio](https://visualstudio.microsoft.com/) (VS) and [Rider](https://www.jetbrains.com/rider/) using [.NET](https://dotnet.microsoft.com/en-us/) - -### Initial Setup - -To prepare the project for development you will need to: - -1. Run `git clone https://github.com/sp-tarkov/server-csharp.git server` to clone the repository -2. Run `git lfs pull` to download LFS files locally. -3. Open the `project/server-csharp.sln` file in Visual Studio or Rider -4. Run `Build > Build Solution (CTRL + SHIFT + B)` in the IDE - -## Development - -### Commands - -### Debugging - -To debug the project in Visual Studio Code: -1. Choose `Server` and `Spt Server Debug` in the debug dropdowns -2. Choose `Debug > Start Debugging (F5)` to run the server - -### Mod Debugging - -To debug a server mod in Visual Studio, you can copy the mod DLL into the `user/mods` folder and then start the server - -## Contributing - -We're really excited that you're interested in contributing! Before submitting your contribution, please consider the following: - -### Branches - -- **master** - The default branch used for the latest stable release. This branch is protected and typically is only merged with release branches. -- **development** - The main branch for server development. PRs should target this. - -### Pull Request Guidelines - -- **Keep Them Small** - If you're fixing a bug, try to keep the changes to the bug fix only. If you're adding a feature, try to keep the changes to the feature only. This will make it easier to review and merge your changes. -- **Perform a Self-Review** - Before submitting your changes, review your own code. This will help you catch any mistakes you may have made. -- **Remove Noise** - Remove any unnecessary changes to white space, code style formatting, or some text change that has no impact related to the intention of the PR. -- **Create a Meaningful Title** - When creating a PR, make sure the title is meaningful and describes the changes you've made. -- **Write Detailed Commit Messages** - Bring out your table manners, speak the Queen's English and be on your best behaviour. - -### Style Guide - - TODO: style guidance - Ensure that your code is automatically formatted whenever you save a file. - -### Tests - -We have a number of tests that are run automatically when you submit a pull request. You can run these tests locally by running The unit test sub-project. If you're adding a new feature or fixing a bug, please conceder adding tests to cover your changes so that we can ensure they don't break in the future. - -## License - -This project is licensed under the NCSA Open Source License. See the [LICENSE](LICENSE.md) file for details. diff --git a/.gitignore b/.gitignore index 5b96b1d6..43d1e66a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,7 @@ ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore -Server/user/ -SPTarkov.Server/user -SPTarkov.Server/Assets +Libraries/SPTarkov.Server.Assets/SPT_Data/checks.dat # User-specific files *.rsuser diff --git a/Benchmarks/Benchmarks.csproj b/Benchmarks/Benchmarks.csproj index 705c5e3c..948e2e4e 100644 --- a/Benchmarks/Benchmarks.csproj +++ b/Benchmarks/Benchmarks.csproj @@ -1,6 +1,6 @@ - + Exe @@ -8,11 +8,12 @@ - - - - - + + + + + + diff --git a/Benchmarks/ClonerBenchmarks.cs b/Benchmarks/ClonerBenchmarks.cs index 6f5a3f15..e5eb646b 100644 --- a/Benchmarks/ClonerBenchmarks.cs +++ b/Benchmarks/ClonerBenchmarks.cs @@ -3,6 +3,7 @@ using Benchmarks.Mock; using SPTarkov.Server.Core.Models.Spt.Templates; using SPTarkov.Server.Core.Utils; using SPTarkov.Server.Core.Utils.Cloners; +using SPTarkov.Server.Core.Utils.Json.Converters; namespace Benchmarks; @@ -19,7 +20,7 @@ public class ClonerBenchmarks [GlobalSetup] public void Setup() { - var jsonUtil = new JsonUtil(); + var jsonUtil = new JsonUtil([ new SptJsonConverterRegistrator() ]); var importer = new ImporterUtil(new MockLogger(), new FileUtil(), jsonUtil); var loadTask = importer.LoadRecursiveAsync("./Assets/database/templates/"); diff --git a/Ceciler/Ceciler.Interfaces.dll b/Ceciler/Ceciler.Interfaces.dll new file mode 100644 index 00000000..5689d7ee Binary files /dev/null and b/Ceciler/Ceciler.Interfaces.dll differ diff --git a/Ceciler/Ceciler.Launcher.deps.json b/Ceciler/Ceciler.Launcher.deps.json new file mode 100644 index 00000000..2226a9d0 --- /dev/null +++ b/Ceciler/Ceciler.Launcher.deps.json @@ -0,0 +1,67 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v9.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v9.0": { + "Ceciler.Launcher/1.0.0": { + "dependencies": { + "Ceciler.Interfaces": "1.0.0", + "Mono.Cecil": "0.11.6" + }, + "runtime": { + "Ceciler.Launcher.dll": {} + } + }, + "Mono.Cecil/0.11.6": { + "runtime": { + "lib/netstandard2.0/Mono.Cecil.Mdb.dll": { + "assemblyVersion": "0.11.6.0", + "fileVersion": "0.11.6.0" + }, + "lib/netstandard2.0/Mono.Cecil.Pdb.dll": { + "assemblyVersion": "0.11.6.0", + "fileVersion": "0.11.6.0" + }, + "lib/netstandard2.0/Mono.Cecil.Rocks.dll": { + "assemblyVersion": "0.11.6.0", + "fileVersion": "0.11.6.0" + }, + "lib/netstandard2.0/Mono.Cecil.dll": { + "assemblyVersion": "0.11.6.0", + "fileVersion": "0.11.6.0" + } + } + }, + "Ceciler.Interfaces/1.0.0": { + "runtime": { + "Ceciler.Interfaces.dll": { + "assemblyVersion": "1.0.0.0", + "fileVersion": "1.0.0.0" + } + } + } + } + }, + "libraries": { + "Ceciler.Launcher/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Mono.Cecil/0.11.6": { + "type": "package", + "serviceable": true, + "sha512": "sha512-f33RkDtZO8VlGXCtmQIviOtxgnUdym9xx/b1p9h91CRGOsJFxCFOFK1FDbVt1OCf1aWwYejUFa2MOQyFWTFjbA==", + "path": "mono.cecil/0.11.6", + "hashPath": "mono.cecil.0.11.6.nupkg.sha512" + }, + "Ceciler.Interfaces/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + } + } +} \ No newline at end of file diff --git a/Ceciler/Ceciler.Launcher.dll b/Ceciler/Ceciler.Launcher.dll new file mode 100644 index 00000000..dd457afa Binary files /dev/null and b/Ceciler/Ceciler.Launcher.dll differ diff --git a/Ceciler/Ceciler.Launcher.exe b/Ceciler/Ceciler.Launcher.exe new file mode 100644 index 00000000..9fe6d70d Binary files /dev/null and b/Ceciler/Ceciler.Launcher.exe differ diff --git a/Ceciler/Ceciler.Launcher.runtimeconfig.json b/Ceciler/Ceciler.Launcher.runtimeconfig.json new file mode 100644 index 00000000..233cae7c --- /dev/null +++ b/Ceciler/Ceciler.Launcher.runtimeconfig.json @@ -0,0 +1,13 @@ +{ + "runtimeOptions": { + "tfm": "net9.0", + "framework": { + "name": "Microsoft.NETCore.App", + "version": "9.0.0" + }, + "configProperties": { + "System.Reflection.Metadata.MetadataUpdater.IsSupported": false, + "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false + } + } +} \ No newline at end of file diff --git a/Ceciler/Mono.Cecil.Mdb.dll b/Ceciler/Mono.Cecil.Mdb.dll new file mode 100644 index 00000000..55f9ee25 Binary files /dev/null and b/Ceciler/Mono.Cecil.Mdb.dll differ diff --git a/Ceciler/Mono.Cecil.Pdb.dll b/Ceciler/Mono.Cecil.Pdb.dll new file mode 100644 index 00000000..0462a8f2 Binary files /dev/null and b/Ceciler/Mono.Cecil.Pdb.dll differ diff --git a/Ceciler/Mono.Cecil.Rocks.dll b/Ceciler/Mono.Cecil.Rocks.dll new file mode 100644 index 00000000..561b0d74 Binary files /dev/null and b/Ceciler/Mono.Cecil.Rocks.dll differ diff --git a/Ceciler/Mono.Cecil.dll b/Ceciler/Mono.Cecil.dll new file mode 100644 index 00000000..488bd5cb Binary files /dev/null and b/Ceciler/Mono.Cecil.dll differ diff --git a/Libraries/SPTarkov.Common/Extensions/ObjectExtensions.cs b/Libraries/SPTarkov.Common/Extensions/ObjectExtensions.cs index 8688d1ee..2c75f7cd 100644 --- a/Libraries/SPTarkov.Common/Extensions/ObjectExtensions.cs +++ b/Libraries/SPTarkov.Common/Extensions/ObjectExtensions.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Text.Json; +using System.Text.Json.Serialization; namespace SPTarkov.Common.Extensions; @@ -60,6 +61,20 @@ public static class ObjectExtensions foreach (var prop in list) { + // Edge case + if (Attribute.IsDefined(prop, typeof(JsonExtensionDataAttribute))) + { + if (prop.GetValue(obj) is not IDictionary kvp) + { + // Not a dictionary, skip iterating over its keys/values + continue; + } + + result.AddRange(kvp.Select(jsonExtensionKvP => jsonExtensionKvP.Value)); + + continue; + } + result.Add(prop.GetValue(obj)); } @@ -68,9 +83,37 @@ public static class ObjectExtensions public static Dictionary GetAllPropsAsDict(this object? obj) { - var props = obj.GetType().GetProperties(); + if (obj is null) + { + return []; + } - return props.ToDictionary(prop => prop.Name, prop => prop.GetValue(obj)); + var resultDict = new Dictionary(); + foreach (var prop in obj.GetType().GetProperties()) + { + // Edge case + if (Attribute.IsDefined(prop, typeof(JsonExtensionDataAttribute))) + { + if (prop.GetValue(obj) is not IDictionary kvp) + { + // Not a dictionary, skip iterating over its keys/values + continue; + } + + foreach (var jsonExtensionKvP in kvp) + { + // Add contents of prop into dictionary we return + resultDict.TryAdd(jsonExtensionKvP.Key, jsonExtensionKvP.Value); + } + + continue; + } + + // Normal prop + resultDict.Add(prop.Name, prop.GetValue(obj)); + } + + return resultDict; } public static T ToObject(this JsonElement element) diff --git a/Libraries/SPTarkov.Common/Annotations/Injectable.cs b/Libraries/SPTarkov.DI/Annotations/Injectable.cs similarity index 58% rename from Libraries/SPTarkov.Common/Annotations/Injectable.cs rename to Libraries/SPTarkov.DI/Annotations/Injectable.cs index 11bddf86..111991bc 100644 --- a/Libraries/SPTarkov.Common/Annotations/Injectable.cs +++ b/Libraries/SPTarkov.DI/Annotations/Injectable.cs @@ -1,7 +1,7 @@ -namespace SPTarkov.Common.Annotations; +namespace SPTarkov.DI.Annotations; -[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] -public class Injectable(InjectionType injectionType = InjectionType.Scoped, Type? type = null, int typePriority = int.MaxValue) : Attribute +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public class Injectable(InjectionType injectionType = InjectionType.Scoped, Type? typeOverride = null, int typePriority = int.MaxValue) : Attribute { public InjectionType InjectionType { @@ -9,17 +9,17 @@ public class Injectable(InjectionType injectionType = InjectionType.Scoped, Type set; } = injectionType; - public Type? InjectableTypeOverride - { - get; - set; - } = type; - public int TypePriority { get; set; } = typePriority; + + public Type? TypeOverride + { + get; + set; + } = typeOverride; } public enum InjectionType diff --git a/Libraries/SPTarkov.DI/DependencyInjectionHandler.cs b/Libraries/SPTarkov.DI/DependencyInjectionHandler.cs new file mode 100644 index 00000000..a7dddc46 --- /dev/null +++ b/Libraries/SPTarkov.DI/DependencyInjectionHandler.cs @@ -0,0 +1,234 @@ +using System.Reflection; +using SPTarkov.DI.Annotations; + +namespace SPTarkov.DI; + +public class DependencyInjectionHandler +{ + private static List? _allLoadedTypes; + private static List? _allConstructors; + + private readonly Dictionary _injectedTypeNames = new(); + private readonly IServiceCollection _serviceCollection; + + private readonly Dictionary _injectedValues = new(); + private readonly Lock _injectedValuesLock = new(); + + private bool _oneTimeUseFlag; + + + public DependencyInjectionHandler(IServiceCollection serviceCollection) + { + _serviceCollection = serviceCollection; + } + + public void AddInjectableTypesFromAssembly(Assembly assembly) + { + AddInjectableTypesFromTypeList(assembly.GetTypes()); + } + + public void AddInjectableTypesFromAssemblies(IEnumerable assemblies) + { + foreach (var assembly in assemblies) + { + AddInjectableTypesFromAssembly(assembly); + } + } + + public void AddInjectableTypesFromTypeAssembly(Type type) + { + AddInjectableTypesFromAssembly(type.Assembly); + } + + public void AddInjectableTypesFromTypeList(IEnumerable types) + { + var typesToInject = types.Where(type => + Attribute.IsDefined(type, typeof(Injectable)) && + !_injectedTypeNames.ContainsKey($"{type.Namespace}.{type.Name}")); + if (typesToInject.Any()) + { + foreach (var type in typesToInject) + { + _injectedTypeNames.Add($"{type.Namespace}.{type.Name}", type); + } + } + } + + public void InjectAll() + { + if (_oneTimeUseFlag) + { + throw new Exception("Invalid usage of DependencyInjectionHandler, this is a one time use service!"); + } + _oneTimeUseFlag = true; + var typeRefValues = _injectedTypeNames.Values + .Select(t => new TypeRefContainer(((Injectable[]) Attribute.GetCustomAttributes(t, typeof(Injectable)))[0], t, t)); + // All the components that have a type override, we need to find them and remove them before injecting everything + var componentsToRemove = typeRefValues.Where(tr => tr.InjectableAttribute.TypeOverride != null).Select(tr => + string.IsNullOrEmpty(tr.InjectableAttribute.TypeOverride!.FullName) + ? $"{tr.InjectableAttribute.TypeOverride.Namespace}.{tr.InjectableAttribute.TypeOverride.Name}" + : tr.InjectableAttribute.TypeOverride.FullName!) + .ToHashSet(); + // All the components without the removed overrides + var cleanedComponents = typeRefValues.Where(tr => + { + var name = string.IsNullOrEmpty(tr.Type.FullName) + ? $"{tr.Type.Namespace}.{tr.Type.Name}" + : tr.Type.FullName!; + return !componentsToRemove.Contains(name); + }); + // All the components sorted and ready to be inserted into the DI container + var sortedInjectableTypes = cleanedComponents.OrderBy(tRef => tRef.InjectableAttribute.TypePriority); + + foreach (var typeRefToInject in sortedInjectableTypes) + { + var nodes = new Queue(); + nodes.Enqueue(typeRefToInject); + foreach (var implementedInterface in typeRefToInject.Type.GetInterfaces() + .Where(t => !t.Namespace.StartsWith("System"))) + { + nodes.Enqueue(new TypeRefContainer(typeRefToInject.InjectableAttribute, typeRefToInject.Type, + implementedInterface)); + } + + while (nodes.Any()) + { + var node = nodes.Dequeue(); + if (node.Type.BaseType != null && node.Type.BaseType != typeof(object)) + { + nodes.Enqueue(new TypeRefContainer(node.InjectableAttribute, typeRefToInject.Type, + node.Type.BaseType)); + } + + if (node.Type.IsGenericType) + { + RegisterGenericComponents(node); + } + else + { + RegisterComponent(node.InjectableAttribute.InjectionType, + node.Type, + node.ParentType); + } + } + } + } + + private void RegisterGenericComponents(TypeRefContainer typeRef) + { + try + { + _allLoadedTypes ??= AppDomain.CurrentDomain.GetAssemblies().SelectMany(t => t.GetTypes()).ToList(); + } + catch (ReflectionTypeLoadException ex) + { + Console.WriteLine($"COULD NOT LOAD TYPE: {ex}"); + } + + _allConstructors ??= _allLoadedTypes.SelectMany(t => t.GetConstructors()).ToList(); + + var typeName = $"{typeRef.Type.Namespace}.{typeRef.Type.Name}"; + try + { + var matchedConstructors = _allConstructors.Where(c => c.GetParameters() + .Any(p => p.ParameterType.IsGenericType && + p.ParameterType.GetGenericTypeDefinition().FullName == typeName + ) + ); + + var constructorInfos = matchedConstructors.ToList(); + if (constructorInfos.Count == 0) + { + return; + } + + foreach (var matchedConstructor in constructorInfos) + { + var constructorParams = matchedConstructor.GetParameters(); + foreach (var parameterInfo in constructorParams.Where(x => IsMatchingGenericType(x, typeName))) + { + var parameters = parameterInfo.ParameterType.GetGenericArguments(); + var typedGeneric = typeRef.ParentType.MakeGenericType(parameters); + RegisterComponent( + typeRef.InjectableAttribute.InjectionType, + parameterInfo.ParameterType, + typedGeneric + ); + } + } + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + private static bool IsMatchingGenericType(ParameterInfo paramInfo, string typeName) + { + return paramInfo.ParameterType.IsGenericType && + paramInfo.ParameterType.GetGenericTypeDefinition().FullName == typeName; + } + + private void RegisterComponent( + InjectionType injectionType, + Type registrableInterface, + Type implementationType + ) + { + switch (injectionType) + { + case InjectionType.Singleton: + HandleSingletonRegistration(registrableInterface, implementationType); + break; + case InjectionType.Transient: + _serviceCollection.AddTransient(registrableInterface, implementationType); + break; + case InjectionType.Scoped: + _serviceCollection.AddScoped(registrableInterface, implementationType); + break; + default: + throw new ArgumentOutOfRangeException(nameof(injectionType), $"Unknown injection type on {implementationType.Namespace}.{implementationType.Name}"); + } + } + + private void HandleSingletonRegistration(Type registrableInterface, Type implementationType) + { + var serviceKey = $"{implementationType.Namespace}.{implementationType.Name}"; + if (registrableInterface != implementationType) + { + _serviceCollection.AddSingleton(registrableInterface, (serviceProvider) => + { + object service; + lock (_injectedValuesLock) + { + if (!_injectedValues.TryGetValue(serviceKey, out service)) + { + service = serviceProvider.GetService(implementationType); + _injectedValues.Add(serviceKey, service); + } + } + + return service; + }); + } + else + { + _serviceCollection.AddSingleton(registrableInterface, implementationType); + } + } + + private class TypeRefContainer + { + public Injectable InjectableAttribute { get; } + public Type Type { get; } + public Type ParentType { get; } + + public TypeRefContainer(Injectable injectable, Type parentType, Type type) + { + InjectableAttribute = injectable; + Type = type; + ParentType = parentType; + } + } +} diff --git a/Libraries/SPTarkov.DI/DependencyInjectionRegistrator.cs b/Libraries/SPTarkov.DI/DependencyInjectionRegistrator.cs deleted file mode 100644 index d424f6ba..00000000 --- a/Libraries/SPTarkov.DI/DependencyInjectionRegistrator.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System.Reflection; -using SPTarkov.Common.Annotations; - -namespace SPTarkov.DI; - -public static class DependencyInjectionRegistrator -{ - private static List? _allLoadedTypes; - private static List? _allConstructors; - - public static void RegisterModOverrideComponents(IServiceCollection builderServices, List assemblies) - { - // We get all the services from this assembly first, since mods will override them later - RegisterComponents( - builderServices, - assemblies.SelectMany(a => a.GetTypes()) - .Where(type => Attribute.IsDefined(type, typeof(Injectable))) - ); - } - - public static void RegisterComponents(IServiceCollection builderServices, IEnumerable types) - { - var groupedTypes = types.SelectMany(t => - { - var attributes = (Injectable[]) Attribute.GetCustomAttributes(t, typeof(Injectable)); - var registerableType = t; - var registerableComponents = new List(); - foreach (var attribute in attributes) - { - // if we have a type override this takes priority - if (attribute.InjectableTypeOverride != null) - { - registerableType = attribute.InjectableTypeOverride; - } - // if this class only has 1 interface we register it on that interface - else if (registerableType.GetInterfaces().Length == 1) - { - registerableType = registerableType.GetInterfaces()[0]; - } - - registerableComponents.Add(new RegistrableType(registerableType, t, attribute)); - } - - return registerableComponents; - } - ) - .GroupBy(t => $"{t.RegistrableInterface.Namespace}.{t.RegistrableInterface.Name}"); - // We get all injectable services to register them on our services - foreach (var groupedInjectables in groupedTypes) - { - foreach (var valueTuple in groupedInjectables.OrderBy(t => t.InjectableAttribute.TypePriority)) - { - if (valueTuple.TypeToRegister.IsGenericType) - { - RegisterGenericComponents(builderServices, valueTuple); - } - else - { - RegisterComponent( - builderServices, - valueTuple.InjectableAttribute.InjectionType, - valueTuple.RegistrableInterface, - valueTuple.TypeToRegister - ); - } - } - } - } - - private static void RegisterGenericComponents(IServiceCollection builderServices, RegistrableType valueTuple) - { - try - { - _allLoadedTypes ??= AppDomain.CurrentDomain.GetAssemblies().SelectMany(t => t.GetTypes()).ToList(); - } - catch (ReflectionTypeLoadException ex) - { - Console.WriteLine($"COULD NOT LOAD TYPE: {ex}"); - } - - _allConstructors ??= _allLoadedTypes.SelectMany(t => t.GetConstructors()).ToList(); - - var typeName = $"{valueTuple.RegistrableInterface.Namespace}.{valueTuple.RegistrableInterface.Name}"; - try - { - var matchedConstructors = _allConstructors.Where(c => c.GetParameters() - .Any(p => p.ParameterType.IsGenericType && - p.ParameterType.GetGenericTypeDefinition().FullName == typeName - ) - ); - - var constructorInfos = matchedConstructors.ToList(); - if (constructorInfos.Count == 0) - { - return; - } - - foreach (var matchedConstructor in constructorInfos) - { - var constructorParams = matchedConstructor.GetParameters(); - foreach (var parameterInfo in constructorParams.Where(x => IsMatchingGenericType(x, typeName))) - { - var parameters = parameterInfo.ParameterType.GetGenericArguments(); - var typedGeneric = valueTuple.TypeToRegister.MakeGenericType(parameters); - RegisterComponent( - builderServices, - valueTuple.InjectableAttribute.InjectionType, - parameterInfo.ParameterType, - typedGeneric - ); - } - } - } - catch (Exception e) - { - Console.WriteLine(e); - throw; - } - } - - private static bool IsMatchingGenericType(ParameterInfo paramInfo, string typeName) - { - return paramInfo.ParameterType.IsGenericType && - paramInfo.ParameterType.GetGenericTypeDefinition().FullName == typeName; - } - - private static void RegisterComponent( - IServiceCollection builderServices, - InjectionType injectionType, - Type registrableInterface, - Type implementationType - ) - { - switch (injectionType) - { - case InjectionType.Singleton: - builderServices.AddSingleton(registrableInterface, implementationType); - break; - case InjectionType.Transient: - builderServices.AddTransient(registrableInterface, implementationType); - break; - case InjectionType.Scoped: - builderServices.AddScoped(registrableInterface, implementationType); - break; - default: - throw new ArgumentOutOfRangeException(nameof(injectionType), "unknown injection type"); - } - } - - public static void RegisterSptComponents( - Assembly serverLauncherAssembly, - Assembly coreAssembly, - IServiceCollection builderServices - ) - { - // We get all the services from this assembly first, since mods will override them later - RegisterComponents( - builderServices, - serverLauncherAssembly.GetTypes().Where(type => Attribute.IsDefined(type, typeof(Injectable))) - .Concat(coreAssembly.GetTypes().Where(type => Attribute.IsDefined(type, typeof(Injectable)))) - ); - } - - private sealed class RegistrableType(Type registrableInterface, Type typeToRegister, Injectable injectableAttribute) - { - public Type RegistrableInterface - { - get; - } = registrableInterface; - - public Type TypeToRegister - { - get; - } = typeToRegister; - - public Injectable InjectableAttribute - { - get; - } = injectableAttribute; - } -} diff --git a/Libraries/SPTarkov.DI/SPTarkov.DI.csproj b/Libraries/SPTarkov.DI/SPTarkov.DI.csproj index b0225cdd..0a7bf557 100644 --- a/Libraries/SPTarkov.DI/SPTarkov.DI.csproj +++ b/Libraries/SPTarkov.DI/SPTarkov.DI.csproj @@ -1,6 +1,6 @@ - + SPTarkov.DI @@ -16,15 +16,15 @@ - + - + - + diff --git a/Libraries/SPTarkov.DI/SingletonStateHolder.cs b/Libraries/SPTarkov.DI/SingletonStateHolder.cs new file mode 100644 index 00000000..77f10fab --- /dev/null +++ b/Libraries/SPTarkov.DI/SingletonStateHolder.cs @@ -0,0 +1,14 @@ +namespace SPTarkov.DI; + +public class SingletonStateHolder +{ + public T State + { + get; + } + + public SingletonStateHolder(T state) + { + State = state; + } +} diff --git a/Libraries/SPTarkov.Reflection/CodeWrapper/Code.cs b/Libraries/SPTarkov.Reflection/CodeWrapper/Code.cs new file mode 100644 index 00000000..87c8d832 --- /dev/null +++ b/Libraries/SPTarkov.Reflection/CodeWrapper/Code.cs @@ -0,0 +1,46 @@ +using System.Reflection.Emit; + +namespace SPTarkov.Reflection.CodeWrapper; + +public class Code +{ + public OpCode OpCode { get; } + public Type? CallerType { get; } + public object? OperandTarget { get; } + public Type[]? Parameters { get; } + public bool HasOperand { get; } + + public Code(OpCode opCode) + { + OpCode = opCode; + HasOperand = false; + } + + public Code(OpCode opCode, object operandTarget) + { + OpCode = opCode; + OperandTarget = operandTarget; + HasOperand = true; + } + + public Code(OpCode opCode, Type callerType) + { + OpCode = opCode; + CallerType = callerType; + HasOperand = true; + } + + public Code(OpCode opCode, Type callerType, object operandTarget, Type[] parameters = null) + { + OpCode = opCode; + CallerType = callerType; + OperandTarget = operandTarget; + Parameters = parameters; + HasOperand = true; + } + + public virtual Label? GetLabel() + { + return null; + } +} diff --git a/Libraries/SPTarkov.Reflection/CodeWrapper/CodeGenerator.cs b/Libraries/SPTarkov.Reflection/CodeWrapper/CodeGenerator.cs new file mode 100644 index 00000000..7854a94c --- /dev/null +++ b/Libraries/SPTarkov.Reflection/CodeWrapper/CodeGenerator.cs @@ -0,0 +1,73 @@ +using System.Reflection.Emit; +using HarmonyLib; + +namespace SPTarkov.Reflection.CodeWrapper; + +/// +/// Helper class to generate IL code for transpilers +/// +public class CodeGenerator +{ + public static List GenerateInstructions(List codes) + { + var list = new List(); + + foreach (Code code in codes) + { + list.Add(ParseCode(code)); + } + + return list; + } + + private static CodeInstruction ParseCode(Code code) + { + if (!code.HasOperand) + { + return new CodeInstruction(code.OpCode) { labels = GetLabelList(code) }; + } + + if (code.OpCode == OpCodes.Ldfld || code.OpCode == OpCodes.Ldflda || code.OpCode == OpCodes.Stfld) + { + return new CodeInstruction(code.OpCode, AccessTools.Field(code.CallerType, code.OperandTarget as string)) { labels = GetLabelList(code) }; + } + + if (code.OpCode == OpCodes.Call || code.OpCode == OpCodes.Callvirt) + { + return new CodeInstruction(code.OpCode, AccessTools.Method(code.CallerType, code.OperandTarget as string, code.Parameters)) { labels = GetLabelList(code) }; + } + + if (code.OpCode == OpCodes.Box) + { + return new CodeInstruction(code.OpCode, code.CallerType) { labels = GetLabelList(code) }; + } + + if (code.OpCode == OpCodes.Br || code.OpCode == OpCodes.Brfalse || code.OpCode == OpCodes.Brtrue || code.OpCode == OpCodes.Brtrue_S + || code.OpCode == OpCodes.Brfalse_S || code.OpCode == OpCodes.Br_S) + { + return new CodeInstruction(code.OpCode, code.OperandTarget) { labels = GetLabelList(code) }; + } + + if (code.OpCode == OpCodes.Ldftn) + { + return new CodeInstruction(code.OpCode, AccessTools.Method(code.CallerType, code.OperandTarget as string, code.Parameters)) { labels = GetLabelList(code) }; + } + + if (code.OpCode == OpCodes.Newobj) + { + return new CodeInstruction(code.OpCode, code.CallerType.GetConstructors().FirstOrDefault(x => x.GetParameters().Length == code.Parameters.Length)) { labels = GetLabelList(code) }; + } + + throw new ArgumentException($"Code with OpCode {code.OpCode.ToString()} is not supported."); + } + + private static List