Writing Modules In C
This tutorial explains how to write your own custom modules in C. It is assumed you know how to code and want to learn the tools you have availible to you when writing modules.
Contents
Template Module
The easiest way to write a module in C is to work off a template, as certain parts of all modules are identical. Here is a template that does nothing more than connect with the server:
//Template Module #include "asss.h" // Interfaces local Imodman *mm; // The entry point: EXPORT int MM_template(int action, Imodman *mm_, Arena *arena) { int rv = MM_FAIL; // return value if (action == MM_LOAD) { mm = mm_; rv = MM_OK; } else if (action == MM_UNLOAD) { rv = MM_OK; } else if (action == MM_ATTACH) { rv = MM_OK; } else if (action == MM_DETACH) { rv = MM_OK; } return rv; }
This modules loading function is called MM_template, and it is to be saved int template.c. This will make our module name be template when we are interested in loading it into a server. This is a good place to make sure that your module compiles, and that it loads into a server.
Using Interfaces
Interfaces are your way of interacting with what happens in the game. Want to send a chat message to a player? You need an chat interface. Want to warp a player to (512,512)? You need a game interface. Want to toggle a lvz object? You get the idea...
The first thing you need to do is add your interface as a global like variable (We already have one interface, the module manager). Let's make a chat interface, so we can send chat text for the rest of this tutorial. Let's add our interface definition in our interfaces section:
// Interfaces local Imodman *mm; local Ichat *chat;
Now that we have a place to store the interface, let's get the interface. This is done when our function is loading, in the MM_template function. Here's the new MM_template function:
// The entry point: EXPORT int MM_template(int action, Imodman *mm_, Arena *arena) { int rv = MM_FAIL; // return value if (action == MM_LOAD) { mm = mm_; chat = mm->GetInterface(I_CHAT,ALLARENAS); if (!chat) // check interfaces rv = MM_FAIL; else rv = MM_OK; } else if (action == MM_UNLOAD) { mm->ReleaseInterface(chat); rv = MM_OK; } else if (action == MM_ATTACH) { rv = MM_OK; } else if (action == MM_DETACH) { rv = MM_OK; } return rv; }
Here you can see use getting the interface from the module manager, checking if we actually got it (some servers chose to omit certain modules, so you could actually make a server without chat). You must also release the interface when the module unloads. If your module prevents the server from loading, chances are an interface it is using isn't loaded.
You can see exactly what each interface does, and what functions are available by each interface by viewing the appropriate header file. In chat.h we see the defintion for Ichat, which includes a function pointer to a function we'll use:
/* Send a green arena message to a player. */ void (*SendMessage)(Player *p, const char *format, ...) ATTR_FORMAT(printf, 2, 3);
Okay, so where do we send this message? We can't really put it anywhere, since the only place our code has control is when the module is loading or unloading. We want to be able to do something like send the player a message whenever they change ships, or enter an ASSS Region. For something like this we need a callback.
Listening For Callbacks
Callbacks are your way of listening for events. These include, a player entering an arena, changing ship, entering / exiting an ASSS Region. We'll handle a simple one, players changing ships. Whenever a player changes their ship we will message them that they changed ships (using our Ichat interface). Each callback has a callback function, which will be called when an event occurs. We pass a pointer to this function when we declare our callback. Here's the new code:
#include "asss.h" // Interfaces local Imodman *mm; local Ichat *chat; // Functions local void ShipChange(Player *p,int newship, int newfreq) { chat->SendMessage(p,"You have changed ships."); } // The entry point: EXPORT int MM_template(int action, Imodman *mm_, Arena *arena) { int rv = MM_FAIL; // return value if (action == MM_LOAD) { mm = mm_; chat = mm->GetInterface(I_CHAT,ALLARENAS); if (!chat) // check interfaces rv = MM_FAIL; else { mm->RegCallback(CB_SHIPCHANGE, ShipChange, ALLARENAS); rv = MM_OK; } } else if (action == MM_UNLOAD) { mm->UnregCallback(CB_SHIPCHANGE, ShipChange, ALLARENAS); mm->ReleaseInterface(chat); rv = MM_OK; } else if (action == MM_ATTACH) { rv = MM_OK; } else if (action == MM_DETACH) { rv = MM_OK; } return rv; }
To get the function definition for your callback, you only have to look in the appropriate header file. In game.h we see the definition for CB_SHIPCHANGE:
/** this callback is be called when a player changes ship. */ #define CB_SHIPCHANGE "shipchange" /** the type of CB_SHIPCHANGE * @param p the player changing ship/freq * @param newship the ship he's changing to * @param newfreq the freq he's changing to */ typedef void (*ShipChangeFunc)(Player *p, int newship, int newfreq); /* pycb: player, int, int */
If we compile our code now and run it on the server, we should by messaged whenever we change ships.
Declaring Arena / Player Data
It is very often the case that you will want to declare a piece of data for every player in the arena, or for every arena that is running in your zone. This is done in a similar way to registering callbacks or interfaces (telling the server what to do during MM_LOAD and MM_UNLOAD). The only exception is we define a structure for the data you want to hold, so the server will know how much memory to allocate. We will also need a key which we will use to refer to the data. The interface responsible for allocating per player data is Iplayerdata, and the interface responsible for allocating per arena data is Iarenaman. We'll need to get these interfaces before we can do anything with per player or per arena data. All memory is set by the server to 0 whenever a new player enters, or a new arena is formed.
In our example let's have a count of how many times the player has changed ships, and how many times someone has change ship in the arena. First thing we need to do is declare our structures for holding the data, and the key's we'll use to refer them:
typedef struct MyArenaData { int numShipChanges; } MyArenaData; typedef struct MyPlayerData { int numShipChanges; } MyPlayerData; local int arenaKey; local int playerKey;
We'll now need to allocate the data in the MM_template function, and get it (using the key), and increment it in our shipChange function. Here's the code that does all of this:
#include "asss.h" // Interfaces local Imodman *mm; local Ichat *chat; local Iarenaman *aman; local Iplayerdata *pd; // Data typedef struct MyArenaData { int numShipChanges; } MyArenaData; typedef struct MyPlayerData { int numShipChanges; } MyPlayerData; local int arenaKey; local int playerKey; // Functions local void ShipChange(Player *p,int newship, int newfreq) { MyPlayerData* dataPlayer = PPDATA(p, playerKey); MyArenaData *dataArena = P_ARENA_DATA(p->arena, arenaKey); chat->SendMessage(p,"You have changed ships %i times. People in your arena have changed ships %i times." , ++(dataPlayer->numShipChanges), ++(dataArena->numShipChanges) ); } // The entry point: EXPORT int MM_template(int action, Imodman *mm_, Arena *arena) { int rv = MM_FAIL; // return value if (action == MM_LOAD) { mm = mm_; chat = mm->GetInterface(I_CHAT,ALLARENAS); aman = mm->GetInterface(I_ARENAMAN, ALLARENAS); pd = mm->GetInterface(I_PLAYERDATA,ALLARENAS); if (!chat || !aman || !pd) // check interfaces rv = MM_FAIL; else { // allocate data arenaKey = aman->AllocateArenaData(sizeof(MyArenaData)); playerKey = pd->AllocatePlayerData(sizeof(MyPlayerData)); if (!arenaKey || !playerKey) // check if we ran out of memory { rv = MM_FAIL; } else { // declare callbacks, commands, and timers mm->RegCallback(CB_SHIPCHANGE, ShipChange, ALLARENAS); rv = MM_OK; } } } else if (action == MM_UNLOAD) { // unregeister callbacks mm->UnregCallback(CB_SHIPCHANGE, ShipChange, ALLARENAS); // free data we allocated aman->FreeArenaData(arenaKey); pd->FreePlayerData(playerKey); // release interfaces mm->ReleaseInterface(pd); mm->ReleaseInterface(aman); mm->ReleaseInterface(chat); rv = MM_OK; } else if (action == MM_ATTACH) { rv = MM_OK; } else if (action == MM_DETACH) { rv = MM_OK; } return rv; }
If you get a friend to help you out, you'll now notice when you change ships the server will keep track of how many times you've changed it, as well as a seperate count of how often anyone in the same arena has changed ships.
Adding Timers
Timers are functions that you can declare that will be called(possibily repeatedly) in a certain duration of time. They are useful tools in many modules. The balls module, for example, resends the ball packet about every four seconds to every player. The freqowners module has a delay before kicking a player off a team, which is done using a non-repeating timer. In our example let's use a timer to display a message to send a periodic message every 5 minutes (300 seconds). We'll do this when the module loads up, and release the timer when the module unloads. To use timers we'll need the Imainloop interface. To declare a timer we'll use a function from the Imainloop interface:
/** Starts a timed event. * @param func the TimerFunc to call * @param initialdelay how long to wait from now until the first * call (in ticks) * @param interval how long to wait between calls (in ticks) * @param param a closure argument that will get passed to the timer * function * @param key a key that can be used to selectively cancel timers */ void (*SetTimer)(TimerFunc func, int initialdelay, int interval, void *param, void *key);
The TimerFunc is a function that has a declaration as follows:
/** timer functions must be of this type. * @param param is a closure argument * @return true if the timer wants to continue running, false if it * wants to be cancelled */ typedef int (*TimerFunc)(void *param);
So we'll declare a function, periodicMessage, and set it up with an initial delay of 60 seconds after the modules are loaded, and a repeat interval of 600 seconds (5 minutes). In our timerfunc we'll use our chat interface to send a message to all arenas, using a constant ALLARENAS instead of an arena pointer. Here's the finished code:
#include "asss.h" // Interfaces local Imodman *mm; local Ichat *chat; local Iarenaman *aman; local Iplayerdata *pd; local Imainloop *ml; // Data typedef struct MyArenaData { int numShipChanges; } MyArenaData; typedef struct MyPlayerData { int numShipChanges; } MyPlayerData; local int arenaKey; local int playerKey; // Functions local int periodicMessage(void* dummy) { chat->SendArenaMessage(ALLARENAS,"Press F1 for help!"); return TRUE; } local void ShipChange(Player *p,int newship, int newfreq) { MyPlayerData* dataPlayer = PPDATA(p, playerKey); MyArenaData *dataArena = P_ARENA_DATA(p->arena, arenaKey); chat->SendMessage(p,"You have changed ships %i times. People in your arena have changed ships %i times." , ++(dataPlayer->numShipChanges), ++(dataArena->numShipChanges) ); } // The entry point: EXPORT int MM_template(int action, Imodman *mm_, Arena *arena) { int rv = MM_FAIL; // return value if (action == MM_LOAD) { mm = mm_; chat = mm->GetInterface(I_CHAT,ALLARENAS); aman = mm->GetInterface(I_ARENAMAN, ALLARENAS); pd = mm->GetInterface(I_PLAYERDATA,ALLARENAS); ml = mm->GetInterface(I_MAINLOOP,ALLARENAS); if (!chat || !aman || !pd || !ml) // check interfaces rv = MM_FAIL; else { // allocate data arenaKey = aman->AllocateArenaData(sizeof(MyArenaData)); playerKey = pd->AllocatePlayerData(sizeof(MyPlayerData)); if (!arenaKey || !playerKey) // check if we ran out of memory { rv = MM_FAIL; } else { // callbacks mm->RegCallback(CB_SHIPCHANGE, ShipChange, ALLARENAS); // timers ml->SetTimer(periodicMessage, 6000, 30000, NULL, NULL); // times are in centiseconds // commands rv = MM_OK; } } } else if (action == MM_UNLOAD) { // clear timers ml->ClearTimer(periodicMessage, NULL); // unregister callbacks mm->UnregCallback(CB_SHIPCHANGE, ShipChange, ALLARENAS); // free data we allocated aman->FreeArenaData(arenaKey); pd->FreePlayerData(playerKey); // release interfaces mm->ReleaseInterface(ml); mm->ReleaseInterface(pd); mm->ReleaseInterface(aman); mm->ReleaseInterface(chat); rv = MM_OK; } else if (action == MM_ATTACH) { rv = MM_OK; } else if (action == MM_DETACH) { rv = MM_OK; } return rv; }
Adding Commands
Commands can be accessed use ?commandname or *commandname while inside the server. They can perform pretty much anything you want. Let's make a command that will tell us everyone who is currently logged into the zone. To use the command we'll make the user type ?getplayers. The first thing we need to make sure we do is give someone permission to use the command. If you omit this step, no one will be able to use your command. We want any player to be able to use this command, so we'll edit our conf\groupdef.dir\defaut.conf file. At the top of that file (after any #includes) we must add cmd_getplayers and save the file. If we wanted to make this a moderator or sysop only command, we'd put it in mode.conf sysop.conf. Note that sysops can use any commands that smods, mods, and players can use; you only have to give permission to the lowest access level. The cmd_ before our command implies it's a public send command. Some other commands may have privcmd_ before them. These are commands that are messaged to a player (like ?setship). Now that someone has access to our command, we can begin to add it in our code.
Commands are also added and removed when our module loads and unloads. The interface we use to accomplish this is Icmdman. The function definition we need to use for our command function is defined in cmdman.h:
/** the type of command handlers. * @param command the name of the command that was issued * @param params the stuff that the player typed after the command name * @param p the player issuing the command * @param target describes how the command was issued (public, private, * etc.) */ typedef void (*CommandFunc)(const char *command, const char *params, Player *p, const Target *target);
We also are allowed to have a command help text, which will be displayed when the user type ?help <cmdname>. Here's the code that makes our command:
#include "asss.h" // Interfaces local Imodman *mm; local Ichat *chat; local Iarenaman *aman; local Iplayerdata *pd; local Imainloop *ml; local Icmdman *cmd; // Data typedef struct MyArenaData { int numShipChanges; } MyArenaData; typedef struct MyPlayerData { int numShipChanges; } MyPlayerData; local int arenaKey; local int playerKey; // Functions local void Cgetplayers(const char *command, const char *params, Player *p, const Target *target) { chat->SendMessage(p,"You used the ?getplayers command!"); } local int periodicMessage(void* dummy) { chat->SendArenaMessage(ALLARENAS,"Press F1 for help!"); return TRUE; } local void ShipChange(Player *p,int newship, int newfreq) { MyPlayerData* dataPlayer = PPDATA(p, playerKey); MyArenaData *dataArena = P_ARENA_DATA(p->arena, arenaKey); chat->SendMessage(p,"You have changed ships %i times. People in your arena have changed ships %i times." , ++(dataPlayer->numShipChanges), ++(dataArena->numShipChanges) ); } // Command Help Text local const char* getplayers_help = "Module: template\nTargets: none\nArgs: none\nExample: ?getplayers" "\nDisplays every player in the zone."; // The entry point: EXPORT int MM_template(int action, Imodman *mm_, Arena *arena) { int rv = MM_FAIL; // return value if (action == MM_LOAD) { mm = mm_; chat = mm->GetInterface(I_CHAT,ALLARENAS); aman = mm->GetInterface(I_ARENAMAN, ALLARENAS); pd = mm->GetInterface(I_PLAYERDATA,ALLARENAS); ml = mm->GetInterface(I_MAINLOOP,ALLARENAS); cmd = mm->GetInterface(I_CMDMAN,ALLARENAS); if (!chat || !aman || !pd || !ml || !cmd) // check interfaces rv = MM_FAIL; else { // allocate data arenaKey = aman->AllocateArenaData(sizeof(MyArenaData)); playerKey = pd->AllocatePlayerData(sizeof(MyPlayerData)); if (!arenaKey || !playerKey) // check if we ran out of memory { rv = MM_FAIL; } else { // callbacks mm->RegCallback(CB_SHIPCHANGE, ShipChange, ALLARENAS); // timers ml->SetTimer(periodicMessage, 6000, 30000, NULL, NULL); // times are in centiseconds // commands cmd->AddCommand("getplayers", Cgetplayers, ALLARENAS, getplayers_help); rv = MM_OK; } } } else if (action == MM_UNLOAD) { // remove commands cmd->RemoveCommand("getplayers", Cgetplayers, ALLARENAS); // clear timers ml->ClearTimer(periodicMessage, NULL); // unregister callbacks mm->UnregCallback(CB_SHIPCHANGE, ShipChange, ALLARENAS); // free data we allocated aman->FreeArenaData(arenaKey); pd->FreePlayerData(playerKey); // release interfaces mm->ReleaseInterface(cmd); mm->ReleaseInterface(ml); mm->ReleaseInterface(pd); mm->ReleaseInterface(aman); mm->ReleaseInterface(chat); rv = MM_OK; } else if (action == MM_ATTACH) { rv = MM_OK; } else if (action == MM_DETACH) { rv = MM_OK; } return rv; }
Notice our function doesn't really do what it's supposed to. What we need is some sort of way to loop through every player in the zone, and tell the player each one's name. The way to do this is described in the next section.
Iterating Through Every Player / Every Arena
So now we need to loop through every player in the entire zone. The way to do this involves the Iplayerdata interface. Luckly, we already have that interface for use (we used it to make per player data). There is an ASSS macro, FOR_EACH_PLAYER, that will help us loop through every player. To use this macro we need: a Player* and a Link* named link. We also need to lock the pd interface before we loop through it, and unlock it after. If we forget to unlock it, the server will freeze when another module waits for pd to become unlocked (so don't use returns in the middle of your function). Here's our new command function:
local void Cgetplayers(const char *command, const char *params, Player *p, const Target *target) { Player *player; Link *link; chat->SendMessage(p,"These are the players currently in the zone:"); pd->Lock(); FOR_EACH_PLAYER(player) { chat->SendMessage(p,"%s",player->name); } pd->Unlock(); }
In order to loop through every arena we would use a similar macro, FOR_EACH_ARENA. Here's how we could display every arena:
Arena *a; Link *link; chat->SendMessage(p,"These are the arenas currently in the zone:"); aman->Lock(); FOR_EACH_ARENA(a) { chat->SendMessage(p,"%s",a->name); } aman->Unlock();