How can I architect a moddable game targeting JS if Haxe doesn't have runtime loading?

Hey :). A bit of an architecture issue here, so this is going to be a bit long by word count :).

I’m working on a game that I want to allow mods for (from now on referred to as Data Packs).

The mods will be written in haxe and deployed as JS libraries which I want to load inside the game using externs. I would have wanted haxelibs here but Haxe can’t load libs at runtime from what I know.

Because I don’t want to recompile the game engine every time a new Data Pack is added, the game instead refers to a Data Pack List. This CAN be recompiled and is where the Data Packs will be collected. Every time a new Data Pack is added, the idea is that this DataPackList is recompiled and it produces a larger JS file which is then referred to in the game via extern.

The current issue is that I can’t find a way to make Haxe to “unify” the Data Packs JS into the DataPackList JS. Even though DataPackList has an extern to a DataPack, the game crashes when the DataPackList tries to call a method on the DataPack (the DataPack doesn’t exist).

My ideal architecture, in a nutshell, is:

  • the game, “Nexus”, which I seek to NOT recompile for every added mod.
  • an SDK that is a haxelib and does not concern this question (but this is where DataPackBase is defined)
  • a DataPackList project (which CAN be recompiled: the purpose of which being that HERE is where mods are added, as externs).
  • the DataPacks (mods).

The Game Engine will have a static reference to the SDK.
All Data Packs will have to respect the type requirements of the SDK. This also means that mods don’t necessarily have to be written in Haxe. Modders can use JS as long as the data types can be used via the extern mapping.

The reason why JS/Haxe is required by the mods is because complex game-play logic may reside in the mod.

If I understand the question(s?) correctly, what you need is:

  • provide sdk externs for writing mods in haxe/js (also can generate typescript/flow typings so people can use those). you can probably generate those with a macro (or there’s an older --gen-hx-classes that might do the trick as well), or you can write them by hand

  • load the mods at run-time. Haxe doesn’t really handle that because it’s VERY runtime and even project specific. even for js it depends: on node.js you can do require or async loading, or sandboxed loading, etc. for browser i’m sure there are some ways as well (probably just creating <script> tags programatically or something like that). this is not haxe-specific, this is js-specific, so you’ll have to find the way :slight_smile:

  • initialize/activate the mod. I think this works pretty much the same in all software that supports extensions: the mod is a dynamic library that exposes some “init” function. the engine loads the library (a js file in your case), finds that function and calls it, passing some “context” object for the extension to work with, so it can call your Engine/SDK back to actually do stuff.

The last one is easy to implement: you need to compile a js lib from Haxe, exposing a particular function as the init one, like this:

class MyMod {
    @:expose("activate") // expose this static function as a toplevel "activate" function
    @:keep // this is required in current Haxe releases so the function is not removed by DCE
    static function activate(engine:Engine) {
          engine.print("hello");
    }
}

// ...your engine externs
extern class Engine {
	function print(s:String):Void;
}

and then you compile it mostly like a normal haxe/js project, but instead of -main, you specify your entry point class directly in hxml:

-cp src
-js mymod.js
MyMod

This will give you a js file with the activate function available to call from outside. Note however, that that the “outside” where Haxe exposes things currently is the first object that is not undefined from this list:

  • exports
  • window
  • self
  • this

So, depending on your dynamic loading mechanism you might want to provide a temporary exports object from where you’ll get your activate function. Though I’m sure there are js libs that handle this on the browser already. :slight_smile:

1 Like

Hey :). Thank you for the first answer in this topic. I was thinking to expose the entire mod, but your way makes more sense. I realize I forgot an important part in this question, which is the Mod List project. This is required so that any number of mods can be added into the Engine without having to recompile the Engine. I’m updating the question now.

Actually, I’ve made some good progress on this but I’m stuck on loading externs in an extern :). Can you please have a look here? How to include a JS extern in a project that builds another JS extern?

In case you are interested or can find any useful code or ideas, @larsiusprime made this modding framework: GitHub - larsiusprime/polymod: Atomic modding framework for Haxe

1 Like

Thank you @Confidant :). Bookmarked! I’m currently crunching, rushing to determine the feasibility of my game engine / game logic and to start publishing videos / articles about it and see if I can build some traction. This is kind of “my last hurrah” when it comes to game development. Had 2 shots so far but didn’t work as well, and now time is quickly running out, as life and family obligations are catching up to me more and more :). In about 2 years, my spare time will probably be reduced too much to do stuff like this. So… I’m giving it all I got :). If it doesn’t work, I’ll be back when I’m 45 :D.

I eventually achieved what I needed using dynamically loaded scripts specified by my DataPackList JS file, mapped in the game via extern. If you’re interested, see How to include a JS extern in a project that builds another JS extern?

Great! Reading your notes above I also am reminded of Haxe Modular which you may find useful for the multiple libraries part. All the best!

Oh, that is indeed very useful material. If my game looks good enough and advances past beta I’ll probably make good use of this. Thank you again sir :slight_smile:.

Firstly you shouldn’t create multiple questions after you progress but stick to one thread.

Then critically you don’t provide information regarding the runtime context: is it a browser game? A native app embedding a JS runtime? Etc.

In a browser contex the best approach is to:

  • @:expose a registration API (one class with static functions) from your core,
  • games would access your API class as an extern with @:native to resolve to your core’s exposed API.

Hey Philippe :). Sorry for the “double post”, but actually it evolved quite differently. First, I posted this question and then, as I was working on it, 2 days later, I came up with a more specific question where I provided the context regarding what I was doing. Then, as THAT OTHER discussion progressed, I updated this question’s context.

The 2 topics I created are different in nature:

  • this is a discussion about how to architect a moddable game.
  • the other is a discussion about how to use extern.

To answer your question, it’s both a browser game and will probably also be a native app :slight_smile:

Yes, I ended up using exactly that!