Version 4 (modified by jpawlick, 13 years ago) (diff) |
---|
The following is what I've learned about the ioquake3 engine, which I hope will be helpful to you guys while I'm away.
General Terms
Frames
We call an iteration of a game-driving loop a "frame". Because the client and the server are on different machines, client frames and server frames may operate at completely different frequencies. Quagent AI is called every single server frame (unlike normal Quake bot AI). This is unlikely to cost much computationally on a modern machine, since they are far more powerful than machines made in the early 90s.
Server, Client, Interface
TODO: put text here.
The Virtual Machine
TODO: put text here.
Generally, code inside the following folders are available inside the VM:
- game
- cgame
- client
while any code inside the following folders are strictly outside the VM:
- botlib
some code is shared:
- qcommon
- tools?
and the rest I don't know about.
Getting In and Out of the VM
TODO: put text here.
Adding a Trap Function
Edit these files:
- game/g_public.h: add an element to the gameImport_t enum (order matters - try to maintain the values of all the other entries (just add at the bottom)).
- game/g_local.h: add trap function declarations (see bottom of file). These will be the function names inside the VM.
- game/g_syscalls.asm: add equ statements to the file reflecting the (negative) value of the enum entry you added. It should be off by one like all the others are.
- game/g_syscalls.c: add a patch function that calls syscall with the appropriate enum value and parameters, and returns what it returns unless the return is void.
- qcommon/q_shared.h: add trap function declarations (search "Quagents" and keep them together). These will be the function names outside the VM.
- qcommon/common.c: we've defined all our trap functions here, but you probably don't have to. Still, it's a convenient place to do it, so why don't we keep them together and define the function here.
- server/sv_game.c: edit SV_GameSystemCalls and add a case for your enum value to call your function with. If you're passing memory addresses out of the virtual machine, use VMA(x) to remap address in args[x] from VM-memory to Quake-memory. Keep in mind that there's no way to go the other way - you won't be able to see memory outside the VM from within it.
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.
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. For this reason, it is safe to make multiple calls to trap_Com_QuagentsWriteStr, as the batch rangefinder does.
ai_main.c
TODO: put text here.