-1

I am working on a game that I want to have very good mod support. The game is programmed in C++ and will be completely open source. I would like to be able to have users add and remove mods easily. The game will be open source so modders will know how the code looks, but I would like for modders to be able to override functions in the compiled version of the code and to be able to add new functions. I would not like to create an explicit API or use another language for mods as those are necessarily restrictive on what can be done with them. The ideal situation is for the user to download a mod and put it in a mod folder, run the game once to load the mods, and then close it and run it again to load the game with the mods included. How could this be done?

An example would be, that if I had the program:

#include <iostream>

char const* getName();

int main() {
    std::cout << getName() << std::endl;
}

char const* getName()
{
    return "Robert";
}

I would be able to write a mod that has

char const* getName()
{
    return "Steve";
}

in the same namespace, so that the result of the original code would be "Steve".

  • 1
    You can't do what you want the way you want to do it. – drescherjm Aug 16 '22 at 14:42
  • 1
    Unrelated: [Lifetime of a string literal returned by a function](https://stackoverflow.com/questions/2579874/lifetime-of-a-string-literal-returned-by-a-function). – Jason Aug 16 '22 at 14:43
  • This may help: [https://www.boost.org/doc/libs/1_79_0/doc/html/boost_dll.html](https://www.boost.org/doc/libs/1_79_0/doc/html/boost_dll.html) – drescherjm Aug 16 '22 at 14:44
  • 3
    It could be better, instead of asking how to "override function at runtime", ask "how to allow a program accept mods", where you would describe what research did you do, what other programs that "accept mods" have you researched and what design do they have, and explain what exactly the design you want to have. – KamilCuk Aug 16 '22 at 14:44
  • 4
    Topic has multiple problems: loading dll, polymorphism, managing dll-s, API design. – Marek R Aug 16 '22 at 14:45
  • @MarekR The point of this is to not have an API at all, and to just allow modders direct access to the code. – opfromthestart Aug 16 '22 at 14:46
  • 1
    "direct access to the code" *is* an API – Caleth Aug 16 '22 at 14:47
  • 3
    `allow modders direct access to the code` In what way? Let them recompile your program then. (I think you might want to research (java) dependency injection) – KamilCuk Aug 16 '22 at 14:47
  • 1
    The code _is_ the API. If you have some category of functions that can be overridden, that's your mod API. Refusing to call it what it is just guarantees it will be fragile and undocumented. – Useless Aug 16 '22 at 14:47
  • @KamilCuk That isn't quite the problem that I am trying to fix. I would like mods to be able to be loaded without actually downloading a new executable, probably by patching the machine code of the original executable. – opfromthestart Aug 16 '22 at 14:49
  • 4
    Asking mods to patch your compiled program is insane. That's like asking them to write a virus that sneaks a payload into your program and calling it "mod support". – Useless Aug 16 '22 at 14:51
  • 1
    Just asking what is your current skill level? Do you have some experience with more complex code? Did you do some projects with multiple libraries? – Marek R Aug 16 '22 at 14:51
  • Offtopic but whenever dealing with string literals you should have `char const*` – actually your compiler should at least have warned you about... – Aconcagua Aug 16 '22 at 14:52
  • 3
    Programs that edit machine code are usually called a "crack". Mods are usually implemented as dynamically loaded libraries or with scripted languages executed on runtime. Why not implement it that way? How do think firefox or pidgin or gcc implements plugins? – KamilCuk Aug 16 '22 at 14:52
  • @MarekR I am on a team with other people that know more about it than I do, and the problem is pretty far off so we are exploring what options we have. I myself don't have much experience but many people in the team do. – opfromthestart Aug 16 '22 at 14:53
  • Research plugins and factory pattern instead of patching an executable to replace functionality. – drescherjm Aug 16 '22 at 14:53
  • 2
    And forget this hang-up about APIs. By the time you've written a decent size codebase, you'll have plenty of APIs internally just to organize your code, and exposing them is no big deal. – Useless Aug 16 '22 at 14:54
  • 1
    *'I would not like to create an API'* – that's inevitable. The mods need some well defined interface to be able to communicate with the game engine. That can be arbitrarily complex depending on what you want to offer to be able to modify... – Aconcagua Aug 16 '22 at 14:58
  • I would challenge the assumption that mods _must_ be implemented by having them modify the binary code of the original game executable —why assume that something like this isn't an option: using `dlopen()` to load any DSOs in a "mods" folder, then calling some specially-named hook method or object from each loaded DSO to "register" the mod? – saxbophone Aug 16 '22 at 15:01
  • @Useless not necessarily true - it could be a Big Ball of Mud – user253751 Aug 16 '22 at 15:01
  • @KamilCuk I don't believe that either of those methods would allow a mod to override any function it wants to, only those specified by the original code. I would like the mods to be able to run at a similar performance to the original code to allow massive changes that aren't inefficient. – opfromthestart Aug 16 '22 at 15:01
  • @opfromthestart I don't think there's any proper way to do that. It's technically possible at the assembly code level but it's not supported in the C++ language. – user253751 Aug 16 '22 at 15:03
  • @Aconcagua Thats the thing, I would like for a modder to be able to modify anything. – opfromthestart Aug 16 '22 at 15:04
  • @saxbophone I would like for a modder to be able to modify a function regardless of whether or not I originally put a hook in the function. That method works well for additions but it is lacking for overrides. – opfromthestart Aug 16 '22 at 15:05
  • Microsoft developed a system called "hot patching", see also the library called "detours". But you would have to understand the assembly code. – user253751 Aug 16 '22 at 15:08
  • Another way to make your game more moddable is to write most of it in a more moddable language, such as Lua – user253751 Aug 16 '22 at 15:09
  • 2
    If you allow *any* function to be replaced, you might as well just distribute your program in source form – Caleth Aug 16 '22 at 15:09
  • @user253751 that looks similar to what I want, but I am unsure if it is cross platform, as it seems that it only will work for Windows. – opfromthestart Aug 16 '22 at 15:12
  • 2
    *Anything* would mean to provide the entire source code (consider rendering algorithms, timing behaviour, ...). You might provide part of the code as sources – saw [something alike](https://www.moddb.com/games/pirates-of-the-caribbean) already, there was the game engine doing the rendering, timing, and quite a bit more. The game scenarios where provided by source, though, with a pre-defined entry point called by the engine and quite a large interface how the scenario files could tell the engine how to behave. – Aconcagua Aug 16 '22 at 15:12
  • 1
    Be aware, too, that the more you allow to change the more difficult will it get for users to write compatible mods... – Aconcagua Aug 16 '22 at 15:14
  • @Aconcagua the project is open source, so access won't be an issue. – opfromthestart Aug 16 '22 at 15:15
  • 2
    If the entire source is available anyway then mods might be provided in form [patch files](https://www.howtogeek.com/415442/how-to-apply-a-patch-to-a-file-and-create-patches-in-linux/)... – Aconcagua Aug 16 '22 at 15:17
  • @Aconcagua or branches in a vcs – Caleth Aug 16 '22 at 15:18
  • @Aconcagua While it may work, I don't know if having the source code and a compiler ship with the game would be a viable solution. We are already going to have a shader compiler shipping with the game for mod support, but I don't know if the entire program should need to be compiled each time. Also version control may be hard for regular users to set up in order to use mods. – opfromthestart Aug 16 '22 at 15:22
  • An intermediate bytecode system with a assembler from mod bytecode to machine code is being considered so that mod developers don't have to test their mods on multiple platforms, but that doesn't really fix the original problem. – opfromthestart Aug 16 '22 at 15:24
  • @drescherjm do you really expect the average video game player to know how to use a compiler? The point of this is that it would be in the background for the most part, with the user only having to think about "installing mods". – opfromthestart Aug 16 '22 at 15:25
  • 1
    You don't need the user to know how to use a compiler, you can ship "build_my_game_with_mods.exe" that has the whole build process baked in. – Caleth Aug 16 '22 at 15:27
  • @Caleth I was responding to a comment that said that users could use "the compiler of their choice" when I wouldn't want users to need to know about compilers at all. One of the senior devs would like the entire process to take place in one executable, eg. no game launcher or something like that. – opfromthestart Aug 16 '22 at 15:30
  • 1
    However I strongly advise you not to go down this route. Someone is going to write a mod "harvest all the financial details on the host machine and send it to www.badguy.com, then ransomware you", call it "extra_bouncy_titties.mod" and write in the readme that it needs to be run as admin. – Caleth Aug 16 '22 at 15:31
  • @Caleth Why not integrate that in the game engine itself? Some format checking might be provided, maybe some basic tests included, and perhaps a rollback/uninstall mechanism as well... – Aconcagua Aug 16 '22 at 15:37
  • 2
    "... multiple platforms." If you're adding a cross-platform requirement, that's a huge deal that should be described in the question. This has devolved into an exploratory chat about your very first thoughts on "mods" as a concept. There's no answerable question and the requirements are shifting like sand beneath us all. – Useless Aug 16 '22 at 15:44
  • @opfromthestart "That method works well for additions but it is lacking for overrides." —are you sure about this? What about polymorphism with virtual functions? – saxbophone Aug 16 '22 at 16:22
  • @saxbophone That could work, but it seems like it would require the mod to revert what the base game does and then redo it differently to change behavior, which would be inefficient. A way to remove and reimplement methods would be more efficient, but that would lead to having to have a virtual class for each class. I think it could work though. – opfromthestart Aug 16 '22 at 16:33
  • @opfromthestart the thing is that the C and C++ languages don't have this feature, at all, it's impossible in the language - that is why Microsoft made one that works on the assembly code, but the assembly code has different features on different OSes, so theirs only works for Windows. – user253751 Aug 16 '22 at 20:44
  • @opfromthestart I don't think it would require a mod to revert what the base game does and redo it different to change the behaviour, it would give you the option to either completely replace a method (override) or extend the method by overriding it and calling the base game version in the override, either before or after the mod's version. I recommend checking out the instructions for extending Django (Python). The only downside here is that C++ has no `super` keyword, one has to refer to the base by name. This prevents multi-layer polymorphic extension hierarchies. – saxbophone Aug 16 '22 at 21:47

2 Answers2

2

I'm going to assume by "run the game once to load the mods" you mean "have the user go into a menu and indicate the location of a dynamic library".

Rather than a function getName, you'd have a function pointer getName, and some code to set that to some function loaded from the dynamic library.

As a sketch:

struct Library {
    template<typename Signature>
    Signature getSymbol(std::string name) // get the function named name from this dynamic library
}

std::vector<Library> getMods(); // read from some configuration all the mod file paths, and load them into Library objects

const char* defaultGetName()
{
    return "Robert";
}

using Namer = const char * (*)();

Namer getName = defaultGetName;

int main()
{
    for(auto & lib : getMods()) {
        if (auto namer = lib.getSymbol<Namer>("getName")) {
            getName = namer;
        } // etc for other "overridable" functions
    }
    std::cout << getName() << std::endl;
}

The Library parts are going to be platform-specific. Unix has dlopen / dlsym, Windows has LoadLibrary(Ex) / GetProcAddress.

Caleth
  • 52,200
  • 2
  • 44
  • 75
  • I dont know if this is a feasible solution, as it would need for every function in the codebase to follow this pattern. – opfromthestart Aug 16 '22 at 15:16
  • Every function (non-inlined with external linkage) in the codebase is _already_ like this on UNIX - it's just done for you by the linker. In fact, if you mark them weak, then loading a shared object with a stronger version of the same linker symbol will override functions no problem. It's just platform-specific instead of portable. It's _also_ still an API, just one you refuse to document as such. – Useless Aug 16 '22 at 15:50
  • @Useless I was starting to think that op wanted something akin to a linker. TIL weak symbols – Caleth Aug 16 '22 at 15:53
  • I'm no longer convinced OP knows _what_ they want – Useless Aug 16 '22 at 15:55
  • I'm more referring to what you commented as "other overridable functions", which would preferably be every function in the codebase. I don't want to have to specify which functions I want to allow to be overwritten. – opfromthestart Aug 16 '22 at 16:02
0

If you put the whole game (except for the part which loads mods) into a class, and make every function be virtual, the mod can have a derived class which replaces some of the functions. Then you have written a list of replaceable functions, as they are all inside the class and marked virtual.

You can only load one mod at a time this way. Adding new functions or deleting some functions will break all mods - mods will probably only work on the exact game version they were written for.

You'll probably find that some functions don't make sense to be moddable (e.g. calculate the minimum of two numbers) and some functions don't belong in one big class but should be in other classes instead (e.g. Building class if your game has buildings).

user253751
  • 57,427
  • 7
  • 48
  • 90