-
-
Notifications
You must be signed in to change notification settings - Fork 120
Heya! I've been using Reloaded's libraries in other projects for a while now, but I'm finally trying out RII (and thus the "full" ecosystem). I wanted to ask why the "Template" folder in mods exists - seems like a bit of a weird decision (at least from my perspective). I wanted to ask some of the reasoning behind it, and share my opinions on it.
- The ModBase system seems to primarily exist for passing some things easier to it, but... why? Creating the Reloaded.Hooks instance is three lines (field for the weakref, calling GetController, calling TryGetTarget). The logger is two lines (just the field and the GetLogger call). This is two entrypoints to conceptualize (one really just being a wrapper around the other).
- The mod(s) I've been working on just implement IMod and scrap all of the boilerplate entirely, taking the .csproj from the template and redoing the rest.
- This seems excessive. The above example I linked can do everything minus the config in a lot less lines. Is there a benefit I'm just blankly missing?
- The config system feels like it shouldn't be implemented per mod by default - perhaps a library someone can take from NuGet to handle config serializing (but skip using/fork/use their own). Also, there's two configs in the ModContext(???).
- Abstracting away the config process in a way you can't change it is a bad idea. From the C# modding framework I hail from, configs are serialized with Newtonsoft.Json and a developer has no way to change it without ignoring the provided API and making their own. This is why I think a library is a good idea - not forced on a developer and easy to modify, but not implemented within each mod.
- Because I didn't use the template for my mod and just started it from scratch, I now have to pull in 340 lines of boilerplate to get a single string configurable. Certainly not great.
- How does the Disposing field work? I don't really understand what the point of this is compared to just implementing IDisposable. Some GC magic I'm missing?
My main issue is with the config system - scrapping everything else for a simple clean slate is easy enough, but now I need to un-scrap half of it to use the provided config system or write my own. From my understanding, the config system also integrates with the Reloaded launcher, so I'd be losing out from making my own.
Despite this, I'm having a lot of fun using Reloaded - thank you for making it! 💜
All reactions
Replies: 3 comments 1 reply
(I'll answer this after I eat dinner, gimme 45-60 mins)
All reactions
Nothing wrong with ever asking a question, haha.
Here's the answer for the first question, rest will follow in additional post since this got big.
The ModBase system seems to primarily exist for passing some things easier to it, but... why?
Older templates for Reloaded-II actually were much simpler and gave you a class that inherited IMod, and had a Start function ( later StartEx). But that did change around 2022 however, yeah.
The reasons here are a bit complicated, so grab tight.
Big Reason: Guiding the User & Upgradability
The biggest reason has to do with trying to steer new users towards the right direction when starting out.
For many people, modding games acts as an introduction to programming.
This in turn means you get a lot of inexperienced people trying to use the tooling.
The template was made to avoid certain possible pitfalls.
Two examples of this being:
- Using
Reloaded.Hooksfrom NuGet instead of the shared instance.- Using non-Shared instance adds ~1MB to mod size and adds ~110ms (on 4790k) to startup time due to JIT overhead.
- That's not good if suddenly every mod has that overhead.
- Mixing code that interacts with the loader with mod code.
- That makes updating mod template harder.
- And also makes reading mod code harder.
To help alleviate issues like this, I modified the template over time to give new users the 'bare essentials' out of the box, and to separate the template code as much as possible from the mod code.
That way, not only is it easier to assist new people with code, but you can actually upgrade the template by just making a new project with the same name and replacing the files in your project folder. The very thin bit of abstraction also enables those simple upgrades.
Outside of that, I figured that if the user is advanced enough, they can simply remove the code they don't want.
It's much easier to remove code than to add code.
Why the Template is Part of the Mod Package
Performance.
When you select an item in the launcher, it has to load the main mod DLL as the mods themselves actually contain the logic for:
- Providing configurable items.
- Note: You can have more than 1 config in a mod!!
- Saving and loading configs.
etc.
Loading the DLL involves I/O, and the launcher also has to load the corresponding thumbnail image (256x256 or greater) at the same time.
Because the mod list can be navigated using the keyboard and controller, a key requirement there was to ensure that holding up/down did not lag the UI whatsoever.
Therefore I couldn't move the template out to another mod as that would cause another (synchronous) DLL load, increasing latency.
When scrolling was smooth on my 2014 4790k Stock CPU, I was happy.
Note: In Reloaded3 the config will be defined in a TOML file. I'll be source generating that so loading the DLL should be entirely skippable.
Upgradability
I touched on this earlier, but being able to upgrade the template in place was another factor.
Any mod I write is technically 1st party and could be interpreted by someone as an example to follow.
Therefore it's important that at least to some degree, there is some consistency.
If someone wanted to try follow along and see how a mod is built, the entry point should be the same.
In the early mods I wrote, small mods would often have their code mixed with the template code (i.e. code in method which implements IMod). When I needed to upgrade the template, things then were not trivial.
The process taking around 5 minutes in itself is not a big deal, however for me, when I have around 30-40 code mods (I lost track at this point), that added up to a lot of time.
Extra
At the time when Reloaded was being written, C# was extremely uncommon for modding native games
built on non-.NET languages. It still is, in fact.
A lot of these things such as Reloaded.Hooks had to be written from scratch.
And there were no existing good guides, libraries etc.
All reactions
Thank you for the quick and detailed answer!
All reactions
The config system feels like it shouldn't be implemented per mod by default - perhaps a library someone can take from NuGet to handle config serializing.
Reason is explained above.
I wanted to avoid additional DLLs if possible as to not increase latency when scrolling in the mod list.
You can throw it in a NuGet if it makes your life easier though.
I'll be doing that in Reloaded3 when config schema will be a separate source generated file.
Also, there's two configs in the ModContext(???).
One of them is a copy of your Config.json obtained from the loader.
This can be handy for some loader API calls such as "which folder is the root of my mod in".
When you're using ReadyToRun (R2R), your code might end up being built in a subfolder.
Therefore if your mod ships additional assets, knowing details such as your ModId and Version without hardcoding or changing it in multiple places can be useful.
Note: This doesn't increase memory use except for 8 bytes to store the field. The copy returned is what the loader itself holds and requires for some API calls.
I now have to pull in 340 lines of boilerplate
Yeah, unfortunately.
The good news is you basically never have to change that boilerplate ever.
I myself don't remember the last time I had to.
Generally only reason you ever would need to is to:
- Add additional configs (add extra config to array)
- Launching a custom binary to handle the config (if you need custom UI).
The actual boilerplate code handles:
- Changing configs in real time.
- Putting the configs in the correct standardized location.
- Loading/Saving of configs.
That boilerplate is also not super tied in with the serializer, so you can easily swap out to another serialization method if desired.
I went with System.Text.Json for performance reasons. The actual loader uses it, and the runtime ships it. Therefore it's already pre-loaded, JITted and ready to go.
Reloaded-II has been very aggressively optimized for startup times, so using Json here made sense, since startup size is negligible and mod size is unaffected (no extra library).
Note: It is possible to share a library between mods via IExports to efficiently share a serializer between mods, but you can't really beat 0 JIT startup overhead, since you can't prevent System.Text.Json from being loaded.
How does the Disposing field work
It's stated in the template.
(Also wow, copy paste error in that file, whoops).
It's an error from the original R2 API in 2019; and I can't really remove it because I can't ever break API.
Mods made day 0 all the way back in 2019 should (and in fact do) work today without any issues.
Basically your entry point inherits from IMod interface. Disposing is part of the IMod interface.
Before calling Unload, the mod loader will call Disposing to let you know you're about to be unloaded.
In practice, this event is redundant as you can either subscribe to ModUnloading in IModLoader (and check if the ModId is yours), or put the code in Unload. It should probably be removed from ModBase in the template. I probably missed it because the rework to get to current template from the previous iteration was 1 big lump of work.
Misc Note:
It's also possible to execute your own binaries to handle configuring mods.
Some mods of mine which require custom UI/Logic (namely binding controllers) do this.
I talked about that a bit here.