Difference between revisions of "Writing Modules In C"

From ASSS Wiki
Jump to: navigation, search
Line 630: Line 630:
 
</pre>
 
</pre>
  
[[Category: Module]]
+
[[Category:Module]]
[[Category: Tutorial]]
+
[[Category:Guides]]

Revision as of 09:31, 9 May 2005

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

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;
	}
	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 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 from within the server type ?insmod template, the server will reply with confirmation text and you can double check with the command ?lsmod.

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;
	}
	else if (action == MM_ATTACH)
	{		
		rv = MM_OK;
	}
	else if (action == MM_DETACH)
	{
		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;
	}
	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 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
			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 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();