Tutorial 3 - Hello World

Published 3 years, 3 months ago

In this tutorial you'll learn the basics of AER modding and write and then run your first mod.

Note: This tutorial is for an outdated version of the mod runtime environment.


1 Mod Definition

At the heart of every AER mod is the "mod definition function". This function tells the mod runtime environment what functionality the mod supports. It must be a publicly exported function with one of the following names:

  • define_mod
  • definemod
  • defineMod
  • DefineMod

and it must have the signature:

void DefineMod(AERModDef *def);

This means that it's a function that doesn't return anything and as an argument it takes an instance of the AERModDef struct by reference. This stuct is declared in the MRE header file aer/mod.h. It has 8 members, each of which is a function pointer. These are callback functions that your mod can provide, and the MRE will execute them at the appropriate stage of runtime execution. However, they are not required and are all initialized to NULL.

To help get you started, the MDK provides a bare-bones module called moddef made of the files moddef.h and moddef.c. Go ahead and open them now:

/* moddef.h */
#ifndef MODDEF_H
#define MODDEF_H

#include "aer/mod.h"

/* ----- PUBLIC FUNCTIONS ----- */

void DefineMod(AERModDef *def);

#endif /* MODDEF_H */

/* moddef.c */
#include "moddef.h"
#include "export.h"

/* ----- PUBLIC FUNCTIONS ----- */

MOD_EXPORT void DefineMod(AERModDef *def) { (void)def;

return; }

Notice that moddef.h includes aer/mod.h, thus giving it access to the struct AERModDef that it needs for your mod definition function. It then declares and defines a function called DefineMod which does not modify the AERModDef reference it receives. This means that the MRE will load and unload this mod, but it doesn't actually do anything.

Unless you have an unusual requirement, you shouldn't do anything inside this function other than modify the AERModDef reference. If you do anything more complicated, there's a chance it could cause an error. If you need to initialize mod memory, do so in a dedicated mod constructor function that you pass as constructor.

1.1 Export.h

You may be wondering what the purpose of MOD_EXPORT is before DefineMod's definition. By default, C exposes all functions when it compiles a library's Application Binary Interface (ABI). While you can prefix a function with the static keyword to make it "private", that means that no other modules of the library can use it, either.

Therefore, the MDK's build system was designed to make all functions "internal" by default. By "internal" I mean that a function that doesn't have static or MOD_EXPORT will not be part of the public ABI, but other modules in the same library can still use the function. For a function to be a public part of the ABI, its implementation (not its declaration in the header file) must use the MOD_EXPORT macro which is declared in an automatically generated header file called export.h.

Most mods probably won't need to use this exporting feature for anything other than the mod definition function.

2 Room Change Listener

For now, AERModDef's roomChangeListener callback is a useful function to focus on. It has the signature:

void roomChangeListener(int32_t newRoomIdx, int32_t prevRoomIdx);

When a mod provides this function in its mod definition, the MRE will call it whenever the game changes from one room to another, and it will pass two arguments. The first, newRoomIdx, is the "index" or ID of the new (current) room. The second, prevRoomIdx, is the index of the last room that the game just left.

The MRE header aer/room.h provides an enumeration called AERRoomIndex. This maps room names taken from the HLD executable to their respective room indexes. The reason that room indexes are stored as a signed 32-bit integer rather than an unsigned 32-bit integer is that HLD's engine uses the special room index -1 to represent no room or an invalid room. You'll see this pattern in many other places throughout the framework.

You won't need anything from aer/room.h for the time being, though. Instead, you'll be writing a function that logs an informational message to the console whenever a room change occurs. While it would normally be a good idea to create a new module for this function (since it doesn't really belong in "moddef"), that will be saved for another tutorial. So add a new private function to moddef.c called HandleRoomChange, and give it the signature defined by AERModDef's roomChangeListener callback:

/* moddef.c */
/* Includes... */

/* ----- PRIVATE FUNCTIONS ----- */

static void HandleRoomChange(int32_t newRoomIdx, int32_t prevRoomIdx) {

return; }

/* ----- PUBLIC FUNCTIONS ----- */

/* ... */

The reason you don't need to declare this function in the header moddef.h is because it doesn't need to be included by any external libraries or internal modules. For that same reason, you use the static keyword to force the function to only be visible in the "moddef" module. This means that not other libraries or modules can include this function on their own, but you can still pass a reference to this function for the MRE to call.

2.1 Logging

Now you need a way to log newRoomIdx and prevRoomIdx to the console. Logging is a key part of the AER framework, and you're encouraged to make full use of it whenever you can. Add aer/log.h to moddef.c's includes. This gives the module access to three logging functions: AERLogInfo, AERLogWarn and AERLogErr. It's up to you to decide when to use each of these.

AERLogInfo is probably the function you should use in this situation, so call it from HandleRoomChange and then hover your mouse over it:

From the condensed documentation you can see that AERLogInfo has the exact same signature as stdio.h's printf function. In fact, it works the exact same way, but it wraps your message in a special log format before printing it to the console. You want to print the two integers representing the rooms involved in this room change, so something like this should work:

/* moddef.c */

static void HandleRoomChange(int32_t newRoomIdx, int32_t prevRoomIdx) { AERLogInfo("Changed from room %i to room %i.", prevRoomIdx, newRoomIdx);

return; }

Now you just need to pass a reference to this function in your mod definition ((void)def; was only there to prevent the compiler from warning about an unused parameter):

/* moddef.c */

MOD_EXPORT AERModDef DefineMod(void) { def->roomChangeListener = HandleRoomChange;

return; }

3 Running Your Mod

To see if your mod works, hit F5 to run it. This will also take care of building your mod if you haven't done so already.

Soon you should see HLD launch, but pay more attention to the TERMINAL tab of the output panel. There are a lot of messages to look through, but among them you should find:

[19:17:23][aer][mre] (INFO) Loading mods...
[19:17:23][aer][mre] (INFO) Loading mod "my_mod"...
[19:17:23][aer][mre] (INFO) Successfully loaded mod "my_mod".
[19:17:23][aer][mre] (INFO) Done. Loaded 1 mod(s).
[19:17:23][aer][my_mod] (INFO) Changed from room 0 to room 2.
[19:17:29][aer][my_mod] (INFO) Changed from room 2 to room 3.
[19:17:35][aer][my_mod] (INFO) Changed from room 3 to room 4.
[19:17:41][aer][my_mod] (INFO) Changed from room 4 to room 5.

If you cross reference those room numbers with the constants of AERRoomIndex in aer/room.h, you can see that the game went from AER_ROOM__INIT to AER_ROOM_AUTOSAVEMESSAGE to AER_ROOM_CONTROLLER to AER_ROOM_HEARTMACHINE and finally to AER_ROOM_TITLE.

3.1 Skipping Title Screens

Now that you know your mod is working, you can start manipulating the engine's state. For this, you'll need to include aer/room.h in moddef.c. Using the function AERRoomGoto from that module, you can jump to a new room whenever you want. You just have to pass the index of the room you want to go to. So if you modify HandleRoomChange by adding a conditional test and a call to AERRoomGoto:

/* moddef.c */

static void HandleRoomChange(int32_t newRoomIdx, int32_t prevRoomIdx) { AERLogInfo("Changed from room %i to room %i.", prevRoomIdx, newRoomIdx); if (newRoomIdx == AER_ROOM_AUTOSAVEMESSAGE) AERRoomGoto(AER_ROOM_TITLE);

return; }

and launch the game, you'll effectively go straight from the initial room to the main menu--skipping all the title screens in the process.


Note: This tutorial is for an outdated version of the mod runtime environment.

#dev #moddevtut