Actions (Programming)
As you've noticed, things don't all happen at once in Hell -- the game keeps track of how long an action takes, and makes sure things happen in order. How does this bit of magic happen? The action queue.
The action queue is simply a list of actions waiting to be performed. What's an action?
{OBJ action, LIST arguments, INT 1, STR command-string}
This is an action spec -- a list of four elements describing an action.
- action - the action object to be performed (a child of $action). Most of these have name properties under $actions (i.e. $actions.attack, $actions.move, etc).
- arguments - a list of arguments describing the action; exact format and meaning is specific to each action object.
- 1 - this argument is currently unused. Always pass 1.
- command-string - a string describing the action being performed. If a player-initiated action, the command typed to start the action. This is used in the {cmd|queue} command output.
The action queue is simply a list of lists, each inner list being an <b>action spec</b> of this form. When you type a command that initiates an action, an action spec of this form is constructed and placed into your queue to be processed.
Queueing actions
Putting the action on your queue is done by calling
actor:queue_action(@action-spec)
the four arguments to :queue_action are simply the action spec as described above. If the actor isn't doing anything else already, the action happens immediately. If she is, the action waits in the actor's .queue property for its turn.
In your code you can cause yourself or other creatures to perform actions simply by calling :queue_action() with a properly constructed action spec. Let's look at an example: wielding a weapon.
#5:"hold wield draw" this none none
1: if (this in player.wielding)
2: player:notify("You're already holding " + this:dname() + ".");
3: elseif (this.location != player)
4: player:notify("You're not carrying " + this:dname() + ".");
5: else
6: if (this.handed > 1 || $skills.quickdraw:check(player, this.size) < 0);
7: player:queue_action($actions.wield, {this}, 1, verb + " " + argstr);
8: else
9: player:queue_action($actions.wield, {this}, 1, verb + " " + argstr);
10: endif
11: endif
This code does a few sanity checks, then gets to the meat -- player:queue_action().
The first argument is $actions.wield -- the wield action. Simple enough.
The second is a list of arguments to the action -- in this case, we know $actions.wield only takes one argument, the item being wielded, so we pass that as the only element of the list. How do we know that? In the worst case we find out by looking at the arguments to the action's :_start verb, but in a perfect world we would find documentation in the Actions List.
The third and fourth arguments are 1 and some semblance of the player's typed command (for queue display purposes).
And that's it! We've queued an action in response to the player typing {cmd|wield <whatever>}, and now the player will wield the item at his next convenience -- probably immediately, but quite possibly later on if he's already got actions going (say, in combat). It might never happen at all (say, if the player eats a grenade between the time he typed {cmd|wield <whatever>} and the time it would have executed). It may sound obvious, but this is very important to keep in mind: you can't assume when, if ever, your queued action will execute.
Action Queue Processing: Under the hood
What actually happens when an action executes?
Actions are executed by actor:process_queue(). This verb is initiated when an action is thrown onto an empty queue (or under certain other conditions we'll see later), and it doesn't stop until the queue is empty. Let's walk through its logic:
- It pulls the next action off the .queue. This is just an action spec, as we described above.
- It calls :forbid_action(action-spec) on everything in the room, including the room itself. If any of those verbs exist and return 1,
the action is skipped entirely; it never had a chance to get started. This forbid_action call is your opportunity as a coder to stop actions from happening under certain circumstances. For instance, it could happen if you're coding a force field that, when active, prevents players from standing up.
- The action's spec is copied into actor.executing, so we know what action we're in the middle of. You can test this property on creatures to see if they're doing something.
- action:_start(OBJ actor, LIST arguments) is called on the action object, where 'actor' is the actor performing the action, and 'arguments'
is the list of arguments the action object expects -- the arguments originally given to us when :queue_action() was called. action:_start() does what it needs to do -- test for sanity, print a start message, etc -- and returns back a two item list of the form
{FLOAT duration, ANY pass-to-finish}
'duration' is simply a duration, in float seconds, to wait before the action finishes. 'pass-to-finish' can be anything -- it's used for any data that needs to be given to the :_finish() verb about the results of starting this actions. For many actions this is nothing. For many others this is simply an INT 1 or 0 telling whether the action succeeded.
- :process_queue() suspends for the number of seconds given back by the :_start() verb in 'duration'. While this task is suspended, any additional :queue_action() calls
on the actor will simply push actions onto the queue, because the actor knows it's already doing something.
- action:_finish(OBJ actor, LIST arguments, ANY pass-to-finish is called. For most actions, the meat of the work happens here. The appropriate time has elapsed and
the action can actually do whatever it is it wanted to do, possibly based on the results forwarded to us by 'pass-to-finish'.
- Now that the action is completely done, .executing is cleared, and we move on to the next action, or die quietly if there are no more actions to process.
Creating your own actions
Generally, anything that would take more than a second or two to do in the real world should be an action. Anything that you shouldn't be able to do instantly in combat should be an action. In fact, almost anything that interacts with or alters the virtual reality in any way should probably be an action. As an example, let's create a rape action.
First, @create a child of $action. Give it a nice short one-word name -- in this case, 'rape'.
Next, write action:_start(). As noted above, this takes the arguments <b>(OBJ actor, LIST arguments)</b> -- you'll need to decide for yourself what arguments your action needs to know about. In this case, we only need one argument -- the rape victim.
<CODE> $actions.rape:_start this none this<BR> rapist = args[1];<BR> </CODE> This is the actor performing the action.
<CODE> victim = args[2][1];<BR> </CODE> This is our one argument -- the victim. Since 'arguments' is a list, we have to pull off the first (and only) element of it.
<CODE> if (rapist.location == victim.location)<BR>
rapist:aat(rapist:dnamec(), " starts raping the hell out of ", victim:dname(), ".");<BR>
</CODE> Notice we have to sanity-check the locations of victim and rapist, because someone might have moved since the time this action was queued. Never underestimate the importance of this sort of checking!
<CODE> return {3.0, 1};<BR> </CODE> Now we return two elements -- the duration of the action, and a pass-to-finish value to pass to :_finish(). In this case we're passing the success of the rape.
<CODE> else<BR>
return 0;<BR>
endif<BR> </CODE> For the failure case, we're returning 0. Note that :_finish() will still get called.
Now we write the :_finish() verb. Same arguments as before, except we also get a third argument -- the pass-to-finish value returned from :_start().
<CODE> $actions.rape:_finish this none this<BR> rapist = args[1];<BR> victim = args[2][1];<BR> passtofinish = args[3];<BR> if (passtofinish)<BR> </CODE> We test passtofinish to see if the :_start() succeeded okay.
<CODE>
rapist:aat(rapist:dnamec(), " pulls up ", rapist.pp, " pants and smiles at ", victim:dname(), ".");<BR>
endif<BR> </CODE>
And we're done! A brand new action.
A <B>very important</B> note about action programming: your action code should never, ever do a suspend(). This will change the task_id of the process_queue in which the action runs, which is poison to the action system. Don't do it. If you need to delay something, use fork.
---+++NPC actions
The next part of this tutorial is probably the part you've been waiting to hear about -- making your NPCs perform actions. Move on to ProgActionsNPC.