Jump to content

[WIP][Plugin/Tool][November 1] AssemblyReloader - Tool for plugin authors


xEvilReeperx

Recommended Posts

One of the first things almost everybody that writes a plugin for KSP discovers is that you're going to spend a lot of time restarting the game. It'd be pretty neato if you could just press a button and see your current version running in a few seconds, wouldn't it? This tool is designed to do that with a few restrictions.

I've brain dumped most of the important details into the manual. If you find a bug, have a comment or suggestion and so on let me know. It's only been tested on Windows 7/32 bit KSP so far but there isn't any technical reason aside from coder goof that would prevent it from running on other systems as far as I know.

Javascript is disabled. View full album

Supported types:

 

  • MonoBehaviour (w & w/o KSPAddon)
  • PartModule
  • ScenarioModule
  • VesselModule
  • Any type not blacklisted below

 

Known Issues:

 

  • None yet

 

Planned Features (note: if your plugin contains any of these types, it won't be reloadable -- see manual):

 

  • Contracts
  • Strategies
  • Kerbal experience traits
  • Kerbal experience effects
  • Parts (the type itself)
  • InternalModules
  • Undoubtedly I've forgotten some other types that need custom code
  • Interface improvements

 

This is meant to replicate stock behaviour as closely as possible, down to the order things are initialized. If you see something that doesn't seem right or behaviour is different when loaded by AssemblyReloader as opposed to the game's AssemblyLoader, let me know.

Don't use it with a save you care about naturally, there's all kinds of potential to wreck things using this. Make a backup, quicksave before reloading, etc

Download:

 

Download

 

 

Changelog:

1.0.2 (Nov 16 2015)

  • PartModule.OnInitialize now called correctly for reloaded PartModules
  • KSPAddons now created after all loaders have run
  • EveryScene addons no longer created twice
  • Fixed some serialization issues
  • Added ApplicationLauncher button
  • Fixed an issue that prevented Cecil from correctly resolving assemblies referenced by a reloadable plugin outside of the reloadable plugin's folder

 

1.0.1

  • Added check against KSPAddon-marked PartModules
  • Fixed a bug that tried to access opcodes in method definitions that had no body

Source:

AssemblyReloader (GPLv3)

ReeperCommon (dependency) (GPLv3)

ReeperAssemblyLibrary (dependency) (GPLv3)

ReeperLoader (dependency) (GPLv3)

StrangeIoC (dependency) (APACHE license)

Mono.Cecil 0.9.6 (dependency) (MIT/X11 license)

License: GPLv3

Edited by xEvilReeperx
1.3
Link to comment
Share on other sites

In theory I'll be diving back into AGX shortly here which is a pretty involved mod and so should be a good test of this. If it works it will save me a ton of time.

My only question would be if non-inherited classes reload? I have a lot of code in the PartModule and KSPAddon types that will be caught, but I then also have a lot of classes of my own present that would need to reload as well.

public class MyClass
{
/code
}

Is what I'm talking about.

D.

Link to comment
Share on other sites

Yes, all of the types local to a version of the assembly are unique to it and will be "reloaded" in the sense that from their perspective, they're being created for the very first time.

The types that need special code from AssemblyReloader are the ones that KSP "magically" finds. I put the restriction in place because it would otherwise be possible to have multiple versions of an assembly being actively used leading to a great deal of confusion.

You'll be responsible for any cleanup though. Example:

public class MySettingsSingleton
{
private static MySettingsSingleton _instance;

public static MySettingsSingleton Instance
{
get
{
if (_instance != null) return _instance;
_instance = new MySettingsSingleton();
GameEvents.onGameStateSave.Add(_instance.OnGameSave);
GameEvents.onGameStateLoad.Add(_instance.OnGameLoad);

return _instance;
}
}

private void OnGameSave(ConfigNode config)
{
// ...
}

private void OnGameLoad(ConfigNode config)
{
// ...
}
}

When the plugin containing this type is reloaded, this particular object will stick around due to its static nature. When the "new" version refers to a MySettingsSingleton, it will refer to its own version which hasn't been initialized and suddenly you've now got two of them hanging around. This example is pretty harmless because those GameEvent registrations will be forcefully removed on reload making it inert (and a warning will be displayed in the log letting you know you've forgotten to unregister something) but you can see how there might be unintended consequences depending on what exactly this object does

Link to comment
Share on other sites

Hmmm.

I'll have to remember to do clean testing at the end then, I do have some static stuff that might run afoul of that, but I think it all reloads on game scene switch so a quick jump back to spacecenter should fix any issues I run into.

I'll let you know how it goes when I get a chance to look at this, will probably be a few days still.

Actually, how do you use this at all? I have my dev environment setup to create the revised .dll files directly into my testing KSP install GameData directory, if I try to build a new version with KSP still open the build fails because Visual Studio can't lock the files to overwrite them. Do I need to build to another directory and copy them in via Windows Explorer?

(Apologies if this is in the manual, on mobile and have not yet downloaded it.)

D.

Link to comment
Share on other sites

It is but no biggie. Rename the dll to reloadable ("MyPlugin.dll" -> "MyPlugin.reloadable", "MyPlugin.dll.mdb" -> "MyPlugin.reloadable.mdb" if you use debug symbols). The tool will load it in a way that doesn't lock the file. A simple post-build event works great

Link to comment
Share on other sites

Okay, I've gotten a chance to try this out and unfortunately it seems to cause issues with ModuleManager.

Making no other changes (so my mod is still AGExt.dll), extracting the AssemblyReloader folder into my GameData directory causes ModuleManager to stall at the following location:

[ModuleManager] ModuleManager env info
Win32NT FFFFFFFFFFFFFFFF
Args:

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ArgumentException: The specified path is not of a legal form (empty).
at System.IO.Path.InsecureGetFullPath (System.String path) [0x00000] in <filename unknown>:0

at System.IO.Path.GetFullPath (System.String path) [0x00000] in <filename unknown>:0

at System.Diagnostics.FileVersionInfo.GetVersionInfo (System.String fileName) [0x00000] in <filename unknown>:0

at ModuleManager.MMPatchLoader.PrePatchInit () [0x00000] in <filename unknown>:0

at ModuleManager.MMPatchLoader+<ProcessPatch>c__Iterator0.MoveNext () [0x00000] in <filename unknown>:0

(Filename: Line: -1)

Note that AssemblyeReloader doesn't actually throw an error, KSP just sits with a stalled loading bar and nothing writes to the log.

Note I alt-tabbed, this is the last thing in the output_log.txt with KSP still running.

After Alt-F4'ing KSP and deleting the AssemblyReloaded folder from my GameData directory, making no other change, KSP starts normally.

Full output_log.txt here: https://drive.google.com/file/d/0B6-5UOjSWq7bQ2FZZ1dTY2p1MFE/view?usp=sharing

Note that I've added the "KSPSTALLSHERE" line to indicate where KSP stalls in the loading process.

To test, I went ahead and renamed my AGExt.dll file to AGExt.reloadable and tried booting KSP, but that caused AssemblyReloader to throw the following error:

Load(Audio): SmartParts/Sounds/buzz

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ART: AGExt: No configuration file found at "H:\1.0.4 Dev\GameData\Diazo\AGExt\AGExt.reloadable.config"; defaults will be used

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ART: AGExt: First-load for plugin Diazo/AGExt/AGExt

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ART: AGExt: No previous assembly handle

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ART: AGExt: Loading plugin AGExt

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ART: AGExt: Loading AGExt without symbols

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ART: AGExt: Checking AGExt for unsupported types...

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ART: AGExt: Unsupported type check complete

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ART: AGExt: Changing plugin identity

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ART: AGExt: Helper type successfully inserted

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ART: AGExt: Creating proxy method for get_CodeBase

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ART: AGExt: Proxy method System.String AssemblyReloaderInjected.Helper::get_CodeBase(System.Reflection.Assembly) created

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ART: AGExt: Creating proxy method for get_Location

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ART: AGExt: Proxy method System.String AssemblyReloaderInjected.Helper::get_Location(System.Reflection.Assembly) created

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ART: AGExt: Replacing KSPAddon attributes with ReloadableAddonAttribute

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ART: AGExt: Replacing KSPAddon attribute on ActionGroupsExtended.AGXMainMenu

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ART: AGExt: Replacing KSPAddon attribute on ActionGroupsExtended.AGXEditor

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ART: AGExt: Replacing KSPAddon attribute on ActionGroupsExtended.AGXFlight

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ART: AGExt: Replacing KSPAddon attribute on ActionGroupsExtended.AGExtMainMenu

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ART: AGExt: Replacing KSPAddon references with ReloadableAddonAttribute references

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ART: AGExt: Exception while loading plugin: System.NullReferenceException: Object reference not set to an instance of an object

at AssemblyReloader.ReloadablePlugin.Weaving.Operations.CommandReplaceKSPAddonWithReloadableAddon.ReplaceKSPAddonReferencesWithReloadableAddon (Mono.Cecil.TypeDefinition type) [0x00000] in <filename unknown>:0

at AssemblyReloader.ReloadablePlugin.Weaving.Operations.CommandReplaceKSPAddonWithReloadableAddon.Execute () [0x00000] in <filename unknown>:0

at strange.extensions.command.impl.CommandBinder.executeCommand (ICommand command) [0x00000] in <filename unknown>:0

at strange.extensions.command.impl.SignalCommandBinder.invokeCommand (System.Type cmd, ICommandBinding binding, System.Object data, Int32 depth) [0x00000] in <filename unknown>:0

at strange.extensions.command.impl.CommandBinder.next (ICommandBinding binding, System.Object data, Int32 depth) [0x00000] in <filename unknown>:0

at strange.extensions.command.impl.CommandBinder.ReleaseCommand (ICommand command) [0x00000] in <filename unknown>:0

at strange.extensions.command.impl.CommandBinder.next (ICommandBinding binding, System.Object data, Int32 depth) [0x00000] in <filename unknown>:0

at strange.extensions.command.impl.CommandBinder.ReleaseCommand (ICommand command) [0x00000] in <filename unknown>:0

at strange.extensions.command.impl.CommandBinder.next (ICommandBinding binding, System.Object data, Int32 depth) [0x00000] in <filename unknown>:0

at strange.extensions.command.impl.CommandBinder.ReleaseCommand (ICommand command) [0x00000] in <filename unknown>:0

at strange.extensions.command.impl.CommandBinder.next (ICommandBinding binding, System.Object data, Int32 depth) [0x00000] in <filename unknown>:0

at strange.extensions.command.impl.CommandBinder.ReactTo (System.Object trigger, System.Object data) [0x00000] in <filename unknown>:0

at (wrapper delegate-invoke) System.Action`2<strange.extensions.signal.api.IBaseSignal, object[]>:invoke_void__this___IBaseSignal_object[] (strange.extensions.signal.api.IBaseSignal,object[])

at strange.extensions.signal.impl.BaseSignal.Dispatch (System.Object[] args) [0x00000] in <filename unknown>:0

at strange.extensions.signal.impl.Signal`1[T].Dispatch (.T type1) [0x00000] in <filename unknown>:0

at AssemblyReloader.ReloadablePlugin.Weaving.WovenRawAssemblyDataFactory.Weave (Mono.Cecil.AssemblyDefinition definition) [0x00000] in <filename unknown>:0

at AssemblyReloader.ReloadablePlugin.Weaving.WovenRawAssemblyDataFactory.Create (ReeperAssemblyLibrary.ReeperAssembly assembly) [0x00000] in <filename unknown>:0

at AssemblyReloader.ReloadablePlugin.Weaving.WriteRawAssemblyDataToDisk.Create (ReeperAssemblyLibrary.ReeperAssembly assembly) [0x00000] in <filename unknown>:0

at ReeperAssemblyLibrary.ReeperAssemblyLoader.LoadAssembly (ReeperAssemblyLibrary.ReeperAssembly assembly) [0x00000] in <filename unknown>:0

at ReeperAssemblyLibrary.ReeperAssemblyLoader.Load (ReeperAssemblyLibrary.ReeperAssembly assembly) [0x00000] in <filename unknown>:0

at AssemblyReloader.ReloadablePlugin.CommandLoadReloadablePlugin.Execute () [0x00000] in <filename unknown>:0

(Filename: C:/buildslave/unity/build/artifacts/StandalonePlayerGenerated/UnityEngineDebug.cpp Line: 56)

ART: AGExt: Plugin loaded successfully!

However, this also hits the ModuleManger stall I linked above so I can't actually test anything as KSP never loads.

This is on a Windows 7 64-bit computer.

Let me know if there is any information I can get you.

D.

Edited by Diazo
Link to comment
Share on other sites

Cool, thanks for trying it out. The issue with ModuleManager is mentioned briefly in the manual but basically it's trying to get a file path of an assembly and because of the way I load them, the field is blank. It's easily fixed inside the MM code though (bolded):

foreach (AssemblyLoader.LoadedAssembly mod in AssemblyLoader.loadedAssemblies)
{
try
{
[B] if (string.IsNullOrEmpty(mod.assembly.Location))
continue;[/B]

FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(mod.assembly.Location);

AssemblyName assemblyName = mod.assembly.GetName();
[snip]

Accessing that same field inside of your reloaded assembly (usually via Assembly.GetExecutingAssembly().Location/CodeBase) is fine since it will be replaced with a call to a method that returns the correct thing, assuming you have the relevant option enabled.

The other issue was a facepalm oversight of mine and is now fixed in 1.0.1 :)

Link to comment
Share on other sites

Okay, just to make sure I'm following you correctly, I need to download the ModuleManger.cs file, make the change you've noted and compile a custom version correct? (I'm pretty sure I've found the exact location I need to make the change already.)

As I use MM to add custom partModules, this is something I need to do before I can proceed any farther with this.

Now, another question about AssemblyLoader. I think I'm okay as I'm referencing another mod, but in my mod this line of code is okay? (referencing the FAR .dll file)

Assembly FarAsm = null;
foreach (AssemblyLoader.LoadedAssembly Asm in AssemblyLoader.loadedAssemblies)
{
if (Asm.dllName == "FerramAerospaceResearch")
{
//Debug.Log("far found");
FarAsm = Asm.assembly;
}
}

As the FAR.dll file is not reloadable, this is okay as I understand the manual included in the .zip file. (The Dependencies/Reference section under Requirements near the start of the .pdf.)

My only concern is that when I hit my own mod (which is reloadable), will this cause issues and break the forEach?

Will test once I'm home from work tonight.

D.

Link to comment
Share on other sites

MM: Yep

Code snippet: Yep also safe. I'm going to have to come up with a better explanation in the manual. Essentially I load the [reloadable] assemblies from memory which prevents them from having an actual Assembly.Location value and causes Assembly.CodeBase on them to point to the loader instead. Inside of your reloadable plugin, I can literally rewrite the call, so something like this:

public class TestMethodInterception : MonoBehaviour
{
private void Awake()
{
print("CodeBase (executing assembly): " + Assembly.GetExecutingAssembly().CodeBase);
print("CodeBase (typeof assembly): " + typeof (TestMethodInterception).Assembly.CodeBase);

print("Location (executing assembly): " + Assembly.GetExecutingAssembly().Location);
print("Location (typeof assembly): " + typeof (TestMethodInterception).Assembly.Location);
}
}

Will be rewritten as (see YourPlugin.reloadable.patched inside your favorite disassembler):

public class TestMethodInterception : MonoBehaviour
{
private void Awake()
{
MonoBehaviour.print("CodeBase (executing assembly): " + Helper.get_CodeBase(System.Reflection.Assembly.GetExecutingAssembly()));
MonoBehaviour.print("CodeBase (typeof assembly): " + Helper.get_CodeBase(typeof(TestMethodInterception).Assembly));
MonoBehaviour.print("Location (executing assembly): " + Helper.get_Location(System.Reflection.Assembly.GetExecutingAssembly()));
MonoBehaviour.print("Location (typeof assembly): " + Helper.get_Location(typeof(TestMethodInterception).Assembly));
}
}

With that helper method being injected as:

internal class Helper
{
public static string get_CodeBase(System.Reflection.Assembly assembly)
{
if (object.ReferenceEquals(System.Reflection.Assembly.GetExecutingAssembly(), assembly))
{
return "file:///[...]/Kerbal Space Program/GameData/TestMethodInterception/TestMethodInterception.reloadable";
}
return assembly.CodeBase;
}

public static string get_Location(System.Reflection.Assembly assembly)
{
if (object.ReferenceEquals(System.Reflection.Assembly.GetExecutingAssembly(), assembly))
{
return "[...]\\Kerbal Space Program\\GameData\\TestMethodInterception\\TestMethodInterception.reloadable";
}
return assembly.Location;
}
}

But for other plugins that have already been loaded, it's already too late and nothing I can do. That's why MM stalls out, an empty Assembly.Location is not a path of legal form

Link to comment
Share on other sites

Alright, with the modified ModuleManager installed, this appears to work as advertised.

I ran out of time tonight and only got some very limited testing in with adding and removing some Debug.Log lines from my KSPAddon classes, but reloading in the game did work and the Debug.Log lines changed as I expected.

Did not get a chance to test PartModule (or other class types) unfortunately.

However, as this does require a modified ModuleManager, I am making my tweak available under MM's CC Share-Alike license.

ModuleManager 2.6.9 AssemblyReloader version available here. (Including Source)

License remains unchanged from Sarbien's ModuleManager version. However, please only use this with AssemblyReloader installed, the tweak is minor but I have not extensively tested it and make no promises it won't do something odd.

Standard MM install, drop the .dll in your GameData directory and make sure it is the only ModuleManager.dll present.

I'll be on my mobile tomorrow, but will not be back at my KSP computer for any further testing until the weekend.

D.

Edited by Diazo
Link to comment
Share on other sites

Wow, I must say I'm certainly liking this mod.

I got in several hours of development work on my own mod today and this mod made testing code changes so much faster.

Do a new build, run a .bat file to copy the new .dll file over to the .reloadable file and hit a button inside KSP and your changes are there.

It does take KSP a few seconds to reload your mod's files, but it is massively faster then the minutes it takes to exit and restart KSP itself.

As an added bonus I gave Sarbian a heads-up about the fact I'd released a derived version of ModuleManager (to handle the empty FileVersionInfo tag) and he's actually incorporated them into the official ModuleManager. Not released yet so you will have to use the modulemanager in my previous post for now, but on a go forwards basis ModuleManager 2.6.12 or newer should work with AssemblyReloader without issue.

Thanks very much for the time saver EvilReeper, I'll be using this on all my mod development work going forward.

D.

Edited by Diazo
Link to comment
Share on other sites

  • 3 weeks later...

Yep, if there are symbols they'll be read as well (Foo.reloadable and Foo.reloadable.mdb for example). I detach, rebuild and reattach on my copy of VS ... not sure if this is required or not. It's not tested on MonoDevelop but I don't see why it wouldn't work there too. Let me know if not

Link to comment
Share on other sites

Cool. I'm using VS2015 so can't help you with MonoDevelop. One more (dumb) question: Since it doesn't appear to integrate with Stock toolbar yet, if you accidentally hit the "X" button is there a way to bring the Assembly Reloader window back up? (Sorry if I missed this in the manual)

Also a suggestion: It would be really slick if we could find a way to automate the build-attach-reload cycle (i.e. so that hitting one button in Visual Studio would cause the following: Build, Debug | Attach Unity Debugger, select the first instance, hit ok, then fire off a "sideband" message to your addin to reload mine). That would speed up the Edit-Test cycle even more!

Edited by Fwiffo
Link to comment
Share on other sites

This thread is quite old. Please consider starting a new thread rather than reviving this one.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...