diff --git a/.editorconfig b/.editorconfig index cfb34337..3445c271 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,8 +15,8 @@ indent_style = space indent_size = 4 charset = utf-8-bom -# XML project files -[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +# XML + XML project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,xml,xsd}] indent_size = 2 # XML config files diff --git a/CHANGES.md b/CHANGES.md index c9b23ab5..f5dbf7c3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,9 @@ Since v2, ModTek adheres to [Semantic Versioning](http://semver.org/) for runtim - Some mods expect the managed assemblies location to be in the `Managed` directory, however injected assemblies are now found under `Mods/.modtek/AssembliesInjected` or loaded directly into memory after injection. - The HarmonyX feature works well, however some mods might rely on some buggy Harmony 1.2 behaviors that HarmonyX shims don't replicate. +- Missing (markdown) documentations: + - ModTekSimpleInjector -> link to xml, but from where? + - Run injectors from dotnet/msbuild thanks to netstandard2.0 -> still experimental ## 4.4 - CptMoore @@ -17,6 +20,8 @@ For modders: - Includes and Excludes were renamed to Include/Exclude and use the `PrefixesToIgnore` syntax. - `PrefixesToIgnore` was removed. - Removed the newly introduced simple mod logger in `mod.json` and keeping the undocumented `Logs` logger config. +- Added ModTekSimpleInjector, simplifies adding instance fields to existing base game types. + - See [ModTekSimpleInjector.Example.xml](ModTekSimpleInjector/ModTekSimpleInjector.Example.xml) for documentation. ## 4.3 - CptMoore diff --git a/ModTek.Injectors/Runner.cs b/ModTek.Injectors/Runner.cs index 676a7fea..f0d3afce 100644 --- a/ModTek.Injectors/Runner.cs +++ b/ModTek.Injectors/Runner.cs @@ -92,7 +92,12 @@ private void InvokeInjector(string name, MethodInfo injectMethod) Console.SetError(errorLogger); try { - injectMethod.Invoke(null, new object[] { _assemblyCache }); + injectMethod.Invoke(null, [_assemblyCache]); + } + catch (Exception ex) + { + Logger.Main.Log($"Injector {name} threw an exception: " + ex); + throw; } finally { diff --git a/ModTek.sln b/ModTek.sln index d29beddb..65c4d72f 100644 --- a/ModTek.sln +++ b/ModTek.sln @@ -34,6 +34,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModTek.Injectors", "ModTek. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModTek.InjectorsTask", "ModTek.InjectorsTask\ModTek.InjectorsTask.csproj", "{D4306496-4941-47A6-9AEA-B37A3E7BD15D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModTekSimpleInjector", "ModTekSimpleInjector\ModTekSimpleInjector.csproj", "{1B0D10A3-7AB5-4658-B0F7-D5B4AC0676E2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -80,6 +82,10 @@ Global {D4306496-4941-47A6-9AEA-B37A3E7BD15D}.Debug|Any CPU.Build.0 = Debug|Any CPU {D4306496-4941-47A6-9AEA-B37A3E7BD15D}.Release|Any CPU.ActiveCfg = Release|Any CPU {D4306496-4941-47A6-9AEA-B37A3E7BD15D}.Release|Any CPU.Build.0 = Release|Any CPU + {1B0D10A3-7AB5-4658-B0F7-D5B4AC0676E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B0D10A3-7AB5-4658-B0F7-D5B4AC0676E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B0D10A3-7AB5-4658-B0F7-D5B4AC0676E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B0D10A3-7AB5-4658-B0F7-D5B4AC0676E2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ModTekSimpleInjector/Injector.cs b/ModTekSimpleInjector/Injector.cs new file mode 100644 index 00000000..ecb769e6 --- /dev/null +++ b/ModTekSimpleInjector/Injector.cs @@ -0,0 +1,178 @@ +using System; +using System.IO; +using System.Xml.Serialization; +using Mono.Cecil; +using FieldAttributes = Mono.Cecil.FieldAttributes; + +namespace ModTekSimpleInjector; + +/* +What is hard and therefore not supported: +- custom attributes + - e.g. [JsonIgnored] + - mainly due to constructor arguments being hard [SerializableMember(SerializationTarget.SaveGame)] +- custom types introduced by mods + - injectors run before mods are loaded. if a modded type is added, but the mod is not even loaded.. what now? + - also how does the injector know where to find the types? -> mod dlls are unknown during injector time + - some kind of special assembly type with "injectable" types would be a solution, requires some ahead of type loading +- modifying existing fields + - visibility should not be modified during runtime since it can crash (subclassing), and compile time via publicizer is good enough + - changing static, const would also crash stuff + - changing type -> contra/covariance issues + - custom attributes -> might be interesting but highly situational. +- adding properties + - this is more complicate due to compiler backed fields etc.. just use extension methods and "Unsafe.As" +- adding methods + - a mod can just use static methods and use the first argument with (this ok ...) and it will look like instance methods +*/ +internal static class Injector +{ + public static void Inject(IAssemblyResolver resolver) + { + var baseDirectory = Path.GetDirectoryName(typeof(Injector).Assembly.Location); + var files = Directory.GetFiles(baseDirectory, "ModTekSimpleInjector.*.xml"); + Array.Sort(files); + var serializer = new XmlSerializer(typeof(Additions)); + foreach (var file in files) + { + if (file.EndsWith("ModTekSimpleInjector.Example.xml")) + { + continue; + } + Console.WriteLine($"Processing additions in file {file}"); + using var reader = new StreamReader(file); + var additions = (Additions)serializer.Deserialize(reader); + ProcessAdditions(resolver, additions); + } + } + + private static void ProcessAdditions(IAssemblyResolver resolver, Additions additions) + { + foreach (var addition in additions.AddField) + { + Console.WriteLine($"Processing {addition}"); + ResolveAssemblyAndType(resolver, addition, out var assemblyDefinition, out var typeDefinition); + ProcessAddField(assemblyDefinition, typeDefinition, addition); + } + foreach (var addition in additions.AddEnumConstant) + { + Console.WriteLine($"Processing {addition}"); + ResolveAssemblyAndType(resolver, addition, out _, out var typeDefinition); + ProcessAddEnumConstant(typeDefinition, addition); + } + } + + private static void ResolveAssemblyAndType( + IAssemblyResolver resolver, + Addition addition, + out AssemblyDefinition assemblyDefinition, + out TypeDefinition typeDefinition + ) { + var assemblyName = new AssemblyNameReference(addition.InAssembly, null); + assemblyDefinition = resolver.Resolve(assemblyName); + if (assemblyDefinition == null) + { + throw new ArgumentException($"Unable to resolve assembly {addition.InAssembly}"); + } + typeDefinition = assemblyDefinition.MainModule.GetType(addition.ToType); + if (typeDefinition == null) + { + throw new ArgumentException($"Unable to resolve type {addition.ToType} in assembly {addition.InAssembly}"); + } + } + + private static void ProcessAddField(AssemblyDefinition assembly, TypeDefinition type, AddField fieldAddition) + { + var fieldType = ResolveType(fieldAddition.OfType); + var fieldTypeReference = assembly.MainModule.ImportReference(fieldType); + var field = new FieldDefinition(fieldAddition.Name, fieldAddition.Attributes, fieldTypeReference); + type.Fields.Add(field); + } + + private static void ProcessAddEnumConstant(TypeDefinition type, AddEnumConstant enumConstant) + { + const FieldAttributes EnumFieldAttributes = + FieldAttributes.Static + | FieldAttributes.Literal + | FieldAttributes.Public + | FieldAttributes.HasDefault; + var constantValue = enumConstant.Value; + var field = new FieldDefinition(enumConstant.Name, EnumFieldAttributes, type) + { + Constant = constantValue + }; + type.Fields.Add(field); + } + + private static Type ResolveType(string additionType) + { + var typeName = additionType; + var isArray = typeName.EndsWith("[]"); + if (isArray) + { + typeName = typeName[..^2]; + } + var fieldType = typeof(int).Assembly.GetType(typeName); + if (isArray) + { + fieldType = fieldType.MakeArrayType(); + } + return fieldType; + } +} + +// XmlSerializer requires the following classes to be public + +[XmlRoot(ElementName = "ModTekSimpleInjector")] +public class Additions +{ + [XmlElement(ElementName = "AddField")] + public AddField[] AddField = []; + [XmlElement(ElementName = "AddEnumConstant")] + public AddEnumConstant[] AddEnumConstant = []; +} + +public abstract class Addition +{ + [XmlAttribute("InAssembly")] + public string InAssembly; + [XmlAttribute("ToType")] + public string ToType; + + public override string ToString() + { + return $"{this.GetType().Name}:{InAssembly}:{ToType}"; + } +} + +[XmlType("AddField")] +[XmlRoot(ElementName = "AddField")] +public class AddField : Addition +{ + [XmlAttribute("Name")] + public string Name; + [XmlAttribute("OfType")] + public string OfType; + [XmlAttribute("Attributes")] + public FieldAttributes Attributes = FieldAttributes.Private; + + public override string ToString() + { + return $"{base.ToString()}:{Name}:{OfType}:{Attributes}"; + } +} + +[XmlType("AddEnumConstant")] +[XmlRoot(ElementName = "AddEnumConstant")] +public class AddEnumConstant : Addition +{ + [XmlAttribute("Name")] + public string Name; + [XmlAttribute("Value")] + public int Value; + + public override string ToString() + { + return $"{base.ToString()}:{Name}:{Value}"; + } +} diff --git a/ModTekSimpleInjector/ModTekSimpleInjector.Example.xml b/ModTekSimpleInjector/ModTekSimpleInjector.Example.xml new file mode 100644 index 00000000..16182f4b --- /dev/null +++ b/ModTekSimpleInjector/ModTekSimpleInjector.Example.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ModTekSimpleInjector/ModTekSimpleInjector.csproj b/ModTekSimpleInjector/ModTekSimpleInjector.csproj new file mode 100644 index 00000000..34e0c6c8 --- /dev/null +++ b/ModTekSimpleInjector/ModTekSimpleInjector.csproj @@ -0,0 +1,19 @@ + + + + + + + + + + + + All + runtime + + + False + + + \ No newline at end of file