diff --git a/.editorconfig b/.editorconfig index 8247b27b..a873b41c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -29,9 +29,6 @@ csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_between_query_expression_clauses = true -# ReSharper properties -resharper_wrap_object_and_collection_initializer_style = chop_always - # Indentation preferences csharp_indent_block_contents = true csharp_indent_braces = false @@ -77,13 +74,14 @@ dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case csharp_using_directive_placement = outside_namespace:error dotnet_sort_system_directives_first = true csharp_prefer_braces = true:warning -csharp_preserve_single_line_blocks = false -csharp_preserve_single_line_statements = false csharp_prefer_static_local_function = true:suggestion csharp_prefer_simple_using_statement = false:none csharp_style_prefer_switch_expression = true:suggestion dotnet_style_readonly_field = true:suggestion +# csharp_preserve_single_line_blocks = false +# csharp_preserve_single_line_statements = false + # Expression-level preferences dotnet_style_object_initializer = true:suggestion dotnet_style_collection_initializer = true:suggestion @@ -105,13 +103,11 @@ csharp_style_expression_bodied_operators = false:warning csharp_style_expression_bodied_properties = false:warning csharp_style_expression_bodied_indexers = false:warning csharp_style_expression_bodied_accessors = false:warning -csharp_style_expression_bodied_lambdas = false:warning csharp_style_expression_bodied_local_functions = false:warning +csharp_style_expression_bodied_lambdas = true # Pattern matching -csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion -csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_prefer_pattern_matching = false:warning # Null checking preferences csharp_style_throw_expression = true:suggestion @@ -121,6 +117,7 @@ csharp_style_conditional_delegate_call = true:suggestion csharp_style_prefer_index_operator = false:none csharp_style_prefer_range_operator = false:none csharp_style_pattern_local_over_anonymous_function = false:none +csharp_style_inlined_variable_declaration = true # Space preferences csharp_space_after_cast = true @@ -145,6 +142,10 @@ csharp_space_between_method_declaration_name_and_open_parenthesis = false csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_square_brackets = false +# ReSharper properties +resharper_wrap_object_and_collection_initializer_style = chop_always +resharper_merge_into_pattern_highlighting = none + # Xml project files [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] indent_size = 2 diff --git a/.github/ISSUE_TEMPLATE/1-bug-report.yml b/.github/ISSUE_TEMPLATE/1-bug-report.yml new file mode 100644 index 00000000..692c8f57 --- /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/server-.log`." + 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/Benchmarks/Benchmarks.csproj b/Benchmarks/Benchmarks.csproj index 705c5e3c..2e9d441a 100644 --- a/Benchmarks/Benchmarks.csproj +++ b/Benchmarks/Benchmarks.csproj @@ -1,6 +1,6 @@ - + Exe @@ -8,11 +8,12 @@ - - - - - + + + + + + diff --git a/Libraries/SPTarkov.Common/Annotations/Injectable.cs b/Libraries/SPTarkov.DI/Annotations/Injectable.cs similarity index 65% rename from Libraries/SPTarkov.Common/Annotations/Injectable.cs rename to Libraries/SPTarkov.DI/Annotations/Injectable.cs index 11bddf86..44c62578 100644 --- a/Libraries/SPTarkov.Common/Annotations/Injectable.cs +++ b/Libraries/SPTarkov.DI/Annotations/Injectable.cs @@ -1,6 +1,6 @@ -namespace SPTarkov.Common.Annotations; +namespace SPTarkov.DI.Annotations; -[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] +[AttributeUsage(AttributeTargets.Class, Inherited = false)] public class Injectable(InjectionType injectionType = InjectionType.Scoped, Type? type = null, int typePriority = int.MaxValue) : Attribute { public InjectionType InjectionType @@ -9,12 +9,6 @@ public class Injectable(InjectionType injectionType = InjectionType.Scoped, Type set; } = injectionType; - public Type? InjectableTypeOverride - { - get; - set; - } = type; - public int TypePriority { get; diff --git a/Libraries/SPTarkov.DI/DependencyInjectionHandler.cs b/Libraries/SPTarkov.DI/DependencyInjectionHandler.cs new file mode 100644 index 00000000..005a968d --- /dev/null +++ b/Libraries/SPTarkov.DI/DependencyInjectionHandler.cs @@ -0,0 +1,220 @@ +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 object _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 sortedInjectableTypes = _injectedTypeNames.Values + .Select(t => + new TypeRefContainer(((Injectable[]) Attribute.GetCustomAttributes(t, typeof(Injectable)))[0], t, t)) + .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"); + } + } + + 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..a54002f6 --- /dev/null +++ b/Libraries/SPTarkov.Reflection/CodeWrapper/CodeGenerator.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +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