Writing Modules In Python

From ASSS Wiki
Revision as of 20:52, 7 November 2008 by Initrd.gz (talk | contribs) (Timers)
Jump to: navigation, search

Basic python module

I have tried to comment what is going on in the source. This module demonstrates callbacks, commands and using interfaces.

# demo asss python module
# dec 28 2004 smong

# nearly always use this
from asss import *


# get some interfaces
# see chat.h for where I_CHAT comes from, see other .h files for more (fx:
#  game.h)
chat = get_interface(I_CHAT)


# a callback
# this function is called when a player enters/leaves, see core.h for PA_???
#  constants
def paction(p, action, arena):
    # start indenting
    if action == PA_ENTERARENA:
        # see chat.h for the names of more functions like SendMessage
        chat.SendMessage(p, "hello " + p.name)

# tell asss to call 'paction' when CB_PLAYERACTION is signalled
# see .h files for CB_??? names
cb1 = reg_callback(CB_PLAYERACTION, paction)


# a command
# see cmdman.h for what each parameter does
def c_moo(cmd, params, p, targ):
# help text (?help moo)
    """\
Module: <py> demo
Targets: none
a sample command.
"""
    chat.SendMessage(p, "moo cows")

# tell asss to call 'c_moo' when a player types ?moo
# note: add cmd_moo to conf/groupdef.dir/default so players have permission to
#  use this command.
cmd1 = add_command("moo", c_moo)

# setting chat (or other interfaces), cb* or cmd* to None is equivalent to
#  unregistering that item.

Save this in bin/demo.py. Then ingame make sure pymod is loaded by using ?lsmod and ?insmod. Then add this module with the following command: ?insmod <py> demo. Re-entering the arena and typing ?moo should do some stuff.

Code snippets

The bread and butter of most custom modules.

Callbacks

Callbacks are triggered when a player does something, like killing another player, or scoring a goal.

from asss import *

chat = get_interface(I_CHAT)

def goal(arena, p, bid, x, y):
    chat.SendArenaMessage(arena, "goal.")

cb1 = reg_callback(CB_GOAL, goal) 

The ref_callback function takes two parameters: An ID for the action, which are constants imported from the asss module and begin with "CB_", and the function to call when the action is triggered. Different IDs take different parameters, and some require you to return a value. This simple module sends the text "goal." to the arena that the player who scored a goal is in.

Commands

Useful for controlling events, fx: ?elim start. In this case the command is ?t1.

from asss import *

chat = get_interface(I_CHAT)

# cmd = the name of the command, fx: elim
# params = any parameters, fx: start
# p = who sent the command
# targ = player or arena
def c_mycmd(cmd, params, p, targ):
    """\
some help text
"""
    if isinstance(targ, ArenaType):
        chat.SendMessage(p, "command sent to public chat")
    elif isinstance(targ, PlayerType):
        chat.SendMessage(p, "command sent as priv msg to %s" % targ.name)

cmd1 = add_command("t1", c_mycmd)
#where the "t1" is located would be the name of the command, ex: ?t1

You can parse integers from the params using the following code:

try:
    val = int(params)
except ValueError:
    # here the conversion of params from a string to an int failed.
    # change the next line to 'pass' to silently ignore the conversion error,
    # 'return' to exit the function, or 'val = 0' to set a default value to val.
    chat.SendMessage(p, "Integer parameter required.")

Manipulating built-in stats (such as kills/points)

This example makes it so team kills don't effect the wins/losses of both players involved.

stats = get_interface(I_STATS)

def kill(arena, killer, killed, bty, flags, pts, green):
    # This checks to see if the killer killed someone on the same team.
    # If so then it will take away a kill from the killer and take away a death from the killed player.
    # The scores are updated right away and the stats appear as if you never died.

    if killer.freq == killed.freq:
        # stats can be incremented...
        stats.IncrementStat(killer, STAT_KILLS, -1)

        # ...or set to an absolute value
        deaths = stats.GetStat(killed, STAT_DEATHS, INTERVAL_RESET)
        stats.SetStat(killed, STAT_DEATHS, INTERVAL_RESET, deaths - 1)

        # SendUpdates must be called so everyone's F2 box shows the correct values
        stats.SendUpdates()
    
    return pts, green

cb1 = reg_callback(CB_KILL, kill)

More STAT_* types can be found in statcodes.h. If you look at stats.h you can see there is also an IncrementStat function

Per-player/arena data

Use this to store game state, player score, etc. Note that the data is deleted after the server goes down or the player logs out.

def shipchange(p, newship, newfreq):
    # prefix mymod_ an abbreviation of your module name to the variable
    #  so that it doesn't clash with other modules. per arena data works
    #  in exactly the same way.
    p.mymod_lastship = p.ship

cb1 = reg_callback(CB_SHIPCHANGE, shipchange)

Persistent per-player data

ASSS has built in mechanisms for saving data per player and per arena across sessions (users logging off or server restarting).

Note: ASSS must be compiled with the berkeleydb option for persistent data to be available.

Since 1.4.2, persistent data functions are stored in classes. The example below is a complete module.

# each player can save a note that only they can see

from asss import *

chat = get_interface(I_CHAT)

# show and store a note
def c_note(cmd, params, p, targ):
    if params:
        p.note = params
    if p.note:
        chat.SendMessage(p, "note: %s" % p.note)
    else:
        chat.SendMessage(p, "no note set")

cmd1 = add_command("note", c_note)

# return the data to save for player p
# returning None means "don't store a record in the database,
# and delete any record that's there already".
class persistent_note:
    key = 1234
    interval = INTERVAL_FOREVER
    scope = PERSISTENT_ALLARENAS

    def getpd(p):
        return p.note

    # restore the data for player p
    def setpd(p, d):
        p.note = d

    # reset/clear the data for player p
    def clearpd(p):
        p.note = None

mypd = reg_player_persistent(persistent_note())

The function reg_player_persistent automatically grabs the key, interval, scope, and functions from the class.

  • The key distinguishes your module's persistent data from others. Ideally you should define a random constant at the top and use it in all your persistent classes. Note that lower numbers are reserved for ASSS's core modules, so to be safe, I recommend that you pick a number over 1,000.
  • The interval is how often the data stored is reset. INTERVAL_FOREVER means that it is never reset, unless you clear it in your code. There are several other constants besides INTERVAL_FOREVER.
  • The scope tells ASSS if the data stored is only for one arena or for the whole zone. PERSISTENT_ALLARENAS means that the data is available everywhere in your zone, while PERSISTENT_GLOBAL means that there is only one copy per zone.

Persistent data is stored in the zone's database. The functions set up the variables after ASSS loads the player's values from the database. You can name these functions whatever you want, but they must be in order: get, set, and clear. I will use getdata(), setdata(), and cleardata().

  • getdata() is called when the player leaves the zone or the zone shuts down. The value returned is what gets stored in the database.
  • setdata() is called after cleardata() when a player enters the zone. It has an argument that the others do not have, and that is what ASSS gets from the database for the player. This is where you assign p.data to whatever you want, more often than not the data ASSS gets from the database.
  • cleardata() is called before setdata() when a player enters the zone. Here, you simply define the names you want to use, along with any defaults.

After the functions automatically run, you can access the data by however you set it in the setdata() or cleardata() functions. Usually this is done with p.data. Once the player logs out, getdata() gives the server the data and the server stores it in the database.

Ideally you should load the module when the server starts. Alternatively if you load it dynamically you can kick everyone (not desirable) or alter the code to catch AttributeError when it attempts to read from the per player data .note.

Here are some of the intervals and their meanings.

  • INTERVAL_FOREVER: I discussed this in the example.
  • INTERVAL_RESET: Every time the server goes through a reset, the data is cleared.
  • INTERVAL_MAPROTATION: Every time the map changes, the data is cleared.

If you define data with these intervals, the data is not stored for the whole zone, rather just the arena the module is attached to.

  • INTERVAL_GAME: After the flag game is over, the data is cleared.
  • INTERVAL_FOREVER_NONSHARED: Same as forever, only that it is not shared with other arenas.

Attach/Detach

Attaching and detaching is similar to load/unload in a C module except it is arena specific. So you can use it to initialise per-arena data.

def mm_attach(arena):
    # do stuff with arena
def mm_detach(arena):
    # undo stuff

Looping over all players

This example counts the number of players in an arena.

def count_players(arena):
    # a list must be used as all other variables are immutable to
    #  nested functions.
    players = [0]
    def cb_count(p):
        if p.arena == arena:
            players[0] = players[0] + 1
    for_each_player(cb_count)
    return players[0]

The for_each_player runs a specified function with the current player as the argument. Since immutable objects cannot be changed inside functions nested inside other functions, you must use a mutable type, such as a list.

Timers

Good for checking if a game is over yet. A reference to the timer is returned and must be retained (you can use per-arena data to store it). Losing the reference will cancel the timer.

initial is the time in 1/100th's of a second before the nested function timer() will be called, you can cancel the timer before it is called. interval is the time gap, again in 1/100th's of a second between all future calls of timer(). The last parameter is a parameter that is sent to the function. You only get one, so if you need more, put them all in a tuple. So make_hello_timer(100, 200, arena) will make it send the arena message "hello" every 2 seconds starting from 1 second after make_hello_timer() was called.

The third argument to set_timer (interval) can be omitted and it will be assumed to be the same as initial.

The parameter arena is needed in this case because SendArenaMessage() requires an arena parameter.

def make_hello_timer(initial, interval, arena):
    def timer():
        # announce
        chat.SendArenaMessage(arena, "hello")
        # non-repeating timer. return 1 for it to be called after the next interval
        return 0
    return set_timer(timer, initial, interval, arena)

def somefunc(arena):
    # create a hello timer that will execute after 1 second, and then every
    #  2 seconds until canceled. timers can cancel themselves, see above.
    myref = make_hello_timer(100, 200, arena)

    # cancel the timer by losing the reference to it
    myref = None

Regions

This is untested but it should go something like this:

mapdata = get_interface(I_MAPDATA)

# regionname is a string, x and y are map tile coords.
def region_contains(arena, regionname, x, y):
    success = 0

    rgn = mapdata.FindRegionByName(arena, regionname)

    if rgn != None and mapdata.Contains(rgn, x, y):
        success = 1

    return success

It is a good idea to cache rgn within per-arena data so you don't add unnecessary load to the server looking it up every time.

You might come across a region callback when browsing the .h files. This is currently (1.4.2) not available in python.

Moving Balls

This is untested but it should go something like this:

balls = get_interface(I_BALLS)

# xy are in tiles
# bid is ball id (0-7, depends how many balls are in the arena)
def move_ball(arena, bid, x, y):
    bd = balldata()
    bd.state = BALL_ONMAP
    bd.x = x * 16
    bd.y = y * 16
    bd.xspeed = bd.yspeed = 0
    bd.carrier = None
    bd.freq = -1
    bd.time = current_ticks()
    balls.PlaceBall(arena, bid, bd)

Targets

Many of the module interfaces use Targets to specifiy which players should be affected by a function call.

# entire arena
tgt = arena

# specific player
tgt = p

# specific freq
tgt = (arena, freq)

# entire zone
tgt = "zone"

# example interface function that requires a tgt
game.WarpTo(tgt, x, y)

Currently (asss 1.4.3) list targets are not supported.

Trouble shooting

If code won't run at all check to see if there is a mix of tabs and spaces used for indenting (SyntaxError: invalid syntax). If so convert all indents to the same type (tabs or spaces). It may not be necessary to do this for the entire module, just in affected functions.

Look at the asss console for execution errors (at the time of writing not all errors are relayed to logged in staff), and if that doesn't help, add some chat.SendArenaMessage(ALLARENAS, "i'm at line ...") type messages to locate the buggy piece of code.