Skip to content

Commit

Permalink
Added ModTekSimpleInjector.
Browse files Browse the repository at this point in the history
  • Loading branch information
CptMoore committed Jan 26, 2025
1 parent 75faa87 commit 908af52
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 3 deletions.
4 changes: 2 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
7 changes: 6 additions & 1 deletion ModTek.Injectors/Runner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
6 changes: 6 additions & 0 deletions ModTek.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
178 changes: 178 additions & 0 deletions ModTekSimpleInjector/Injector.cs
Original file line number Diff line number Diff line change
@@ -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}";
}
}
42 changes: 42 additions & 0 deletions ModTekSimpleInjector/ModTekSimpleInjector.Example.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Introduction
ModTekSimpleInjector allows you to quickly add fields to existing types without having to write your own injectors.
Start by placing a modified copy of this file next to the dll. Use your mod name in the file name:
> `ModTekSimpleInjector.YOURMOD.xml`
Multiple `AddField` and `AddEnumConstant` elements (0..n each) can be specified.
Do not rely on fields added by other mods, unless explicitly advised to do so.
-->
<ModTekSimpleInjector>
<!-- ToType & InAssembly
Nested types are referenced using `/`, seems to be Mono.Cecil specific.
Only assemblies found in `Managed/` can be referenced.
Assemblies already loaded and can't be modified, e.g. mscorlib, System and System.Core.
-->

<!-- AddField
AddField adds a new instance field to an existing type.
Fields can be of any types found in mscorlib.
Keywords (e.g. byte) are not supported, use the types full name (e.g. System.Byte).
For custom types use `System.Object` and `Unsafe.As<object, MyType>(ref addedField)` for high performance access.
A `[]` suffix means that the type should be an array of the specified type.
-->
<AddField ToType="HBS.Logging.Logger/LogImpl" InAssembly="Assembly-CSharp"
Name="nameAsBytes2" OfType="System.Byte[]" />

<!-- AddField and Attributes
Added fields are private by default, using other attributes is **strongly** discouraged.
Publicizers allow to efficiently access private fields anyway.
-->
<AddField ToType="HBS.Logging.Logger/LogImpl" InAssembly="Assembly-CSharp"
Name="secondaryName" OfType="System.String" Attributes="Public" />

<!-- AddEnumConstant
AddEnumConstant adds an enum constant to an existing enum type.
Multiple enum constants with the same value can be added, they should behave like aliases. This is usually unwanted.
Make sure to understand `[Flags]` in case it applies.
Enums are just ints and one can convert to and from enums using ints, other mods might already be using an int value you are targeting.
-->
<AddEnumConstant ToType="HBS.Logging.LogLevel" InAssembly="Assembly-CSharp"
Name="Trace" Value="200" />
</ModTekSimpleInjector>
19 changes: 19 additions & 0 deletions ModTekSimpleInjector/ModTekSimpleInjector.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\CommonNetStandard.props" />

<Target Name="CopyFilesToGame" AfterTargets="CopyFilesToOutputDirectory">
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(BattleTechGameDir)\Mods\ModTek\Injectors\" />
<Copy SourceFiles="ModTekSimpleInjector.Example.xml" DestinationFolder="$(BattleTechGameDir)\Mods\ModTek\Injectors\" />
</Target>

<ItemGroup>
<!-- we only need Mono.Cecil, but HarmonyX pins the version we need -->
<PackageReference Include="HarmonyX">
<PrivateAssets>All</PrivateAssets>
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
<Reference Include="Mono.Cecil">
<Private>False</Private>
</Reference>
</ItemGroup>
</Project>

0 comments on commit 908af52

Please sign in to comment.