XStab -- The Gory Details
Table of Contents
I. Preface
I am a UNIX Systems Administrator, not a developer. And while any good administrator is
a master of hacked scripts, sitting down and writing a complex, complete, and expandable
application is not something we usually do. System Administrators (and Network Administrators
for that matter, as the two are generally one and the same) have two primary attributes:
Laziness and Paranoia. I have tried to banish these demons in XStab, but it would be against
my base nature to deny them fully. As such, I'm sure you'll find Laziness in some places
where a more elegant solution is desired. Feel free to patch the code. Paranoia can mostly
be seen in the regular expressions. I've never trusted user input, and I never will.
Despite these warnings, I feel XStab is easy enough to understand for developers to start
writing modules for it, and am looking forward to what will be set forth.
Of course, a project of this depth cannot be created in a void, and there are many who
contributed directly to its creation. First, let me thank LiQUiD_X for politely listening
(or at least politely ignoring) my non-stop talk about XStab, for lending his keen
eye to find those errors which I could not, and for being my primary tester and patiently
helping to track down logic errors.
I need to thank Reaper, for all of the graphics you see on this webpage. When I asked for
graphics help on the oltl.net forums I was expecting pansy-ass crap to be submitted, but
Reaper knows his shit. Thanks for all the purty images :)
To Memnoch, 2Drunk, KillMe, Vlkodlak, Frog, Shinobi, and everyone else I have forgotten:
I would not do this if it weren't for you. I would not spend every fucking night and weekend
coding if I had nobody to enjoy it with. Thanks for your encouragement, support, and patience.
Happy Fragging.
II. Intended Audience
This document is written primarily to those who desire to write new modules for XStab, for
those who want to debug XStab, and for those who simply want to understand how XStab works.
This is NOT a programming tutorial, nor is it a perl tutorial, and it is assumed readers
are familiar with coding in general, and at least know a little perl. To simply run
a released package of XStab suited for a particular game, there is no need to know HOW
XStab works, and instead should consult the documentation for installing that particular
release.
This document is NOT a tutorial, and it is NOT meant as a replacement for reading through
pre-existing module code, as this code should serve as your primary example to creating
new modules. What this document is are my more subtle notes and concepts about XStab, which
can help you understand how everything works together. I make no claim that this documentation
is complete. Any help in cleaning it up, adding to it, and making it better would be
highly appreciated.
If you are writing a module and would like help, have questions, etc, please email me
at dranok@users.sourceforge.net, and I'll see what I can do, or visit http://forums.oltl.net
and go to the XStab forums. I'll do everything I can to help you complete your module.
III. Conventions
I have used a few conventions in the creation of XStab. These will be discussed more
later but for now you should be aware of the following: Methods starting with an _
(underscore) are intended to be private -- of course, perl doesn't particularily
give a swimming hoot about the notions of public or private, but by explicitly specifying
which methods are private in this way it is easier to know which ones are public, and
therefore likely required as part of the API.
Often its useful to pass a string separated by an identifier. This can be passed by
normal methods such as "foo,bar,blah" or "foo:bar:blah", but sometimes this is not
sufficient as foo or bar may contain the separator in them: "foo,bar,bl,ah" would be
split as 'foo', 'bar', 'bl' and 'ah' instead of 'foo', 'bar' and 'blah'. Also, such
a string may be created in one module and passed to a different one, which would then need
to know how to split the string. XStab's answer is to use a global separator string, which
is currently ':x:'. This is represented by $Global{sp}. The forementioned string would
thus be set to "foo$Global{sp}bar$Global{sp}bl,ah". This is messier and less readable
than the previous examples, but has many benefits, and its use is required in the API.
In the root XStab directory there is a script called 'build_config.pl'. This will traverse through
the modules you include from the 'xstab' file, finding which %Global values are used, and
placing them into a config file. If you write a module which requires new config settings, you
need to add these to the hash at the top of build_config (view build_config.pl for more info).
%Players is obviously the main storage hash, which is then subdivided into $Players{ClientID}.
Since XStab is a real-time bot, it is assumed there is some kind of numerical (usually) id
for those players currently connected. This is, for example, the number shown as 'slot' in
a status in quake-like games. This is NOT a player's unique identifier, which is also
required, and is a set field in the config file. For example, RTCW uses GUIDs, which are stored
as $Player{$client}{guid}. Games which do not have a unique ID such as this will have to use
something else, like the player's name, which is much less reliable to track players with.
The client number is simply a convenient method of identifying currently playing records. It is
not 100% necessary to use the clientid in your game module; you could code around the concept
of $Players{$guid} -- however, this requires passing a longer string to any subfunctions (and
'client' is almost always passed), and also increases the risk of running out of ram from non-deleted
but unused $Players{$guid} records (for example, XCore removes hashes when it receives a ClientDisconnect
in the log, but if a player crashes their record won't be deleted). The client id method doesn't
suffer from this problem, since, given a 20 player server, you only have 20 slots, or 'ClientIds', thus
when a new player connects to slot 19, you know you can safely delete the old player record (XCore
does this a different way -- only when the GUID doesn't match the old GUID -- this is because
ClientConnect happens every round, whereas the shrub-specific uConnect only happens once when
a player connects).
The other important thing is constructor names. I don't like using new() as a constructor,
and so have used create(). Since XStab needs to create these objects without knowing anything
about them, it would be ill advised to make your own constructor require any explicit arguments.
Keeping with the name convention and calling your package constructors create() is strongly
advised.
As for variable names, I've never particularly liked any given styles, and since almost
all variables are either part of the %Players hash in XData.pm or local to a package,
there's no true need to enforce naming rules. That said, there are a few hash prefixes
which have special meaning. Hash elements starting with XS_ are XSerialize fields. This
is best illustrated with an example. Take RTCW: $Players{client}{mod_kills}{MOD_BLAH} = 5;
to store this in a key:value way (and make it readable to webpages/whatever needs to
access this), the easiest solution is to convert this into "MOD_BLAH:5,MOD_FOO:3,...".
So XS_mod_kills in this case would mean XSerialize mod_kills. It will make more sense
as you view the current modules
The following applies only to XDbiMySQL, which is most likely the Storage module you will be
using. I place this info here as it should be carried over into any other Storage modules which
are written for sanity.
Like XS_, XA_ means append, which will be covered in the Storage module section. NOTE:
If you do this, your database rows will become HUGE, which is why I don't use this in XCore
or XShrub -- you will need to write some kind of opitimzation script which runs off a cron
to convert this giant list into normal serialization fields. But the support is there, should you want
it.
There is also XI_, which simply means to ignore this field. Fields starting with XI_ should not
be loaded and should not be saved. This is useful when your append field (XA_) is parsed on
a cron and placed in better format into a different column. In this case, XStab doesn't need
to give a flying hoot about this new column, so by prefixing it with XI_ you can tell XStab this
info is irrelevant.
Finally,
hash fields prefixed with r_ are intended to be reset often, and used as short-term counters.
In fact, XCore and XShrub delete all the r_ fields at the end of each round. As you might
have guessed, r_ stands for "round statistic"
IV. Documentation
The real work of XStab is done inside the modules. Wherever possible, modules should be completely
agnostic and not care about what other modules are being used, or aren't being used. There
are, of course, exceptions to this. XStab uses seven types of modules. They are:
- The Game Module -- The brains of the outfit. Determines what stats to collect, and what to do with them.
- The Command Module -- Only used by the game module. Contains code for user/admin commands
- The Sensor Modules -- Used to collect data from various sources
- The Voice Module -- Used to issue commands to the game server (RTCW, Q3, etc)
- The Storage Module -- Used to store/retrieve player records
- The Debug Module -- Used to (DUH!) debug XStab
- The Data Module -- Global storage. This is the one module which should never, ever, ever be changed.
The literal xstab file uses all of these modules with the exception of the command module, which is
specific to the game module. In other words, the 'use XSTAB::GAME::Command.pm' or whatever is in
the game module, not the main module. Nothing other than the game module has use for the command module,
and the only reason to separate it into its own little module is to simplify the coding process. In
practice a game module should always be distributed with its command module. For XCore and XShrub there's
even a simple API between these modules, so that printing text to a server is simplified with the _do_say method,
and adding commands to the voice queue is simplified with the _do_cmd method. More on that later.
As I mentioned, not all modules are purely agnostic. Here is a list of these exceptions:
The command module requires to know about the game module, and the game module needs to know at
least what the command module's API is. For example, XCore uses XCoreCommands, while XShrub
uses XShrubCommands. Someone could write an XShrubCommands_alt, and XShrub wouldn't complain,
as long as the API XShrub uses was followed. These two modules are reliant on each other, as can
be imagined.
The sensor modules are completely agnostic, but the game module can require certain modules to be
present. For example, using RTCW::XCore, both the XTail and XUdp module need to be present, and
any other sensors would be ignored. When you think about it, this makes rational sense -- the sensors
don't need to know what they're sending data to, or even what the data is -- but the game module which
depends on this data better have a fairly good idea of what it is.
The Voice module is completely agnostic, and while the game module assumes it knows what the Voice
module is doing, this isn't always the case. You could swap the XRcon voice module for an XIrc one,
for example, although it would be somewhat amusing seeing "chatclient help" statements being sent
to an IRC channel *grin*.
The Storage module is also completely agnostic, assuming the game module follows a key:value sequence.
While the Debug module has support for nested hashes (up to a point), for simplicity the storage modules
and game modules are advised to keep any data which must be stored/loaded to two levels. The first level
can be stored as a simple key:value, while the second can be stored as key:XS_value (using the XSerialize
method in XData). The game module need know nothing about the storage module. It would be humours to write
XRandom, where stored data is sent to /dev/null, and loaded data is randomly generated.
The Debug module is highly agnostic, and only really cares about the Players hash and the Global hash.
Debug does need one method implimented in the game module (get_status), but other than that, it needs
to know absolutely zilch about the other modules. Likewise, the game module shouldn't even be aware a
debug module exists.
The Data module should never, ever, every be changed. It contains global functions (like do_log), and the
global hashes shared between all the other modules. It is agnostic in the sense that it's simple, stupid, and
only a package because it is easier that way than storing the data in the xstab file itself. You shouldn't
think of the Data module as a module which is swappable, as all it contains are data structures and very
simple functions.
In order to write your own modules for XStab, you simply need to re-create the public methods for the type
of module you wish to write. Click on a module, and view its public methods. Anything else is up to you.
As long as your public methods accept the same arguments, and return in the same method, your module should
work seamlessly with XStab.
IV.0. XStab
IV.i. The Game Modules
The Game Module is the most important aspect of XStab from a user's perspective. Note that due to the style
I coded XCore and XShrub in, deriving classes requires a bit of copy-and-paste (the data hases aren't
inherited). Feel free to design better OOP code when you write your own modules.
IV.ii. The Command Modules
The Command module is ONLY used by the Game Module, if you like you don't need to make this its own
module, and instead only use the Command module. XStab doesn't care. Like XCore and XShrub,
deriving classes requires a bit of copy-and-paste (the data hases aren't
inherited). Feel free to design better OOP code when you write your own modules.
IV.iii. The Sensor Modules
IV.iv. The Voice Module
IV.v. The Storage Module
IV.vi. The Debug Module
IV.vii. The Data Module
V. Notes, Tips, and Caevats
The included XStab modules try to be as efficient as possible, and wherever possible are coded to be fully multiplexing.
Be careful of writing any module which has a chance to sit there blocking, waiting for input, as this will effectively
bring XStab crawling to a stop. You need to remember control gets passed to the game module once every main loop, and
the game module should attempt to return control to XStab ASAP. When you're logged into the debug module, you can see
how many iterations of the main loop per second XStab is achieving. As long as this is above 30ish, you should be OK.
If this drops below 20, the bot will start appearing lagged, and if it drops below 10 something is horribly wrong.
The one module which unfortunately can't be as efficient as the others is the Storage module. The storage module needs to save
the current state of the %Players hash, or it needs to load player data (and basically has to wait here). As most database
servers will be ran local, this probably isn't that big of a deal, but its certainly something to be aware of.
Should the Storage module become an issue, the following advice could be used to speed things up. Loading players could be
interrupted should the process take to long, at which point the %Players{$client} hash should be deleted, thus starting the
load process over again (as if a new player joined). This may become inconvenient. Assuming you have CPU/memory to spare, an easy
solution to save latency would be to fork() before you transfer control to the Storage module. Be careful with this method, and
realize there are reasons I do not do this in the core modules, but instead allow the Storage module to destroy XStab in a worst
case scenario. The alternative, which is what I will run on SmokeHerb, is as follows: The Storage module uses blocking IO (unlike
the rest of XStab), and so the database queries will block indefinately, effectively halting XStab at that point. After a time,
the Storage module will issue the die command, and XStab will terminate. This is its intended behavior. On a server like SmokeHerb,
player records are everything, and insuring absolute integrity is more important than the bots other functions. As such, this method
is acceptable. On a server where player records are less important, this can be changed.
It is not necessarily to use one of every module, if not doing so makes sense. For example, should you not want the extra overhead
of the debug module (not that its very much) You could simply write a Debug module which returns immediately on its public method
of do_one_loop. Making dummy classes as this is prefered to directly editing xstab. Same goes for the storage module -- if you never
intend to save/load player records, feel free to write a stupid class which just returns immediately on all of its public method
calls.
VI. History
I remember the first time I played RTCW. It was the first mptest, and
I couldn't figure out who were Axis and who were Allies. I thought the
game sucked. I wasn't really into FPS games (not that I am now), but
RTCW had a way of growing on me. I bought two copies the day it came
out.
But RTCW had some serious problems, problems commonly known as 'wankers'.
Yes, these were the boys who couldn't actually *play* the game, so instead
took pleasure in killing their teammates. This led more and more servers
to become non-TK servers (aka, no friendly fire), which seriously hampered
the fun of the game, especially in OLTL, which is my prefered game.
The one day I stumbled upon TQC, and a glimmering and awesome concept
was displayed to me. Here I saw one of the earliest versions of WAB, and
I knew immediately what it was -- WAB was a simple script reading
data from the game's log file and sending data to the RCON system. It
was like seeing a windmill for the very first time -- I knew how every
component worked, but putting it together in such a way had never
crossed my mind.
Of course, my mind thought, I could do it far better. So I tried.
And I failed.
Instead of a new bot, a creation known as PotBot briefly entered the
world. Storing data in plain-text files, PotBot was merely a few
extra methods to Wab.pm and an extra depth array to the main Players
hash. I didn't understand WAB too well at that time, and it seemed
WAB didn't understand WAB too well, and PotBot lived up to its name
and turned out utterly confused, forgetful, and -- well, let's just
say it didn't ever actually work right.
We were stupid enough to host the first SmokeHerb.net off of rackspace,
which while a fairly decent colocation company, are NOT meant for
individuals. I gave up on PotBot, and started mindlessly screaming
at any who would utter its name, and indulged myself in EverQuest
instead. SmokeHerb.net was shut down, the dream indefinately on
haitus.
But EverQuest has a way of eating your brain, and when late summer
came I was itching to create something new. I had read several new
books on Perl, and had explored the concets of multiplexing and
non-blocking IO. Thus, when a prick from a small clan named |COOL|
asked me to create a stats system for him, I jumped at the possibility.
From this WStab was born.
WSN was the logical extension to WStab, but by December the entire
system was simply too much to maintain. As I write this WStab
has been out nearly seven months, and no less than two updates
are made per week. The code is a mess. Some of the concepts I had
toyed with simply didn't work right. And while on the whole everyone
likes WStab I knew I could do better. Even as I was exploring
avenues to expand WStab I had started to think of its successor
in the back of my mind.
That which would succeed WStab would be required to be modular,
for it should be able to work with other games besides RTCW,
and it should be expandable for different modes of operation.
It should have a detailed trace method. It would require an entire
new method of thinking about a simple perl bot.
And from this myriad of thoughts, XStab was conceived.