-
Notifications
You must be signed in to change notification settings - Fork 20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pre-load patching capability #279
Comments
So, did some prototyping, this was actually pretty fun. Writing here what I did, as I'm not sure if and when I will push this further. First step was the usual This was actually more difficult than anticipated. There are plenty of off the shelf assembly strippers (and often publiciziers), but they are meant to produce a reference assembly that can be used in another project, they don't particularly care about producing a valid assembly that is somewhat close to the original source once decompiled. They also tend to mess around with member visibility and attributes. I ended up putting together a custom command line utility for that, using Mono.Cecil, which :
Once I got my somewhat clean stripped DLL, I decompiled it back to C# with ILSpy, with the C# 7.3 profile. A bit of compiler error fixing latter, I had my re-compilable-but-stripped Assembly-CSharp source project. There are probably a few things that didn't round-trip perfectly identically like base constructor calls, and I suspect some event declarations might be messed up too, but that wasn't strictly necessary anyway. Now the fun part could begin. The pre-loader and pre-patcher prototype is based on UnityDoorstop and AsmResolver respectively. Getting AsmResolver to run wasn't trivial, as it depends on various nuget packages, but more problematic, is targeting netstandard 2.0. But a nice thing about preloading is that it gives total control over which assemblies are loaded, so I took up the task of adding all the missing stuff in the KSP BCL distribution. The easiest way to find out what was need was to make a 2019.4 unity project, import AsmResolver with NugetForUnity, build the result and grab everything missing from the KSP distribution : The only Mono/BCL dll I removed was The key point here is having the UnityDoorstop only loads mscorlib, so from my managed entry point, I manually loaded the few BCL libraries shipped with KSP from So back the actual patching, I took my stripped source project, made all classes I'm then building the assembly, putting it somehwere in a KSP sub-dir, and using AsmResolver, I loaded the original Fortunately, AsmResolver provide a very neat MemberCloner thing handling most of the heavy lifting of rewiring everything properly. What we are trying to do (replacing a method body by another) isn't exactly what it was designed for. The purpose of that tool is to rewire all type/member cross references when moving stuff from an assembly to another. But in our case, we not only need cross references to be rewired, but also any reference to a type/member in Assembly-CSharp from our stripped version to the original version. This can be achieved by feeding the protected override ITypeDefOrRef ImportType(TypeDefinition type)
{
if (type.Module == patchAssembly)
foreach (TypeDefinition kspType in kspAssembly.GetAllTypes())
if (Comparer.Equals(kspType, type))
return kspType;
return base.ImportType(type);
} The final step is to call I love this AsmResolver thing. The code is fully documented, the manual is extensive and super clear. The absolute opposite of Mono.Cecil and MonoMod. I guess things might get a bit more complicated in some cases (generics ? compiler generated members ?), but so far this seems to be working nicely with a minimal amount of intervention. A few remarks / ideas :
For reference :
|
So, update on this journey (this is kinda starting to feel like a story, no ?) I refactored the whole thing to get it a bit more streamlined, looks much better than the previous mess. I also implemented the additional patching options, in addition to modifying stock methods, it's now possible to add new methods and fields to stock types, and to add whole new types as well. I was suspecting things might not go so smoothly with adding fields top stock types, and indeed my intuition turned out to be true. Adding fields to classes deriving from
This is quite unfortunate, as I believe more or less every type where adding fields would has been useful is coming from a prefab at some point. And even for other types, I'm not sure Unity will handle serialization/deserialization properly. Unity serialization doesn't work on types not defined in Assembly-CSharp, so likely there is some bundled cache about the layout of every type Unity knows. And even if we somehow manage to hack that type layout information, that would still leave us with the issue that the serialized assets don't match the new layout... Edit : Ha stupid me ! |
Okay, so I have a somewhat finalized implementation, but I've been giving some thought about the more general picture. We need to rely on pre-loading / UnityDoorstop. The implicit consequence is that we will be doing things in a new way that while standard in other games modding scenes, is quite new and unusual for the KSP modding scene :
The prototyped Assembly-CSharp patching infrastructure is based on a stripped from every implementation copy of its source code, where all stock types are made
Moving forward with this, one major consideration that we would impose our solution as the sole pre-loader solution for the KSP modding ecosystem, excluding notably BepInEx. AFAIK, there isn't currently any KSP mod using BepInEx or any other preloader, but this mean that we need to consider what the modding ecosystem might want to do, and which capabilities we want to expose :
Finally, we need to think through how to handle the transition from the current ecosystem. We have two separate plugins, HarmonyKSP, and KSPCF having a dependency on it. Through the modding ecosystem, there are various mods having a dependency on either or both. These dependencies are expressed at two levels : in the CKAN metadata and through the KSPAssembly attributes. If we go through with this, we would have up to four somewhat separate components instead of two :
Given the level of cross dependency, it would make sense to have a single monolithic distribution as a major 2.0 update of KSPCF. We would retire the existing HarmonyKSP mod, having KSPCF 2.0 be a provider for the identifier at the CKAN metadata level, and we would load "fake" assemblies with the KSPAssembly attribute we currently define in KSPHarmony (which is what we are doing already, more or less). However, the major consequence would be effectively forcing every mod currently depending on Harmony but not on KSPCF to depend on it, but also to depend on the preloader, which comes with caveats for non-windows end users (see at the beginning of this post). This is the current list of (CKAN tracked) mods having a direct dependency on HarmonyKSP : This might not seem much, but Kopernicus, CC and Shabby are themselves very common dependencies for many other mods. In practice, I'd say HarmonyKSP is likely installed for the overwhelming majority of modded KSP end users. The middle ground solution could be :
In this scenario, this would avoid the caveats for non-windows end users that don't have KSPCF but have mods depending on HarmonyKSP. In the end, this is likely a small subset of users and Linux users are usually tech-savvy enough for this not to be a problem, I would be more worried about the average Mac end user. |
I've been giving some thought about implementing some form of pre-load patching ability for KSPCF.
Main motivations :
The base requirement would be to use an injector, giving us the ability to run code before any dll in
KSP_x64_Data\Managed
is loaded.There are multiple off the shelf options for this, the most stable and well maintained one seems to be UnityDoorstop .
Another option would be BepInEx, but the scope of BepInEx goes well beyond what we need. It is built upon UnityDoorstop, but also has a full fledged mod loader (that we have no real use for), is packaging a custom version of Harmony (which is always somewhat lagging behind the original and has a few compatibility quirks), and comes with a lot of extra bloat that we have no use for.
Moving KSPCF to BepInEx would be a somewhat breaking change for the ecosystem, as we would have to retire the current HarmonyKSP plugin and swap it for BepInEx, which would become the new dependency for mods using Harmony. This being said, this would allow other plugins to have pre-load patching capabilities as well, but in the end, pre-load patching is a very non-cooperative technique and it would be very likely other plugins modifications it would end up conflicting with KSPCF own modifications, plus there are probably not that many real use cases for it, other than in a few "foundational" mods like Kopernicus or ModuleManager, and for such cases, it would likely be worthwhile to put the changes in KSPCF anyway.
One issue is that UnityDoorstop is relying on a platform specific native dll, so we would have to redistribute a separate version for every platform (windows, linux, macos), and this is especially problematic in the context of CKAN, which AFAIK doesn't provide any way to selectively install stuff based on the current platform.
This injection related stuff put aside, there are other considerations as to how exactly we would implement Assembly-CSharp patching.
The usual way (like it exist in BepInEx) is to use either Mono.Cecil to edit the assembly at the IL level, which is extremely cumbersome, or to use MonoMod.Patcher, which is a helper/wrapper built on top of Mono.Cecil and that makes the whole process a bit more usable and practical, a bit like Harmony for runtime patching.
There are a few caveats to the MonoMod.Patcher approach :
Which is why I'm tempted to look into a more radical approach : doing all our changes in a (stripped from all stock code) decompiled assembly-csharp source. The idea would be to have every stock method body replaced with
throw new NotImplementedException()
, and every type madepartial
. This would allow us to make our changes in separate source files, although replaced methods would likely have to be deleted from the original source (full support for partial methods is implemented in C# 9 / .NET 5, but it's doubtful we can use such a target).Added and modified members would have to be annotated with an attribute. Then, on pre-loading, we could inject the changes identified by those attributes into the original Assembly-CSharp (or maybe the other way around, inject the original Assembly-CSharp into our own assembly), then load the modified version. In theory, it should be possible to merge the debug symbols as well. Options for doing all this are either Mono.Cecil or the newer and much better documented AsmResolver
Doing this would obviously require quite a lot of low level infrastructure work, but there are several benefits :
The text was updated successfully, but these errors were encountered: