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