[MOD] BaseUtils proof-of-concept for std mod init, mod internal timers and decoupling from original scripts with generic publish/subscribe hooks

+
[MOD] BaseUtils proof-of-concept for std mod init, mod internal timers and decoupling from original scripts with generic publish/subscribe hooks

Hello everybody,

after another mod merge session I wondered if there is a more or less simple way for mod creators to reduce script conflicts with other mods.

It seems many conflicts arise because

  • every mod needs to modify an original script just to get started
  • many mods require some kind of timer so they are embedded in an original script and extend it with additional callback methods
  • some mods change bigger portions of methods inline
  • ...

Obviously all of those reasons increase the number of modified lines and files which increases the chance of merge conflicts. And it's just a pita for users.

So I invested the last couple of days to try some ideas that popped into my head. One thing led to another and before it got out of control (meaning my spare-time) I packaged some of the seemingly more useful things to a basis for easier mod creation. In addition I ported a couple of small mods/tweaks as test examples. I'm sure this topic came up already and I am late to the party (especially since the modding cooled down) but anyway here's something that someone might find useful...

Now before I bore you to death with the details a disclaimer:
I did not dive too deep into the partly messy w3 script code (>14k LoC in one file? really?) and I have no modding experience at all. There are surely many things that I missed or just didn't understand properly. Therefore this is only a proof of concept which hopefully can be improved by you, dear reader :)

Nevertheless I decorated the code with TODOs here and there, basically some random ideas/thoughts which may or may not make sense after all.

The details...

1. Standardized mod initialization


Haven't solved this one completely. But it's simplified. Really. A new Mod needs to

  • extend from a provided BaseClass
  • overwrite its init method and
  • provide a global construct function (one liner)

A Mod user must add one codeline to a provided central script file. Yeah, this sucks, but all mods based on this go into the same file. So it's manageable with virtually no merge conflicts.

Mod will be created and initialized on game started/loaded. And the base class provides a logger and some other goodies.

Short example (longer examples in attachment):

PHP:
ModExampleTemplate.ws
// ----------------------------------------------------------------------------
class CModExampleTemplate extends CModBase {
    default modName = 'ExampleTemplate';

    public function init() {}
    public function onGameStarted() {}
    public function destroy() {}
}
function modCreate_ExampleTemplate() : CModExampleTemplate {
    return new CModExampleTemplate in theGame.MOD_Registry;
}
// ----------------------------------------------------------------------------

modBaseUtils/content/scripts/modRegistry.ws:
// ----------------------------------------------------------------------------
class CModRegistry extends CModFactory  {
    protected function createMods() {
        // add mod creation calls here, like this:
        //
        // add(modCreate_<ModName>());
        // ...

        add(modCreate_ExampleTemplate());
    }
}
// ----------------------------------------------------------------------------

2. Mod timers (realtime and gametime)


It works but it's ugly. Basically it's a standardized way to attach new timers (realtime and gametime) to the player object which then calls a generic dispatcher which calls the dispatcher of the mod which calls the appropriate method. The ugly parts are mostly invisible for mod creators but as long as there are no function parameters or somebody figures out something else (I'm looking at you ScheduleTimeEvent function) the wrapping of context and method is the main culprit.

This should work (I hope) as long as the original scripts do not overwrite timers of the player. The good news is: the implementation is hidden from the user (=modauthor) so if anyone finds a better hack it "should be easy" to exchange without adapting mods.

Usageexample is more verbose, see the ported modAlwaysFinish example (originally by skacikpl) but here are the most important lines:

PHP:
class CModExampleTemplate extends CModBase {
    private var timer: CModGameTimeTimer;

    public _dispatch(id: name, optional payload) {
        switch (id) {
            // id and method named equally by convention
            case 'onTime': this.onTime(); break;
        }
    }

    private function onTime() {}

    public function init() {
        timer = new CModGameTimeTimer in this;
        timer.init(this.modName, modCallback(this, 'onTime'), true);
    }
    
    prublic function onGameStarted() {
        // called hourly starting in one gametime hour
        timer.start(GameTimeCreate(0,1,0,0));
    }
}

I have no clue how good this will work in practice besides my tests. Obviously there is some overhead involved but those are no high frequency timers anyway...

3. Generic hook subscription and custom events

This part is more complex. It's basically an additional publish/subscribe pattern/messaging system. It allows to define standardized hooks in the original code without knowing which/if any mod will listen to these events.

Authors of other mods don't need to touch the original code (in fact they are not allowed to anymore). They just subscribe to channels/eventnames and handle the msgs and its parameters.

There are some usecases for these hooks that came to mind (ordered by complexity):
  • just notify (=call) some method in one or more mods (onCombatFinished, etc.)
  • as above but provide some info (original paramters or expose some internal data) in parameters to mods (onNpcSpawn which one?)
  • as above but integrate some result from the callbacks (e.g. modify some cost of... something :))
  • as above but skip some or the rest of the after-the-hook method (total or not so total conversion of original method)

The first three are covered by the examples. Though keep in mind, only simple examples. I haven't found a small enough mod in my collection which required something like the fourth usecase. So no example but there is an idea outlined in some comment. But it's tough. Simply because the original method has to be partitioned in some generic way to be well, generically useful.

Channel subscribers are called in the subscription order with the result of the previous subscriber as input thus building a chain. The last subscriber defines the result delivered back to the hooked method. However any subscriber may cut the chain, details in the mentioned comments... This is merely meant to provide a possibility to stop havoc for 50 camera mods chained to the same event. Oh and recursion should be detected, too (I think).

In practice this event msg passing is not restricted to w3 code -> mods. It's possible to pass between mods or even between objects within the same mod. Imagine independent mods which can react on each other events. For example a camera mod could react to some modded perfectStorm-brewing-event by starting with a monumental camera sweep :p If not, well, they work independently without the extras. No hard dependency and thus no problems for users picking just one mod.

The messaging system seems to works but there are some open questions
  • high frequency hooks *may* or *may not* hurt/kill performance, haven't tested
  • many subscribers add overhead, haven't found any shiny hashmaps, so it's just lists (at least it feels like lists)
  • it may be a little overengineered to allow usecases which just don't matter in practice, but hey who cares :)
  • personally I don't like the usage. It's a little cumbersome (again context+method binding and no generic type for payloads require subclassing to cover every usecase)

I also have not put more hooks than needed by the examples into the original code. It makes only sense to put them in on-demand. Unfortunately all of this means the modified original code must be maintained, extended for other mods and last but not least ported for every new patch. And there must be one version only - or little will be gained... Who's up for the task? :)

All in all this may be overly ambitious because I have no idea how/if this works/scales with multiple mods, multi threading or sufficiently at all. So maybe this was just a long shot - which missed :)

Examples

As mentioned above I ported some small mods as examples. They seem to work but I didn't test too much. It was just curious how difficult it is to port existing mods. Granted all those mods are minimal but I picked them more or less randomly from my mod collection :)

I also looked at the SOTR beta package and skimmed over the code changes but didn't even try to start porting it. Too. Much. Cool. Stuff. Guys... I'm impressed! You *really* got invested! Awesome! Well, I got the impression that many parts of the code could be ported without much technical difficulty, too. But there are also some parts which would require some serious thoughts (basically how to create more or less generic API hooks as to not prefer SOTR :)). But all in all the number of files with bigger changes could be reduced and some aspects of the mod could be splitted to independent parts. IMHO that last part would be great too, because it would be easy to cherrypick what part the user wants (for example roach stash? brilliant! new alchemy? brilliant! different crafting options? nah, std is enough... you get the picture). Yes I know, options for everything is another possibility. But it has its limits when you have to think about all the if statements... trust me :)

To cut a long story short: Have fun :)

P.s: and damn do include some info.txt in your mod with some author info. I had to search for every damn mod again for the porting credits.
 

Attachments

  • modBaseUtils+Examples.zip
    285.8 KB · Views: 58
Last edited:
Top Bottom