Writing Modules In C

From ASSS Wiki
Revision as of 19:48, 10 January 2005 by 24.195.232.161 (talk) (Iterating Through Every Player / Every Arena)
Jump to: navigation, search

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

//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();