wiki:ServerProgrammingQuagents

Version 1 (modified by jpawlick, 13 years ago) (diff)

--

Quagents Behavior

The entry point for an individual Quagent's behavior is the !BotQuagentAI(...) function in game/ai_main.c. Obviously, this already tells you that Quagent AI is running almost entirely in the virtual machine (the only parts that don't are things we trap out of it, such as socket reads, writes, and elementary actions). BotQuagentAI is called between the motion parts of the frame, so usually you have to set up a motion to execute, you can't actually move stuff (or if you do, it can be undone elsewhere!). The main purpose of BotQuagentAI is to (in the following order): check to see if the bot was just set up (and if so, do some initialization, call some built-in functions to get fresh data structures (such as BotUpdateInventory), read any pending commands from the socket and add them to the command queue, attempt to respawn if dead, update some "latent" values, and then execute as many AINodes as possible.

All Quagent behavior runs through an AINode. An AINode is a function assigned to the bs->ainode pointer in bot_state_t, as you can see in the large switch statement.

Generally all our modifications to ai_main.c are at the bottom of that file.

CommandQueue

As you know, the Quagents store their commands into a command queue that is actually an array 1024 commands long. There are two array indexes that are used to point into this array: bs->finalcommand and bs->currentcommand. If finalcommand is ever greater than currentcommand, there are no orders on the queue. When they are equal (pointing to the same command), that command is the only command. Otherwise, finalcommand points at the last command and currentcommand points at the next to be executed. Most commands are atomic, but some will take some duration of time. All manipulation of the queue itself is done in AIExit_Quagents and AIEnter_Quagents in game/ai_quagentsnodes.c.

The queue is an array of "quagent_command_t"s, which are a struct that contains command state information. Because it must be statically sized, it is as large as the state information of the largest command (rb). It has generic spaces for information - by convention, these values are in the same order as ProtocolZero parameters. So for example, if I issue a "n ro 4 180 0", the command for that will have:

command = QCMD_RO
id = 4
valf1 = 180
valf2 = 0

These values are assigned as part of BotAIQuagentsParseCommand's routine. After the command is executed in one frame, the Quagent has rotated a few degrees, and valf1 is updated to be however much is left to go (so, say, valf1 = 166). This way, if the user issues another command that interrupts the rotate, when the rotate eventually resumes it will only rotate the remaining amount. So typically the valf* track parameters, but in some cases they've been used as state machines.

AINodes

All AINodes are functions that takes a bot_state_t* (which is defined in game/ai_main.h) and returns either qfalse or qtrue.

Adding an AINode

You will need to edit 3 files: game/ai_quagentsnodes.c, game/ai_quagentsnodes.h, and game/ai_main.c. First, in game/ai_quagentsnodes.h, declare your new AINode function (stick to the format, please), and make a #define for the opcode. Next, in game/ai_quagentsnodes.c, add the case for your opcode macro to AI_QuagentsOpCode (the first function in that file). This method is used to correctly print returns and response. You will also have to write your actual AINode function in game/ai_quagentsnodes.c. Finally, in game/ai_main.c, you need to add the case for your command in the large switch statement in BotQuagentAI, and then add a case for your node in BotAIQuagentsParseCommand. The cases are currently in alphabetical order by opcode to help the programmer recognize opcode collisions. All the case should do is call AIEnter_Quagents. Copy the first four parameters from any other call (but replace QCMD_?? with your opcode macro). The remainder of the parameters are what should be assigned into the command's val1, valf1, valf2, valf3, and so on slots. You can reference these from your AINode function by saying:

quagent_command_t* cmd=bs->commandqueue+bs->currentcommand;
float valf1=cmd->valf1;
float valf2=cmd->valf2;
//and so on...

in your AINode function. Remember that anything compiled inside the virtual machine must be in (a restricted form of) ANSI C90, so all variable declarations must be at the beginning of compound statements, etc.

Your AINode function should fulfill these requirements:

  • When the behavior is finished, the function should call AIExit_Quagents. The parameters you should pass to this are (1) the botstate, (2) the id of the command (this is cmd->id), (3) the value of your command macro (usually easiest to just do cmd->command), and (4) a string stating the return status ("done" for normal execution, or a meaningful error message (no spaces allowed) otherwise). After exiting, the function should return qfalse so that the next command on the command queue is handled.
  • When the function has run everything it can this frame but the behavior is not finished (has not called AIExit_Quagents), the function must return qtrue, else the function will be called again.
  • All current Quagent-specific AINodes are named AINode_G*, where * is the task name. The G is for quaGent, and is there to prevent collision with Quake's built-in AINodes.

BotQuagentAILatentNodeHelper()

Some Quagent behaviors require updates after the movement portion of the frame has been executed. However, because the user may interrupt the action with another, the functions cannot rely on being called again on the next frame (because a different command may be executed), and there is never a guarantee that they will ever be called again (for example if the user commands a "n fa", all previous interrupted behaviors are never resumed). For some behaviors (usually ones that track progress of movement), this is unacceptable, so part of their functionality is defined in BotQuagentAILatentNodeHelper() in game/ai_main.c. This function is called every AIFrame, and usually updates a progress value for whatever behavior was active in the most recent frame and then clears its memory of which were active.

If you examine the function you'll see that it updates the values pointed to by pointers in bot_state_t. These pointers point into the commandqueue, at elements of a quagent_command_t. Under our design, a quagent command must store all progress and state information into its quagent_command_t. This is because multiple commands of the same type are allowed to be on the queue, so per-type storage won't work, and because the queue has already allocated space for storing their parameters.

Take mb as an example (AINode_GWalkBy is the function). A moveby command attempts to move the bot in a specified direction for a specific distance. However, it is difficult to tell how far exactly the bot will move on a given frame, so what the function does is update a pointer in bot_state, merely request a move, return, and then when BotQuagentAILatentNodeHelper is called on the next AI frame, it figures out how far the bot has moved and decrements the moveby distance by that amount. So on the first frame, the distance may be 50, but on the second, it may be 41, then 22, then 2, then -5. When it becomes negative the bot enters a "backpedaling" scenario, and (if on the ground) teleports back to where it should have moved to.

Thread Safety

Thread safety is not a concern because Quake is single-threaded. If a user sends a command while an AINode is midway through execution, it will not be read until the next AI frame. You are guaranteed that the state of the command queue is constant through the execution of an AINode. (That is, until you return, no function anywhere can interrupt you.) For this reason, it is safe to make multiple calls to trap_Com_QuagentsWriteStr, as the batch rangefinder does.