Skip to content
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

QModManager 4.0 - Migration to a BepInEx plugin #155

Merged
merged 97 commits into from
Jul 27, 2020
Merged

QModManager 4.0 - Migration to a BepInEx plugin #155

merged 97 commits into from
Jul 27, 2020

Conversation

toebeann
Copy link
Contributor

@toebeann toebeann commented Jul 2, 2020

Downloads for Beta Testers

BZ Experimental

QModManager_Setup_7-27-BZ-EXP-31692.zip
SMLHelper_BZ_EXP_31692.zip

SN1 and BZ

QModManager_Setup_7-27-SN1-BZ.zip


Proposal: Migrate to a BepInEx plugin for loading QModManager rather than making permanent patches to the game files.

BepInEx makes use of a doorstop approach for running entry points. It relies on Harmony, Mono.Cecil, MonoMod among others, and from my testing for the past few weeks it appears quite mature. BepInEx is currently used as the primary modding framework for Risk of Rain 2 and Outward, among others.

Advantages

  • If Subnautica/BZ updates, no need to run the installer again, QMods will still work.
  • BepInEx supports Windows, Linux and macOS out of the box.
  • Not permanently patching the game files increases compatibility, including with game-agnostic mods such as Runtime Unity Editor, which is written for BepInEx and shows detailed debugging info in-game - very useful for modders.
  • New version of Harmony! (NOTE: this won't break QMods that are written for older Harmony - more on this later)

Disadvantages

  • Reliance on an external framework for launching QModManager. If it dies, we'd need to go back to permanent patches, or consider implementing a doorstop approach ourselves.

Implementation

This PR is a proof of concept and potential initial release for this proposal. Notably, I've added three projects:

  • QMMLoader - A simple BepInEx plugin that runs QModManager's entry point via QModInstaller.dll.
  • QMMHarmonyShimmer - As BepInEx is built on top of Harmony 2, I wrote a patcher (heavily inspired by IPALoaderX - a BepInEx plugin/patcher combo which loads mods written for IPA in BepInEx), which runs before any BepInEx plugins, identifies QMods written for older versions of Harmony, backs them up and then shims them to use Harmony 2 instead. This way we don't have to worry about multiple Harmony versions running and all of the associated headaches that would bring.
  • UnityAudioFixer - A patcher which takes care of enabling Unity Audio at launch.

QMods can continue to be developed as they were before, bundled up and put in the QMods directory. QModManager takes care of all of their dependencies as it always has.

In my testing I have so far not identified any mods that cause issues, but I'm only one guy and I can't test every QMod that exists. Please see the attached QModManager_Setup.zip at the bottom for a build if you want to give it a whirl without compiling it yourself. Zip includes installer built with Inno Setup, which handles migrating from previous QModManager and installing BepInEx.

Thoughts, concerns? Lemme have it.

@toebeann
Copy link
Contributor Author

toebeann commented Jul 2, 2020

At this time, it seems this method is not viable. The proof of concept attached and developed in this PR does work under some circumstances, but is failing for many mods - specifically mods loading assets like textures or assetbundles.
This is curious, as in my experience with BepInEx in other games, this hasn't been an issue at all.

@PrimeSonic and I attempted to find a workaround for several hours and were unsuccessful. I'm going to investigate the possibility of working with our own doorstop so that we are not using BepInEx, but get most of the same advantages. We won't be able to interop easily with BepInEx plugins, but we should be able to exist alongside many of them without issue, which is still a win.

Closing the PR.

@toebeann toebeann closed this Jul 2, 2020
… use that as our entry point to avoid issues with loading assets, which was previously caused by QMMLoader running patch methods and loading QMods during UnityEngine.CoreModule's Application constructor, which was too early.
@toebeann
Copy link
Contributor Author

toebeann commented Jul 2, 2020

This PR is not dead after all!

It turns out, BepInEx was initialising our plugin using UnityEngine.CoreModule.dll's Application constructor as the entry point, which was too soon for QModManager to be initialising. As such, I tried altering the BepInEx config to set the entry point as GameInput.Awake as per how QModManager patches Assembly-CSharp.dll, and it works as expected. This was an oversight on my part, my apologies for the time lost by @PrimeSonic and myself spent trying to work out where this went wrong.

For now, instead of altering the BepInEx config, I think it is wiser to instead use a Harmony patch on GameInput.Awake to invoke our entry point instead (removing the patch once done). This is because having to ship the BepInEx.cfg with an entry point configured is additional overhead - perhaps down the line their configuration files won't support this method anymore, for example. Additionally, other BepInEx plugins may rely on the Application constructor as being the entry point - or some other method entirely - so changing it could lead to incompatibility issues. We might want to revisit this in the future.

The .zip in the initial comment above outlining the proposal has been updated with a build reflecting these changes.

Re-opening PR! Have fun testing :)

@toebeann toebeann reopened this Jul 2, 2020
@toebeann
Copy link
Contributor Author

Added preliminary BepInEx plugin <-> QMod mod interop.

BepInEx plugins can depend on QMods:

[BepInDependency("SMLHelper")]
[BepInPlugin("MyCoolPlugin", "My Cool Plugin", "1.0")]
class MyBepInExPlugin : BaseUnityPlugin {
    // ...
}

QMods can depend on BepInEx plugins:

"Dependencies": [
    "RuntimeUnityEditor"
]

Works with versioned dependencies too, as well as load after/load before. However, due to the nature of how QMods waits until GameInput.Awake to begin running QMods patch methods while BepInEx by default only waits until the Application constructor, QMods and BepInEx plugins will always be loaded when they are expected, but there are no guarantees that patch methods will run in any specific order - except for the orders guaranteed by BepInEx and QModManager. Ie., a QMod with a LoadBefore on a BepInEx plugin is guaranteed to be loaded before the BepInEx plugin, but the patch methods are not guaranteed to be run before the BepInEx plugin, and vice versa.

Build attached for testing.

QModManager_Setup_7-25.zip

@toebeann
Copy link
Contributor Author

toebeann commented Jul 26, 2020

@toebeann
Copy link
Contributor Author

toebeann commented Jul 26, 2020

BZ Experimental [branch]

QModManager_Setup_7-27-BZ-EXP-31692.zip

SN1 and BZ

QModManager_Setup_7-27-SN1-BZ.zip

At this point I'm calling this done except for bug fixes.

@toebeann toebeann merged commit 97785cc into devQ2 Jul 27, 2020
@toebeann toebeann deleted the qmmloader branch July 27, 2020 04:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants