Difference between revisions of "Writing Modules In C"
m (link to advanced) |
|||
(20 intermediate revisions by 9 users not shown) | |||
Line 1: | Line 1: | ||
− | + | 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. | |
− | |||
− | |||
− | |||
== Template Module == | == Template Module == | ||
+ | The easiest way to write a module in C is to work from a template, as certain parts of all modules are identical. Here is a template that does nothing more than connect with the server: | ||
<pre> | <pre> | ||
//Template Module | //Template Module | ||
− | |||
#include "asss.h" | #include "asss.h" | ||
Line 27: | Line 24: | ||
else if (action == MM_UNLOAD) | else if (action == MM_UNLOAD) | ||
{ | { | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
rv = MM_OK; | rv = MM_OK; | ||
} | } | ||
Line 42: | Line 31: | ||
</pre> | </pre> | ||
− | This modules loading function is called MM_template, and it is to be saved | + | This modules loading function is called MM_template, and it is to be saved in the text file 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. You will need to compile it as a shared library (.so or .dll). Windows users may find they have to include zlib.lib, pthread.lib and util.c into the compilation. Place the resulting file into the bin/ directory and load the module. |
+ | |||
+ | To load the module from from within the game, type the command in this format: ?insmod <so or dll name without the extension removed>:<modulename> | ||
+ | |||
+ | Assuming your dll is called Template.dll, you would type: | ||
+ | |||
+ | ?insmod Template:template | ||
+ | |||
+ | The server will reply with confirmation text: "Module Template:template loaded successfully". You can double check with the command ?lsmod. If you see "template" in the list of loaded modules, the module is loaded. | ||
== Using Interfaces == | == 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 | + | Interfaces are your way of interacting with what happens in the game. Want to send a chat message to a player? You need a reference to the 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... Have a look at the .h files to find out what the interfaces are and what they have to offer. |
− | The first thing you need to do is add | + | The first thing you need to do is add the interface you want as a global variable so we can keep a reference to it throughout the module. (We already have one interface, the module manager). Let's make a chat interface, so we can send various types of chat text for the rest of this tutorial. Let's add our interface definition in our interfaces section: |
<pre> | <pre> | ||
Line 77: | Line 74: | ||
{ | { | ||
mm->ReleaseInterface(chat); | mm->ReleaseInterface(chat); | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
rv = MM_OK; | rv = MM_OK; | ||
} | } | ||
Line 92: | Line 81: | ||
</pre> | </pre> | ||
− | Here you can see | + | Here you can see us getting the interface from the module manager, checking if we actually got it (some servers operators choose to omit certain modules, so it is theoretically possible to run a server without chat). You must also release the interface when the module unloads. If you get module unload errors when shutting down the server you may have forgotten to release an interface. If your module prevents the server from loading, chances are the module an interface belongs to isn't being loaded (fx: aliasdb depends on mysql). |
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: | 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: | ||
− | < | + | <pre> |
− | / | + | /* Send a green arena message to a player. */ |
void (*SendMessage)(Player *p, const char *format, ...) | void (*SendMessage)(Player *p, const char *format, ...) | ||
ATTR_FORMAT(printf, 2, 3); | ATTR_FORMAT(printf, 2, 3); | ||
Line 108: | Line 97: | ||
<pre> | <pre> | ||
+ | #include "asss.h" | ||
+ | |||
// Interfaces | // Interfaces | ||
local Imodman *mm; | local Imodman *mm; | ||
+ | local Ichat *chat; | ||
// Functions | // Functions | ||
− | local void ShipChange(Player *p,int | + | local void ShipChange(Player *p,int newship, int newfreq) |
{ | { | ||
chat->SendMessage(p,"You have changed ships."); | chat->SendMessage(p,"You have changed ships."); | ||
} | } | ||
− | // The entry point: | + | // The entry point: |
− | EXPORT int MM_template(int action, Imodman *mm_, Arena *arena) | + | EXPORT int MM_template(int action, Imodman *mm_, Arena *arena) |
{ | { | ||
int rv = MM_FAIL; // return value | int rv = MM_FAIL; // return value | ||
Line 132: | Line 124: | ||
else | else | ||
{ | { | ||
− | + | mm->RegCallback(CB_SHIPCHANGE, ShipChange, ALLARENAS); | |
rv = MM_OK; | rv = MM_OK; | ||
} | } | ||
Line 142: | Line 134: | ||
rv = MM_OK; | rv = MM_OK; | ||
} | } | ||
− | else if (action == | + | |
− | { | + | return rv; |
+ | } | ||
+ | |||
+ | </pre> | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <pre> | ||
+ | /** 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 */ | ||
+ | </pre> | ||
+ | |||
+ | If we compile our code now and run it on the server, we should be messaged whenever we change ships. | ||
+ | |||
+ | For a list of callbacks and arguments they take, check out the [[Callbacks]] page. | ||
+ | |||
+ | == 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: | ||
+ | |||
+ | <pre> | ||
+ | typedef struct MyArenaData | ||
+ | { | ||
+ | int numShipChanges; | ||
+ | } MyArenaData; | ||
+ | |||
+ | typedef struct MyPlayerData | ||
+ | { | ||
+ | int numShipChanges; | ||
+ | } MyPlayerData; | ||
+ | |||
+ | local int arenaKey; | ||
+ | local int playerKey; | ||
+ | </pre> | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <pre> | ||
+ | #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 | ||
+ | { | ||
+ | // release interfaces if loading failed | ||
+ | mm->ReleaseInterface(pd); | ||
+ | mm->ReleaseInterface(aman); | ||
+ | mm->ReleaseInterface(chat); | ||
+ | rv = MM_FAIL; | ||
+ | } | ||
+ | else | ||
+ | { | ||
+ | // allocate data | ||
+ | arenaKey = aman->AllocateArenaData(sizeof(MyArenaData)); | ||
+ | playerKey = pd->AllocatePlayerData(sizeof(MyPlayerData)); | ||
+ | |||
+ | if (arenaKey == -1 || playerKey == -1) // check if we ran out of memory | ||
+ | { | ||
+ | // release interfaces if loading failed | ||
+ | mm->ReleaseInterface(pd); | ||
+ | mm->ReleaseInterface(aman); | ||
+ | mm->ReleaseInterface(chat); | ||
+ | |||
+ | if (arenaKey != -1) // free data if it was allocated | ||
+ | aman->FreeArenaData(arenaKey); | ||
+ | |||
+ | if (playerKey!= -1) // free data if it was allocated | ||
+ | pd->FreePlayerData(playerKey); | ||
+ | |||
+ | 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; | rv = MM_OK; | ||
} | } | ||
− | + | ||
+ | return rv; | ||
+ | } | ||
+ | </pre> | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <pre> | ||
+ | /** 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); | ||
+ | </pre> | ||
+ | |||
+ | The TimerFunc is a function that has a declaration as follows: | ||
+ | <pre> | ||
+ | /** 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); | ||
+ | </pre> | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <pre> | ||
+ | #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 | ||
+ | { | ||
+ | // release interfaces if loading failed | ||
+ | mm->ReleaseInterface(pd); | ||
+ | mm->ReleaseInterface(aman); | ||
+ | mm->ReleaseInterface(chat); | ||
+ | mm->ReleaseInterface(ml); | ||
+ | |||
+ | rv = MM_FAIL; | ||
+ | } | ||
+ | else | ||
+ | { | ||
+ | // allocate data | ||
+ | arenaKey = aman->AllocateArenaData(sizeof(MyArenaData)); | ||
+ | playerKey = pd->AllocatePlayerData(sizeof(MyPlayerData)); | ||
+ | |||
+ | if (arenaKey == -1 || playerKey == -1) // check if we ran out of memory | ||
+ | { | ||
+ | // release interfaces if loading failed | ||
+ | mm->ReleaseInterface(pd); | ||
+ | mm->ReleaseInterface(aman); | ||
+ | mm->ReleaseInterface(chat); | ||
+ | mm->ReleaseInterface(ml); | ||
+ | |||
+ | if (arenaKey != -1) // free data if it was allocated | ||
+ | aman->FreeArenaData(arenaKey); | ||
+ | |||
+ | if (playerKey!= -1) // free data if it was allocated | ||
+ | pd->FreePlayerData(playerKey); | ||
+ | |||
+ | 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; | rv = MM_OK; | ||
} | } | ||
Line 153: | Line 438: | ||
return rv; | return rv; | ||
} | } | ||
+ | </pre> | ||
+ | == Adding Commands == | ||
+ | |||
+ | Commands can be accessed using ?commandname or *commandname when logged in to 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 a group of players permission to use the command. If you omit this step, no one will be able to use the command. We want any player to be able to use this command, so we'll edit our conf/groupdef.dir/default file. Anywhere in the file add cmd_getplayers, I suggest at the bottom and add a comment saying when you added it (this will help if you upgrade ASSS later). If we wanted to make this a moderator or sysop only command, we'd put it in conf/groupdef.dir/mod or conf/groupdef.dir/sysop respectively. Note that sysops can use the commands that smods, mods, and players can use (because their permissions are #include'd in conf/groupdef.conf, but don't edit that file); you only have to give permission to the lowest access level. The cmd_ prefix means the commmand can be sent publicly, fx: ?getplayers. Prefixing privcmd_ means you can send the command to a single player or a frequency, fx: :red leader:?setfreq 0. Now that the permissions have been set for our new command, we can begin to write the code for it. | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <pre> | ||
+ | /** 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); | ||
</pre> | </pre> | ||
− | + | We are also allowed to have a command help text, which will be displayed when the user types ?help <cmdname>. Here's the code that makes our command: | |
+ | |||
+ | <pre> | ||
+ | #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 | ||
+ | // Note the naming convention of command handlers (Cnameofcommand) | ||
+ | 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 helptext_t getplayers_help = | ||
+ | "Module: template\n" | ||
+ | "Targets: none\n" | ||
+ | "Args: none\n" | ||
+ | "Displays 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 | ||
+ | { | ||
+ | // release interfaces if loading failed | ||
+ | mm->ReleaseInterface(pd); | ||
+ | mm->ReleaseInterface(aman); | ||
+ | mm->ReleaseInterface(chat); | ||
+ | mm->ReleaseInterface(ml); | ||
+ | mm->ReleaseInterface(cmd); | ||
+ | |||
+ | rv = MM_FAIL; | ||
+ | } | ||
+ | else | ||
+ | { | ||
+ | // allocate data | ||
+ | arenaKey = aman->AllocateArenaData(sizeof(MyArenaData)); | ||
+ | playerKey = pd->AllocatePlayerData(sizeof(MyPlayerData)); | ||
+ | |||
+ | if (arenaKey == -1 || playerKey == -1) // check if we ran out of memory | ||
+ | { | ||
+ | // release interfaces if loading failed | ||
+ | mm->ReleaseInterface(pd); | ||
+ | mm->ReleaseInterface(aman); | ||
+ | mm->ReleaseInterface(chat); | ||
+ | mm->ReleaseInterface(ml); | ||
+ | mm->ReleaseInterface(cmd); | ||
+ | |||
+ | if (arenaKey != -1) // free data if it was allocated | ||
+ | aman->FreeArenaData(arenaKey); | ||
+ | |||
+ | if (playerKey!= -1) // free data if it was allocated | ||
+ | pd->FreePlayerData(playerKey); | ||
+ | |||
+ | 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; | ||
+ | } | ||
+ | |||
+ | return rv; | ||
+ | } | ||
+ | </pre> | ||
+ | |||
+ | Notice our function doesn't really do what it's supposed to. What we need is some sort of way to go through every player in the zone, and tell the caller each player's name. The way to do this is described in the next section. | ||
== Iterating Through Every Player / Every Arena == | == 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. Luckily, 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 playerdata (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 return in the middle of your function). Here is our new command function: | |
+ | |||
+ | <pre> | ||
+ | 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(); | ||
+ | } | ||
+ | </pre> | ||
+ | |||
+ | In order to loop through every arena we would use a similar macro, FOR_EACH_ARENA. Here's how we could display every arena: | ||
+ | |||
+ | <pre> | ||
+ | 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(); | ||
− | + | </pre> | |
− | + | When you are ready for something more advanced, read [[Writing Advanced Modules In C]] | |
+ | [[Category:Module]] | ||
+ | [[Category:Guides]] |
Latest revision as of 01:56, 5 March 2010
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 from 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; } return rv; }
This modules loading function is called MM_template, and it is to be saved in the text file 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. You will need to compile it as a shared library (.so or .dll). Windows users may find they have to include zlib.lib, pthread.lib and util.c into the compilation. Place the resulting file into the bin/ directory and load the module.
To load the module from from within the game, type the command in this format: ?insmod <so or dll name without the extension removed>:<modulename>
Assuming your dll is called Template.dll, you would type:
?insmod Template:template
The server will reply with confirmation text: "Module Template:template loaded successfully". You can double check with the command ?lsmod. If you see "template" in the list of loaded modules, the module is loaded.
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 a reference to the 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... Have a look at the .h files to find out what the interfaces are and what they have to offer.
The first thing you need to do is add the interface you want as a global variable so we can keep a reference to it throughout the module. (We already have one interface, the module manager). Let's make a chat interface, so we can send various types of 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; } return rv; }
Here you can see us getting the interface from the module manager, checking if we actually got it (some servers operators choose to omit certain modules, so it is theoretically possible to run a server without chat). You must also release the interface when the module unloads. If you get module unload errors when shutting down the server you may have forgotten to release an interface. If your module prevents the server from loading, chances are the module an interface belongs to isn't being loaded (fx: aliasdb depends on mysql).
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; } 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 be messaged whenever we change ships.
For a list of callbacks and arguments they take, check out the Callbacks page.
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 { // release interfaces if loading failed mm->ReleaseInterface(pd); mm->ReleaseInterface(aman); mm->ReleaseInterface(chat); rv = MM_FAIL; } else { // allocate data arenaKey = aman->AllocateArenaData(sizeof(MyArenaData)); playerKey = pd->AllocatePlayerData(sizeof(MyPlayerData)); if (arenaKey == -1 || playerKey == -1) // check if we ran out of memory { // release interfaces if loading failed mm->ReleaseInterface(pd); mm->ReleaseInterface(aman); mm->ReleaseInterface(chat); if (arenaKey != -1) // free data if it was allocated aman->FreeArenaData(arenaKey); if (playerKey!= -1) // free data if it was allocated pd->FreePlayerData(playerKey); 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; } 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 { // release interfaces if loading failed mm->ReleaseInterface(pd); mm->ReleaseInterface(aman); mm->ReleaseInterface(chat); mm->ReleaseInterface(ml); rv = MM_FAIL; } else { // allocate data arenaKey = aman->AllocateArenaData(sizeof(MyArenaData)); playerKey = pd->AllocatePlayerData(sizeof(MyPlayerData)); if (arenaKey == -1 || playerKey == -1) // check if we ran out of memory { // release interfaces if loading failed mm->ReleaseInterface(pd); mm->ReleaseInterface(aman); mm->ReleaseInterface(chat); mm->ReleaseInterface(ml); if (arenaKey != -1) // free data if it was allocated aman->FreeArenaData(arenaKey); if (playerKey!= -1) // free data if it was allocated pd->FreePlayerData(playerKey); 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; } return rv; }
Adding Commands
Commands can be accessed using ?commandname or *commandname when logged in to 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 a group of players permission to use the command. If you omit this step, no one will be able to use the command. We want any player to be able to use this command, so we'll edit our conf/groupdef.dir/default file. Anywhere in the file add cmd_getplayers, I suggest at the bottom and add a comment saying when you added it (this will help if you upgrade ASSS later). If we wanted to make this a moderator or sysop only command, we'd put it in conf/groupdef.dir/mod or conf/groupdef.dir/sysop respectively. Note that sysops can use the commands that smods, mods, and players can use (because their permissions are #include'd in conf/groupdef.conf, but don't edit that file); you only have to give permission to the lowest access level. The cmd_ prefix means the commmand can be sent publicly, fx: ?getplayers. Prefixing privcmd_ means you can send the command to a single player or a frequency, fx: :red leader:?setfreq 0. Now that the permissions have been set for our new command, we can begin to write the code for it.
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 are also allowed to have a command help text, which will be displayed when the user types ?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 // Note the naming convention of command handlers (Cnameofcommand) 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 helptext_t getplayers_help = "Module: template\n" "Targets: none\n" "Args: none\n" "Displays 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 { // release interfaces if loading failed mm->ReleaseInterface(pd); mm->ReleaseInterface(aman); mm->ReleaseInterface(chat); mm->ReleaseInterface(ml); mm->ReleaseInterface(cmd); rv = MM_FAIL; } else { // allocate data arenaKey = aman->AllocateArenaData(sizeof(MyArenaData)); playerKey = pd->AllocatePlayerData(sizeof(MyPlayerData)); if (arenaKey == -1 || playerKey == -1) // check if we ran out of memory { // release interfaces if loading failed mm->ReleaseInterface(pd); mm->ReleaseInterface(aman); mm->ReleaseInterface(chat); mm->ReleaseInterface(ml); mm->ReleaseInterface(cmd); if (arenaKey != -1) // free data if it was allocated aman->FreeArenaData(arenaKey); if (playerKey!= -1) // free data if it was allocated pd->FreePlayerData(playerKey); 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; } return rv; }
Notice our function doesn't really do what it's supposed to. What we need is some sort of way to go through every player in the zone, and tell the caller each player'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. Luckily, 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 playerdata (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 return in the middle of your function). Here is 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();
When you are ready for something more advanced, read Writing Advanced Modules In C