The Command Execution Cycle
by Eric Eve
The command execution cycle in the adv3Lite library is quite complex. This article aims to unpack it step-by-step, both to give game authors a clearer idea of what is happening, and to indicate places where it may be useful to intervene.
To detail every single by-way and wrinkle of the command execution cycle would probably be more confusing than helpful, concealing the main contours of the process behind a mass of not particularly interesting detail. In this article we shall therefore simplify some of the processes involved, so that the usual course of events stands out, unencumbered by a plethora of potential exceptions. If you want the complete picture, you'll have to examine the library source code and try to puzzle it out!
The command execution cycle is, if not a series of wheels within wheels, at least cycles within cycles or subroutines within subroutines. In an attempt to make this less confusing and more easy to follow, we shall try to follow the main line of execution one level at a time. We'll start by giving an overview of the main scheduling loop that runs each turn, and then trace the execution of a player's command piece by piece. Game authors who just want to see an step-by-step chart of the execution cycle focused mainly on the steps where game code might intervene may prefer to jump straight to the expandable summary below.
Broadbrush Overview
At a rough broadbrush approxination, the command execution loop works as follows:
- mainCommandLoop reads the player's command, applies any modifications from StringPreParsers, and passes it to Parser.parse() to parse (interpet).
- The parser attempts to work out what action, actor, and objects are intended in the player's command and creates a new Command object to encapsulate its understanding of the command.
- The Command object then selects the appropriate Doer or Doers to handle the command.
- The Doer object can do whatever it likes with the information passed to it by the Command object, but the usual behaviour is to pass the command information on to the relevant Action by calling the action's exec(cmd) method, where cmd is the Command object.
- The relevant Action then does the bulk of the work related to the specific action concerned, which includes carrying out the before action processing, the action processing, and the after action processing.
We shall next explore each of these steps in more detail.
Top Level Loop – mainCommandLoop
The overall command execution in adv3Lite is carried out by the mainCommandLoop()
function, which does the following:
- Sets the current actor to gPlayerChar to ensure there is a current actor (the current actor may be changed later in the cycle).
- Displays any pending score notifications.
- Executes any current PromptDaemons.
- Outputs a paragraph break.
- Calls
readCommandLine()
which:- Displays the command prompt.
- Reads the player's command and stores it in the local variable txt.
- Allows any active StringPreParsers to modidfy txt.
- If as a result, txt is now nil skips the rest of the loop and goes back to step 1.
- Calls Parser.parse(txt) to parse and execute the command.
- Updates the status line.
- Goes back to Step 1 to execute the next cycle.
Parsing the Player's Command
The main parsing loop is contained in Parser.parse().
Various things can complicate this process. The command may be directed towards another actor (e.g. BOB, PUT RED BALL IN BOX), or the player may have entered several commands at once (e.g. PUT RED BALL IN BOX, TAKE BOX, GO NORTH); or the Player have entered an OOPS command to correct a misspelling, or be responding to a parser query such as a disambiguation prompt, or be entering a sepecial command form such as an implicit SAY command (the text of what the player wants the player character to say without explictly using the command SAY); the parsing loop has to cope with these possibilities.
The full details of this process are far too complex to go into here, and most game authors will not usually either want or need to know about them. Here we shall give a simplified account that should suffice for most purposes. With that caveat, Parser.parse(str) carries out the following steps:
- Resets the special verb handler.
- Tokenizes the input string (str), producing a list of tokens encapsulating each of the separate words and punctuation marks in the player's input.
- If there are no outstanding parser queries, set the current actor to the player character (if there is an outstanding query, the current actor may already have been set).
- Notes any spelling corrects the spelling corrector has made.
- If there are no tokens at all (because the player has entered an empty command) carry out the empty command by calling
emptyCommand()
, which either carries out a LOOK command (if autoLook is true) or otherwise displays "I beg your pardon?". Either way the Parser then stops there. - Otherwise the parser checks for an OOPS command and if there is one correcting a current error, adjusts the command tokens from step 2 to take account of it.
- Updates the vocabulary of any game objects that use alternating or variable vocabulary.
- Gives the special verb manner the chance to amend the command tokens to take account of any SpecialVerbs the player may have used.
- If there's an outstanding parser question (such as a dismbiguation, 'Which do you mean?' or a missing object, 'What do you want to frob?'), and the question takes priority over new commands, try parsing the input against the question if it's the first command on the input line.
- Otherwise, construct a list of new Command objects which could correspond to the player's input.
This involves the construction of a new CommandList object which:
- Creates a list of commands by parsing the tokens passed to the CommandList constuctor by calling its Production's (typically a VerbRule) parseTokens() method. This stage identifes the possible actions the command may refer to.
- Sorts the command list in order of priority.
- Iterates through every command in the command list to call its resolveNouns() method, which in simplified form, does the following:
- Calls the matchVocab() method on each noun phrase, which again, in simplified turns, takes care of any pronouns and calls matchName(tokens) on every Thing in scope for the current action to obtain a list of matching objects.
- Calls buildObjLists() to construct a tentative list of matching objects.
- Determines the actor if the noun we're looking for occupies the actor role (e.g. bob in a command that starts "bob, jump")
- Select the objects from the available matches according to the grammatical mode (definite, indefinite, plural), by calling selectObjects() on each of the objects in our match list. Roughly speaking, this does the following:
- If we're looking for a definite match (e.g. "THE BOOK") and we have one possible match, simply choose that match. If we have more than one, attempt to disambiguate among the possibilities by calling our disambiguate() method which:
- First calls scoreObjects() on the current action, which in turn:
- Calls the verify routine for the current action on every item in the list of objects we want to choose between. If this is TIAction we're using multimethods for, first call the appropriate verifyWhatever() multimethod. In any case next call the verifyPreCondition() method on all PreConditions that apply to the object for the current action. Then call the verify() sections of any dobjFor() and (for TIActions) iobjFor() blocks.
- Adds 100 to the resultant verify score (the lowest logicalRank that emerges from the verify routine).
- Makes a note of this adjusted score and its associated verifyResult if it's better than any previous score we've encountered so far.
- Note whether the verifyResult has resulted in a change of object via a remap.
- Makes a note of which object came out best in whichever role we're currently interested in.
- Calls scoreObject() on any object we're interested, which adds the object's vocabLikelihood to the score and carries out a couple of other tweaks in special circumstances.
- Sorts the objects in descending order of score.
- If the number of objects with the highest score is the number of objects we're looking for, then selects them.
- Otherwise, if we have too many best matches, throws a disambiguation error so the parser can ask the player to clarify their choice.
- First calls scoreObjects() on the current action, which in turn:
- If we're looking for an indefinite match (e.g. "TWO BOOKS) calls scoreObjects() on the action and sorts the result in descending order of score then chooses the first however many objects we want.
- If we're looking for a match to ALL, simply select all the matching objects.
- If we're looking for a definite match (e.g. "THE BOOK") and we have one possible match, simply choose that match. If we have more than one, attempt to disambiguate among the possibilities by calling our disambiguate() method which:
- Goes back and re-resolve any ALL lists. For two-object commands, resolving ALL in one slot sometimes depends on resolving the bject in the other slot first.
- Resolves any reflexives.
- Check for any empty roles (such as a missing indirect object for a TIAction) and throws an error if it finds one.
- Calls buildObjLists() again to Clear out the old object lists, then build them anew. The old object lists were tentative, before disambiguation; we want to replace them now with the final lists.
- If any errors are encountered, notes the first curable one and then continues interating through the list of commands.
- If the previous step failed to find any resolvabe commands, try treating the player's input as the answer to any outstanding parser queries that don't take precedence over a new command.
- If we still don't have any resolvable commands and this is the first command on the line and there is a conversation currently in progress (gPlayerChar.currentInterlocutor isn't nil), and the player character can still talk to their current interlocutor and the player character's current interlocutor allows implicit say commands, then treat the player's input as an implicit SAY command.
- Otherwise, if the first word of the command doesn't match a possible verb, try parsing it as a noun phrase and, if that works, make the noun the direct object of our default action (GO TO for a Room or EXAMINE for any other thing), provided our defaultActions property is true (as it is by default).
- If we've applied a spelling correction, and the command match didn't consume the entire input, make sure what's left of the input has a valid parsing as another command. This ensures that we don't get a false positive by excessively shortening a command, which we can sometimes do by substituting a word like "then" for another word.
- If we didn't find a parsing at all, issue a generic "I don't understand" error. If we found a parsing, but not a resolution, reject it if it's a spelling correction. We only want completely clean spelling corrections, without any errors.
- If, one the other hand, the parser has found a resolvable command, we now execute it:
- First ensure that we don't have multiple objects occupying a slot that the action's grammar (VerbRule) defines as only allowing a single object (singleDobj, singleIobj). If we do, then reject the command and explain the problem.
- Otherwise, execute the command by calling its exec() method. This is outcome we hope will occur in the majority of cases.
- If this wasn't the last command on the command line, go back and parse the next one.
- If, however, we were unable to find a valid command, see if our command list contains a curable command, i.e. a command that could be rectified by the player responding to parser query.
- If we can't find a curable command, try applying the spelling corrector. If that works, try executing the corrected command
- If all has failed, throw an error and report the problem to the player and then stop trying to pause this command.
Executing the Command
Once the initial parsing has resolved the player's command to an action and a set of objects the action should act on (if there are any; for an IAction or most SystemActions there plainly won't be), the Parser calls the appropriate Command object's exec() method, which:
- Carries out a number of housekeeping tasks, such as resetting a number of values, which need not detain us here.
- Calls execGroup(cmd) on the relevant action to give it a chance to act on the entire group of objects involved (e.g. if the command was TAKE ALL or DROP THE PEN, THE GREEN BOOK AND THE GLASS KEY). In the library this does nothing on any actions part from GIVE TO and SHOW TO, where it resets a couple of properties used to summarise the effects of the action.
- If there are no noun roles (because this is an intransitive action) just run our execIter() method for this action.
- Otherwise, first combine any duplicate direct objects into a single object (this is relevant if, for example, the player has types X LEAVES, TWIGS, TREES and BRANCHES and these are all synonyms for the same Decoration object).
- Get the matches for each role (DirectObject, IndirecObject, etc.)
- Sort the roles into canonical order (DirectObject, IndirecObject, AccessoryObject)
- Call our execCombos() method for each set of objects in each role, starting with the DirectObject role: this method does the following for each set of objects:
- Sets the current object for this role.
- If there's more than one role, recursively call execCombos() for the next role.
- Otherwise, if we have reached the final role, call execIter(list), where list is a list in the form [action, dobj, iobj...]. This it turn does the following:
- Gives the special verb manager the opportunity to veto this command if we've been executing a SpecialVerb. Calls preAction(list) on the current actor (for example, to allow it to veto the action if the actor is bound or blindfolded).
- Calls execDoer(list). This finds the list of Doers that match this action and its current set of objects (as specified in list and executes the one with the highest priority. This will lead to the bulk of the action handling on this turn.
- Call reportAction() on the action that's just been executed, which in turn displays any pending implicit action reports and then calls the appropriate report method, reportDobjWhatever, on the last direct object the action acted on, in order to display a default summary report of the action (for any objects whose action() method didn't display anything).
- Display all the afterReports that were created by calls to reportAfter() during execution of the action.
- If the lighting conditions change, display a description of current location if it's now lit or announce the onset of darkness if it's now dark.
- Call afterAction() on all current Scenes.
- Call notifyAfter() on the actor's current Room. This first calls the Room's roomAfterAction() method and then calls regionAfterAction() on all the regions that contain the Room.
- Call afterAction() on every object currently in scope for the actor.
- Calls regionDaemon() on every region that contains the player character's current room.
- Calls roomDaemon() on the player character's current room.
- Calls executeTurn() on the eventManager object, which executes all current Events (Fuses and Daemons) in order of their priority, along with the sceneManager, which activates and deactivates Scenes as appropriate and calls the eachTurn() method on every active Scene and the actorSchedule, which calls the takeTurn() method on every Actor, which in turn carries out a number of Actor related housekeeping tasks including, if the actor hasn't conversed this turn:
- Trigger the relevant NodeContinuationTopic if the actor is the player character's current interlocutor, or
- Failing that, trigger the actor's highest priority AgendaItem, if any, or
- Failing that, call the doScript() method on the actor's current ActorState.
- Advances the turn counter.
Tbis completes the action cycle. At this point the game will return to the start of the main command loop.
The Doer
Once the current Command has identfied the appropriate Doer to handle the command, it calls that Doer's exec(curCommand) command, where curCommand is the current Command object. If the actor for the current command is the player character, we carry out various housekeeping tasks and allow the Doer to redirect the action to a different action, before calling our execAction() method. Otherwise we call handleCommand on the actor (who would then be a Non-Player Character). The normal result of calling execAction is to call our current action's exec(curCmd) method.
In sum a Doer, can intervene to change the action into something completely different, handle it itself, or stop the action althogether but the usual outcome is that the Doer will pass the command straight through to the action's exec() method.
Carrying Out the Action
As just stated, in most cases the relevant Doer will pass control to the appropriate action's exec(cmd) method, where cmd is the current Command object. What happens then depends on what type of action it is: SystemAction, IAction, TIAction or whatever, but we'll start with what virtually all actions do in common.
exec(cmd)
Action.exec(cmd) mostly does the following:
- Resets actionFailed to nil.
- Resets scopeList to an empty list.
- Sets libGlobal.curActor to cmd.actor.
- Notes the actor's current room in the action's oldRoom property.
- Notes that room's illumination state (lit or unlit) in the action's wasIlluminated property.
- Executes the action-processing cycle by calling execCycle(cmd).
A few types of action vary this slightly:
- LiteralActions additionally store cmd.dobj.name (their literal object) in their
literal
property. - TopicActions additionally store cmd.dobj (the current topic) in their
curTopic
property. - NumericActions additionally store cmd.dobj.numVal (the numeric value associated with the action) in their
num
property. - SystemAction.exec(cmd) simply calls execCycle(cmd) without doing anything else.
- A few specific actions defined in the library do their own thing in exec(cmd). These include Again, ExamineOrGoto, TakeFrom (which calls inherited(cmd) on one comditional branch) and TellTo.
execCycle(cmd)
Tbis does the following:
- Unless it's a SystemAction, TAction or TIAction, first calls beforeAction(), which in turn:
- Calls checkActionPreconditions() and stops the action if this returns nil.
- Calls actorAction() on the current actor.
- Calls notifyBefore() on the sceneManager, which calls beforeAction() on any current Scenes..
- Calls notifyBefore() on the actor's current room. This in turn:
- Calls roomBeforeAction() on the room.
- Calls regionBeforeAction() on every Region that (directly or indirectly) contains the room.
- Calls buildScopeList() to build the scope list for the current action if it hasn't already been built.
- Calls beforeAction() on every item in the scope list.
- Calls execAction(cmd).
- If the action is repeatable, stores a clone of the action at libGlobal.lastAction.
SystemAction omits step 1. A TravelAction first sets its direction
property to cmd.verbProd.dirMatch.dir (the direction entered by the player) unless it already has a non-nil predefinedDirection
property. A TAction or TIAction first checks whether ALL was used with an action for which allowAll is nil, and if so displays a message saying that the current command does not allow ALL and then stops the action; they also both call beforeAction() at a later point, during execAction().
execAction(cmd)
How things proceed from now on depend on the kind of action. Broadly speaking we may divide actions into Type 1 Actions that don't involve any physical game objects (or at least, don't have any grammar slots for them in their VerbRule) and Type 2 Actions that do. Type 1 Actions include SystemAction, IAction, TravelAction, TopicAction, LiteralAction and NumericAction. Type 2 Actions include all the rest: TAction, TIAction, TopicTAction, LiteralTAction and NumericTAction. For Type 2 Actions the details of what happens is defined on the dobjFor() and, for TIActions, iobjFor() blocks of the objects (or classes of object) involved. Type 1 Actions are handled in their execAction(cmd) method.
Type 1 Actions (SystemAction, IAction, TravelAction, TopicAction, LiteralAction and NumericAction)
For these types of action, what happens is defined by the execAction() method of the relevant Action object in a way that will vary from Action to Action. This method may call other methods on the Action object relevant to the specific action and/or methods on some physical game objects. An ImplicitConversationAction (a subclass of TopicAction), for example, will call handleTopic() on the player character's current interlocutor, which a TravelAction may well end up call the travelVia() method of an TravelConnector, which in turn will trigger various travel notifications.
Remember that once a Type 1 Action's execAction() has finished its work, control will return to the Command object's exec() method to carry out the post-action processing.
Type 2 Actions (TAction, TIAction, TopicTAction, LiteralTAction, NumericTAction)
From this point on, a Type 2 Action mainly co-ordinates calling the appropriate preCond, verify, check, and action methods on the objects involved in the command, although this will be via a series of methods we'll outline below.
TopicTAction, LiteralTAction and NumericTAction first ensure that the physical game object involved in the command is moved to the direct object slot and the topic, literal or number to the indirect object slot, regardless of the grammatical form of the command issued by the player (such as ASK BOB ABOUT TROUBLES, or WRITE FOO ON PAPER). This ensures that the action handling that follows can assume that the direct object is the Thing or Thing-derived object involved in the action, which in turn means that the relevant methods can always be called on the direct object.
Type 2 Actions then call notePronounAntecedent()
on the object(s) (dobj and possibly iobj) involved in the command so that if the player refers to the same object(s) with a pronoun in the following command (e.g. X BOOK followed by TAKE IT) the parser will know what the player is referring to.
Finally, execAction() calls execResolvedAction() to handle what comes next.
execResolvedAction()
This is similar for all Type 2 Actions, except that TIActions have to call methods on both the objects involved in the command. The steps are:
- Create a new LookUpTable called verifyTab to store our verify results.
- Obtain the verify results for current direct object. If we're a TIAction we also do the same for the indirect object; which comes first depends on the value of resolveIobjFirst for this Action. Note that the verify routines for this action were previously called by scoreObjects() to help the parser choose which objects the player meant. This time round the Action calls verifyObjRole(obj, role) on each of the objects (direct object, and if there is one, indirect object) to ascertain whether the action can go ahead. If verifyObjRole() returns nil, the action is stopped at this point and we skip to the post-action processing. The steps followed by verifyObjRole() are:
- Set up a new LookUpTable, then call verify(obj, role) and store the result in the local variable verResult; verify(obj, role) does the following:
- Clears out any existing verify table
- Determines which object properties to use depending on the role (DirectObject, IndirectObject, AccessoryObject or ActorRole).
- Calls obj.remap(D|I|A)obj to check is we should remap to a different object in this role; if so replace the current object with the object we're remapping to.
- If the object is a Decoration (obj.isDecoration is true) and we're not one of obj's decorationActions, change the verify prop we would otherwise have used to verify(D|I|A)objDefault.
- If we're a TIAction on which multi-method handling has been enabled, call the appropriate multi-method, e.g. verifyWhateverAction(dobj, iobj, obj). This may add verify results to our verify table.
- Call obj.(verifyProp) where verifyProp is the appropriate verify property (e.g. &verifyDobjPutIn) for this object in this role. This may add verify results to our verify table.
- If we don't yet have a verify table, create a new one now.
- If our verifyTable doesn't contain an entry for obj, create one now (a 'logical' result with a logical rank of 100).
- Iterate over all the preConditions defined on obj for this action in this role and call their verifyPrecondition(obj) method, which replace the verify result in our verify table if it's a worse result.
- Return the entry for obj in our verify table. This will the worst result, i.e. the one with the lowest logical rank. (The method that adds entries to a verify table only replaces an entry if the replacement is a 'worse' one).
- If the verify result doesn't allow the action (verResult.allowAction == nil), then note the failure message.
- If we're testing the direct object of the command and this action is iterating over more than one direct object, then announce the object name, unless announceMultiVerify (a property of the Action object) is nil.
- If this is an implicit action, add a failing implicit action report ('first trying to whatever') to the implicit action reports for this action.
- If we have a failure message, display it.
- If the action failed, note that it did.
- If we don't want a failed action to count as a turn (failedActionCountsAsTurn is nil for this Action), throw an
abort
signal to skip the post-action processing. - If the action failed, return nil to our caller.
- Otherwise, if this is an implicit action and our best verify result doesn't allow implicit actions (because it's
nonObvious
ordangerous
throw an abortImplicit signal to abort the implicit action. - Otherwise, if our worst verify result allows the action to go ahead, return true to our caller.
- Set up a new LookUpTable, then call verify(obj, role) and store the result in the local variable verResult; verify(obj, role) does the following:
- If gameMain.beforeRunsBeforeCheck is nil, the order of the next two steps is reversed.
- Calls beforeAction(), which does the following:
- Calls checkActionPreconditions() and stops the action if this returns nil. Note that checkActionPreconditions calls checkPreCondition on the PreConditions defined on the Action rather than those defined on any of the objects involved in the action.
- Calls actorAction() on the current actor.
- Calls notifyBefore() on the sceneManager, which calls beforeAction() on any current Scenes.
- Calls notifyBefore() on the actor's current room. This in turn:
- Calls roomBeforeAction() on the room.
- Calls regionBeforeAction() on every Region that (directly or indirectly) contains the room.
- Calls checkAction(). If this returns nil, stop the action here and jump to the post-action processing; checkAction() does the following:
- Calls checkPreCondition on all the preconditions related to this action defined on the dobj (and, for a TIAction, the iobj) of this action. If any fail, return nil to fail the action.
- If this a TIAction that has been enabled for multi-methods, calls check([dobj, iob], &mmcheck) to call the appropriate multi-method, checkWhateverAction(dobj, iobj).
- Calle the relevant check(dobj, checkDobjProp) and, if this is a TIAction, check(iobj, checkIobjProp), and if either returns nil, return nil to halt the action at this point; checkDobjProp is property pointer of the form &checkDobjActionName; the check() method does the following:
- If this a TIAction that has been enabled for multi-methods and check has been called with a list [dobj, iobj] for its first argument, calls the appropriate multi-method, checkWhateverAction(dobj, iobj), and captures the output to checkMsg.
- Otherwise, call the check method passed in the second argument on the object passed in the first (e.g. call dobj.checkDobjWhateverAction()) and capture the output to checkMsg.
- If checkMsg is neither nil nor the empty string '', then:
- If we're iterating over several objects and this action's announceMultiCheck property is true but its reportFailureAfterSuccess property is nil, display the name of the current object.
- If this is an implicit action, add a failure ('trying to...) message to our implicit action reports.
- Otherwise flush our implicit action reports (display any pending ones and then empty the list).
- If reportFailureAfterSuccess is true, call reportAfter(checkMsg) to add out failure message to the list of reports to be displayed in the post-action handling.
- Otherwise, simply display our checkMsg.
- Note that the action failed, unless the latest check routine called on an object used the noHalt macro.
Return nil to indicate that the action failed the check stage, unless the latest check routine called on an object used the noHalt macro.
- Otherwise, return true to allow the action to proceed.
doActionOnce()
If we've reached this point, the action can proceed, employing the following steps:
- If announceMultiAction is true for this action we're iterating over multiple direct objects for this command, then announce the name of the current direct object.
- If this is an implicit action, add it to the list of pending implicit action reports.
- If this is a TIAction and this action has been enabled for multi-methods, execute the appropriate multi-method, actionWhateverAction(dob, iobj) and note whether this output any text.
- Provided the previous step didn't use the skip macro, call the appropriate action method on the direct object, e.g. dobj.actionDobjWhatever(), and note whether this output any text.
- Certain actions, such as GoThrough, ClimbUp, ClimbDown, PushTravel and Enter, may cause the actor to travel from one room to another, in which case a number of travel notifications will be triggered at this point.
- Add the current direct object to the list of objects this action has added on.
- If this is a TIAction, call the appropriate action method on the indirect object, e.g. iobj.actionIobjWhatever(), and note whether this output any text.
- If none of the methods called above output any text then add the current indirect object to the list of objects we've acted on and add the current direct object to the list of objects to be reported on at the report stage.
- Return true to tell our caller we completed the action successfully.
Control now returns to the Command object's exec() method to carry out the post-action processing.
Travel notifications
TravelAction(s) and some TActions such as GoThrough, ClimbUp, ClimbDown and PushTravel which trigger travel via a TravelConnector (which can include a Room) will or may call a TravelConnector's travelVia() method to execute the travel. This will trigger a further series of notifactions at the action handling point in the command execution cycle, some of which may be used to interrupt the action. The sequence of event triggered by travelVia is as follows:
- Check any travel barriers defined on the TravelConnector. If any fail canTravelerPass() call their explainTravelBarrier() to explain travel isn't possible and halt the action at this point.
- Carry out our travel connector's beforeTravel notifications:
- Call beforeTravel(actor, connector) on every object that's in scope for the actor.
- Call regionBeforeTravel(actor, connector) on every region containing the actor's pre-travel current room.
- Call noteTraversal(actor) on the TravelConnector. If the actor is the player character this will cause the TravelConnector's travelDesc to be displayed.
- If this TravelConnector doesn't go anywhere (because its destination is nil) then if noteTraversal() hasn't displayed anything, then call sayNoDestination to say it doesn't lead anywhere, and it any case stop at this point.
- Call notifyDeparture() on the room the actor is leaving; this does the following:
- Calls travelerLeaving(traveler, dest) on the Room the actor is about to leave.
- Calls travelerLeaving(traveler,dest) on any Regions the actor is about to leave.
- Calls travelerEnteringtraveler,dest) on any Regions the actor is about to enter.
- Calls travelerEnteringtraveler,dest) on the Room the actor is about to enter.
- Move the traveler into the destination room.
- Display the description of the room the traveler has just arrived in, provided the traveler is or contains the player character and the new room's lookAroundOnEntering property is true (which it normally will be).
- Provided the traveler has arrived in a new room, carry out the after travel notifications:
- Call afterTravel(actor, connector) on every item in the actor's new scope.
- Call regionAfterTravel(actor, connector) on every Region that contains the actor's new location.
Summary
For a System Action the important steps are displaying any prompt daemons, accepting the player commmand, parsing it to a System Action (like QUIT or UNDO), passing it through a Doer (which could in theory intervene but normally shouldn't) and then carrying out the command in the Action object's execAction (c) method. User code will very seldom if at all need to intervene at any of the stages. Perhaps the most important to bear in mind with System Actions is not to rule them out by accident, as might happen in the course of a scene when the player character is immobilized and restricted to only a couple of basic actions. In this case you should normally ensure that all System Actions are still allowed (you can check for gAction.ofKind(SystemAction)
, otherwise players may become mightily annoyed with you if they can't even QUIT, let alone UNDO, SAVE or RESTORE.
All other actions are otherwise Type 1 actions or Type 2 Actions.
Type 1 Actions are those not involving any physical game objects (Things) as either direct object or indirect object. These include IAction, TravelAction, TopicAction, LiteralAction and NumericalAction
Type 2 Actions are those that do involve a physical game objects (Things) as either direct object or indirect object (or both). These include TAction, TIAction, TopicTAction, LiteralTAction and NumericTAction. Some TActions (e.g. GO THROUGH) also involve travel.
The table below lists the main points of the command action cycle of game authors in the order in which they occur. Some sections relate only to Type 1 actions, Type 2 actions, or actions involving travel. Readers can view the steps peculiar each by clicking on the little triangle beside the relevant section to expand them, as they can also do to expand various other summary headings.
- Any pending score notifications are displayed.
- Any current PromptDaemons are executed.
- The command prompt is displayed and the player's command read.
- Any active StringPreParsers get a chance to amend the player's input and/or stop the action.
- The command is parsed to identify the actor, action, and objects involved. A SpecialVerb can intervene to translate an non-standard verb into one the parser can understand.
- To identity the objects (Things) involved the parser may call the verify() routines for the relevant action on the direct and, if there is on, indirect object of the action to choose the objects with the highest score ( highest logical rank).
- The command is encapsulated in a Command object.
- The Commmand object calls the preAction() method on the actor (which could stop the action at this point).
- The Command object chooses the most suitable Doer to handle the command and calls its execAction() method. This can modify or stop the action, but normally passes it straight through to the appropriate Action object by calling the Action object's execAction() method.
Type 1 Actions only carry out their before action processing at this point:
- Call checkActionPreconditions() and stops the action if this returns nil.
- Call actorAction() on the current actor.
- Call notifyBefore() on the sceneManager, which calls beforeAction() on any current Scenes..
- Call roomBeforeAction() on the room.
- Call regionBeforeAction() on every Region that (directly or indirectly) contains the room.
- Call buildScopeList() to build the scope list for the current action if it hasn't already been built.
- Call beforeAction() on every item in the scope list.
- Type 1 Actions only then execute the action directly in their execAction method.
Type 1 Travel Actions (e.g. GO EAST) carry out travel notifications and travel at this point:
- Check any travel barriers defined on the TravelConnector. If any fail canTravelerPass() call their explainTravelBarrier() to explain travel isn't possible and halt the action at this point.
- Carry out our travel connector's beforeTravel notifications:
- Call beforeTravel(actor, connector) on every object that's in scope for the actor.
- Call regionBeforeTravel(actor, connector) on every region containing the actor's pre-travel current room.
- Call noteTraversal(actor) on the TravelConnector. If the actor is the player character this will cause the TravelConnector's travelDesc to be displayed.
- If this TravelConnector doesn't go anywhere (because its destination is nil) then if noteTraversal() hasn't displayed anything, then call sayNoDestination to say it doesn't lead anywhere, and it any case stop at this point.
- Call notifyDeparture() on the room the actor is leaving; this does the following:
- Calls travelerLeaving(traveler, dest) on the Room the actor is about to leave.
- Calls travelerLeaving(traveler,dest) on any Regions the actor is about to leave.
- Calls travelerEnteringtraveler,dest) on any Regions the actor is about to enter.
- Calls travelerEnteringtraveler,dest) on the Room the actor is about to enter.
- Move the traveler into the destination room.
- Display the description of the room the traveler has just arrived in, provided the traveler is or contains the player character and the new room's lookAroundOnEntering property is true (which it normally will be).
- Provided the traveler has arrived in a new room, carry out the after travel notifications:
- Call afterTravel(actor, connector) on every item in the actor's new scope.
- Call regionAfterTravel(actor, connector) on every Region that contains the actor's new location.
Type 2 Actions only then call appropriate methods on the direct and (if there is one) indirect objects of the action:
- Calls obj.remap(D|I|A)obj to check is we should remap to a different object in this role; if so replace the current object with the object we're remapping to.
- If the object is a Decoration (obj.isDecoration is true) and we're not one of obj's decorationActions, change the verify prop we would otherwise have used to verify(D|I|A)objDefault.
- If we're a TIAction on which multi-method handling has been enabled, call the appropriate multi-method, e.g. verifyWhateverAction(dobj, iobj, obj).
- Call the verify routines on the direct and (if there is one) indirect object. These may rule out the action at this point.
- Call the verifyPrecondition() method on all the preconditions attached to the direct (and, if any, indirect) objects of the action. These may also rule out the action.
- If
gameMain.beforeRunsBeforeCheck
is nil, the order of the next two steps is reversed. Carry out the before action handling:
- Calls checkActionPreconditions() and stops the action if this returns nil. Note that checkActionPreconditions calls checkPreCondition on the PreConditions defined on the Action rather than those defined on any of the objects involved in the action.
- Calls actorAction() on the current actor.
- Calls notifyBefore() on the sceneManager, which calls beforeAction() on any current Scenes.
- Calls notifyBefore() on the actor's current room. This in turn:
- Calls roomBeforeAction() on the room.
- Calls regionBeforeAction() on every Region that (directly or indirectly) contains the room.
Carry out the check routines
- Call checkPreCondition on all the preconditions related to this action defined on the dobj (and, for a TIAction, the iobj) of this action. If any fail, return nil to fail the action.
- If this a TIAction that has been enabled for multi-methods, call the appropriate multi-method, checkWhateverAction(dobj, iobj).
- Call the check() methods for the current action on the direct and (if any) indirect object of the command.
- If any of the above methods output any text, halt the output at this point (unless a
noHalt
was used).
- Call the action() methods for the current action on the direct and (if any) indirect object of the command.
If the action involves travel (e.g., GO THROUGH DOOR) this will also involve travel notifications:
- Check any travel barriers defined on the TravelConnector. If any fail canTravelerPass() call their explainTravelBarrier() to explain travel isn't possible and halt the action at this point.
- Carry out our travel connector's beforeTravel notifications:
- Call beforeTravel(actor, connector) on every object that's in scope for the actor.
- Call regionBeforeTravel(actor, connector) on every region containing the actor's pre-travel current room.
- Call noteTraversal(actor) on the TravelConnector. If the actor is the player character this will cause the TravelConnector's travelDesc to be displayed.
- If this TravelConnector doesn't go anywhere (because its destination is nil) then if noteTraversal() hasn't displayed anything, then call sayNoDestination to say it doesn't lead anywhere, and it any case stop at this point.
- Call notifyDeparture() on the room the actor is leaving; this does the following:
- Call travelerLeaving(traveler, dest) on the Room the actor is about to leave.
- Call travelerLeaving(traveler,dest) on any Regions the actor is about to leave.
- Call travelerEnteringtraveler,dest) on any Regions the actor is about to enter.
- Call travelerEnteringtraveler,dest) on the Room the actor is about to enter.
- Move the traveler into the destination room.
- Display the description of the room the traveler has just arrived in, provided the traveler is or contains the player character and the new room's lookAroundOnEntering property is true (which it normally will be).
- Provided the traveler has arrived in a new room, carry out the after travel notifications:
- Call afterTravel(actor, connector) on every item in the actor's new scope.
- Call regionAfterTravel(actor, connector) on every Region that contains the actor's new location.
- If nothing at the action stage has output any text, add the direct object to the list of direct objects to be reported on at the report stage.
Carry out the post-action processing
- Call reportAction() on the action that's just been executed, which in turn displays any pending implicit action reports and then, for a Type 2 Action, calls the appropriate report method, reportDobjWhatever, on the last direct object the action acted on, in order to display a default summary report of the action (for any objects whose action() method didn't display anything).
- Display all the afterReports that were created by calls to reportAfter() during execution of the action.
- Next carry out the afterAction() routine for the current action, unless it's a SystemAction or the action failed. This does the following:
- If the lighting conditions change, display a description of current location if it's now lit or announce the onset of darkness if it's now dark.
- Call afterAction() on all current Scenes.
- Call notifyAfter() on the actor's current Room. This first calls the Room's roomAfterAction() method and then calls regionAfterAction() on all the regions that contain the Room.
- Call afterAction() on every object currently in scope for the actor.
- Call turnSequence() on the current action. This does the following:
- Calls regionDaemon() on every region that contains the player character's current room.
- Calls roomDaemon() on the player character's current room.
- Calls executeTurn() on the eventManager object, which executes all current Events (Fuses and Daemons) in order of their priority, along with the sceneManager, which activates and deactivates Scenes as appropriate and calls the eachTurn() method on every active Scene and the actorSchedule, which calls the takeTurn() method on every Actor, which in turn carries out a number of Actor related housekeeping tasks including, if the actor hasn't conversed this turn:
- Trigger the relevant NodeContinuationTopic if the actor is the player character's current interlocutor, or
- Failing that, trigger the actor's highest priority AgendaItem, if any, or
- Failing that, call the doScript() method on the actor's current ActorState.
- Advances the turn counter.
- Call advanceTime() on the current action. This does nothing in the main library but is provided as a hook for the Objective Time extension to use to add to the time taken by implicit actions.
This completes the full command cycle.