Open Albeoris opened 5 months ago
I wasn't feeling well late yesterday, so sorry for the delay. Happened to forget about this too, in the morning.
Anyway, yeah, you pretty much got the jist of what it takes to load .NET Code in a native process from scratch. Simplifying this process is one of the reasons Reloaded-II exists, even though it has far outgrown its original purpose. Some notes below.
Reloaded.Injector
can be made to support the use case, I in fact might have even added that patch to the 2.0.0 branch, but I did deprecate it for the Rust WIP library.
To get your code running in the original process, you also can use a stub DLL, a.k.a. the ASI Loader approach. A third method for some super rare cases is hard patching the EXE to add a loader dependency.
Although I don't like modifying the game folder at all, they are a necessary evil for the 'it just works' experience, as some end users, may for example rely on Steam Input for controller rebinding, which requires the game to be launched by Steam.
There's also some edge cases once you actually get your loader inside the game. One example is Steam DRM Wrapper. You have to delay loading mods because game may be encrypted on boot. Another is forced game reboot by Steam, if not launched by Steam; in which case your DLL Injected code is lost.
There's also a bunch of extra things to do inside your loader too. For example, AssemblyLoadContexts, to isolate mods so they can use their own dependencies. Spawning a console, creating dumps on crashes, etc.
It's all about delivering the mod to the end player: I would like to have one .exe file or .zip archive that needs to be launched/unpacked into the game, after which launching the game on Steam will launch a modified version of the game without performing any additional actions.
You might find it easier to just deploy a portable version of Reloaded-II. For example, the Persona CEP is doing this. I've done something like this for a friend before.
Here's the source code and files for a self-contained installer that deploys Reloaded-II in a portable fashion to FlatOut2. You should be able to get this working for your purposes without requiring major source code changes.
Over there, I faked a bit of the process, as I actually included repacked game files for better load times. Of course, that's normally a no-no, but this wasn't a public release.
Anyway, you should be able to get this adjusted for your game with the following steps
.Reloaded-II/Apps
1.1. With the mods you want enabled already.
1.2. Note you can set a relative path; e.g. "AppLocation": "../../../FlatOut2.exe",
1.3. Pre-enable any mods you want enabled in the App config too.
1.4. Set DontInject
: true
. This is a recently added flag that launches an EXE without DLL Injection from launcher. This lets the game to reboot from Steam, so Steam Input works, and ASI Loader will kick in to load your code..Reloaded-II/Mods
Program.cs
in installer to instead install Reloaded-II
to user's selected game folder.
3.1 Replace all mentions of FlatOut 2 FOJ Edition
with full path to user's game folder.Reloaded.Mod.Loader.Bootstrapper.asi
and whatever DLL ASI Loader spills out to game folder.
4.3 Launcher start script is just Run Mod Manager.bat
.User would just run Setup.exe
, select their game folder, and the rest would be automated.
End result would look something like:
Reloaded stuff in a subfolder, a .bat
to launch Reloaded, ASI Loader, Reloaded Bootstrapper, and plain game files.
User can open Mod Manager from the .bat
file. Clicking Launch Application
from R2 will be equivalent to just normally running the EXE if DontInject
is set to true. And ASI Loader will handle loading the bootstrapper, and thus R2.
Edge Cases
You must run the Reloaded Launcher after setup, because it needs to update the loader paths in AppData/Roaming/Reloaded-Mod-Loader-II/ReloadedII.json
, otherwise the bootstrapper won't find the loader. Unless you want to write a minimal file yourself, that would work too.
You might want to skip deploying ASI Loader if one's already present. AsiLoaderDeployer.cs from R2's source may help.
Ah, I forgot.
If you use the GameFinder
library, you also will be able to auto locate the user's steam game. So you could in fact fully automate an install.
Brilliant! Thank you very much for the detailed explanation!
No worries :p
which requires the game to be launched by Steam
This is how we solved this problem for FF9: SteamFix
This is how we solved this problem for FF9:
Looks like you're stubbing a binary. Yeah, that's another way to handle it, albeit more game specific.
Hi! This is both a request to improve the documentation and an attempt to verify that my understanding of the process.
Please correct me if I'm wrong. To hook a native call in a C++ game, I need:
Launcher: Write a C# application that will launch the game process in the Suspended state and inject a C++ DLL into it, wait for initialization to complete and resume the process. 1.1. To start a process in the Suspended state, use WinAPI CreateProcess with the appropriate flag. 1.2. To load the library, use WinAPI CreateThread(loadLibraryWPtr, pathPtr) 1.3. To allocate memory in a remote process and pass the path to the loaded library, use Kernel32.VirtualAllocEx 1.4. Reloaded.Injector is not suitable for this task, since it cannot inject itself into processes in the Suspended State. 1.5. To bypass the restrictions of Steam, which may refuse to enable the overlay if the original launcher has been replaced, register the custom launcher registration as Debugger in the Windows registry for the original
.exe
.Bootstrap: Write a native C++ DLL that will be injected by the launcher to the game process and then rise the .NET Runtime. 2.1 x86 for x86 games, x64 for x64 games 2.2. This is a simple example: Reloaded.Core.Bootstrap 2.3. This is a advanced example of DllMain which raises .NET Runtime in a separate thread, thereby avoiding deadlock: Reloaded.Mod.Loader.Bootstrapper
.NET Mod: Write a managed .NET8+ DLL that will be loaded from async thread in DllMain and will use Reloaded.Hooks 3.1 Good example of how to call it from DllMain: Reloaded.Mod.Loader.Bootstrapper 3.2 RTFM: GettingStarted
Am I understanding everything correctly? Or are there already easier ways to do this?
P.S. Just in case, I’ll explain why, with lack of knowledge about the wonderful world of C++, I write a launcher myself, rather than using Reloaded-II:
It's all about delivering the mod to the end player: I would like to have one
.exe
file or.zip
archive that needs to be launched/unpacked into the game, after which launching the game on Steam will launch a modified version of the game without performing any additional actions.At the same time, I want to remain compatible with the latest version of all libraries (which makes it seem like a bad idea to ship Reloaded with the mod in the same archive).
At the same time, I want not to break compatibility with other mods that the player already has installed.
As far as I know, you are currently developing Reloaded-3. Perhaps these comments will give you some ideas.
In my ideal world, Reloaded could package itself and selected mods into a self-contained .exe file, which when run, would check for Reloaded in the game folder. If it exists, the mod will add it to it. If it is not there, it will unpack it from the archive or offer to download a newer version if there is a network. Then it will launch the game. And each subsequent launch through this
.exe
file will launch the game with the mod forcibly enabled (and the rest that are included in Reloaded, if any) without displaying the Reloaded window.