exec.t
#charset "us-ascii"
/*
* Copyright (c) 2000, 2006 Michael J. Roberts. All Rights Reserved.
*
* TADS 3 Library: command execution
*
* This module defines functions that perform command execution.
*/
#include "adv3.h"
/* ------------------------------------------------------------------------ */
/*
* Execute a command line, as issued by the given actor and as given as a
* list of tokens.
*
* If 'firstInSentence' is true, we're at the start of a "sentence." The
* meaning and effect of this may vary by language. In English, a
* sentence ends with certain punctuation marks (a period, semicolon,
* exclamation mark, or question mark), so anything after one of these
* punctuation marks is the start of a new sentence. Also in English, we
* can address a command to an explicit target actor using the "actor,"
* prefix syntax, which we can't use except at the start of a sentence.
*
* If the command line consists of multiple commands, we will only
* actually execute the first command before returning. We'll schedule
* any additional commands for later execution by putting them into the
* target actor's pending command queue before we return, but we won't
* actually execute them.
*/
executeCommand(targetActor, issuingActor, toks, firstInSentence)
{
local actorPhrase;
local actorSpecified;
/*
* Turn on sense caching while we're working, until execution
* begins. The parsing and resolution phases of command processing
* don't involve any changes to game state, so we can safely cache
* sense information; caching sense information during these phases
* is desirable because these steps (noun resolution in particular)
* involve repeated inspection of the current sensory environment,
* which can require expensive calculations.
*/
libGlobal.enableSenseCache();
/* we don't have an explicit actor phrase yet */
actorPhrase = nil;
/* presume an actor will not be specified */
actorSpecified = nil;
/*
* If this is the start of a new sentence, and the issuing actor
* wants to cancel any target actor designation at the end of each
* sentence, change the target actor back to the issuing actor.
*/
if (firstInSentence
&& issuingActor != targetActor
&& issuingActor.revertTargetActorAtEndOfSentence)
{
/* switch to the target actor */
targetActor = issuingActor;
/* switch to the issuer's sense context */
senseContext.setSenseContext(issuingActor, sight);
}
/*
* Keep going until we've processed the command. This might take
* several iterations, because we might have replacement commands to
* execute.
*/
parseTokenLoop:
for (;;)
{
local lst;
local action;
local match;
local nextIdx;
local nextCommandTokens;
local extraIdx;
local extraTokens;
/*
* Catch any errors that occur while executing the command,
* since some of them are signals to us that we should reparse
* some new text read or generated deep down inside the command
* processing.
*/
try
{
local rankings;
/* we have no extra tokens yet */
extraTokens = [];
/*
* Parse the token list. If this is the first command on
* the command line, allow an actor prefix. Otherwise, just
* look for a command.
*/
lst = (firstInSentence ? firstCommandPhrase : commandPhrase)
.parseTokens(toks, cmdDict);
/*
* As a first cut at reducing the list of possible matches
* to those that make sense, eliminate from this list any
* matches which do not have valid actions. In grammars for
* "scrambling" languages (i.e., languages with flexible
* word ordering), it is possible to construct commands that
* fit the grammatical rules of sentence construction but
* which make no sense because of the specific constraints
* of the verbs involved; we can filter out such nonsense
* interpretations immediately by keeping only those
* structural interpretations that can resolve to valid
* actions.
*/
lst = lst.subset(
{x: x.resolveFirstAction(issuingActor, targetActor) != nil});
/* if we have no matches, the command isn't understood */
if (lst.length() == 0)
{
/*
* If this is a first-in-sentence phrase, try looking for
* a target actor phrase. If we can find one, we can
* direct the 'oops' to that actor, to allow the game to
* customize messages more specifically.
*/
if (firstInSentence)
{
local i;
/* try parsing an "actor, <unknown>" phrase */
lst = actorBadCommandPhrase.parseTokens(toks, cmdDict);
/* if we got any matches, try to resolve actors */
lst = lst.mapAll({x: x.resolveNouns(
issuingActor, issuingActor,
new TryAsActorResolveResults())});
/* drop any that didn't yield any results */
lst = lst.subset({x: x != nil && x.length() != 0});
/*
*
* if anything's left, and one of the entries
* resolves to an actor, arbitrarily pick the
* first such entry use the resolved actor as the
* new target actor
*/
if (lst.length() != 0
&& (i = lst.indexWhich(
{x: x[1].obj_.ofKind(Actor)})) != nil)
targetActor = lst[i][1].obj_;
}
/*
* We don't understand the command. Check for unknown
* words - if we have any, give them a chance to use
* OOPS to correct a typo.
*/
tryOops(toks, issuingActor, targetActor,
1, toks, rmcCommand);
/*
* try running it by the SpecialTopic history to see if
* they're trying to use a special topic in the wrong
* context - if so, explain that they can't use the
* command right now, rather than claiming that the
* command is completely invalid
*/
if (specialTopicHistory.checkHistory(toks))
{
/* the special command is not currently available */
targetActor.notifyParseFailure(
issuingActor, &specialTopicInactive, []);
}
else
{
/* tell the issuer we didn't understand the command */
targetActor.notifyParseFailure(
issuingActor, &commandNotUnderstood, []);
}
/*
* we're done with this command, and we want to abort
* any subsequent commands on the command line
*/
return;
}
/* show the matches if we're in debug mode */
dbgShowGrammarList(lst);
/*
* Perform a tentative resolution on each alternative
* structural interpretation of the command, and rank the
* interpretations in order of "goodness" as determined by
* our ranking criteria encoded in CommandRanking.
*
* Note that we perform the tentative resolution and ranking
* even if we have only one interpretation, because the
* tentative resolution is often useful for the final
* resolution pass.
*/
rankings = CommandRanking
.sortByRanking(lst, issuingActor, targetActor);
/*
* Take the interpretation that came up best in the rankings
* (or, if we have multiple at the same ranking, arbitrarily
* pick the one that happened to come up first in the list)
* - they're ranked in descending order of goodness, so take
* the first one.
*/
match = rankings[1].match;
/* if we're in debug mode, show the winner */
dbgShowGrammarWithCaption('Winner', match);
/*
* Get the token list for the rest of the command after what
* we've parsed so far.
*
* Note that we'll start over and parse these tokens anew,
* even though we might have parsed them already into a
* subcommand on the previous iteration. Even if we already
* parsed these tokens, we want to parse them again, because
* we did not have a suitable context for evaluating the
* semantic strengths of the possible structural
* interpretations this command on the previous iteration;
* for example, it was not possible to resolve noun phrases
* in this command because we could not guess what the scope
* would be when we got to this point. So, we'll simply
* discard the previous match tree, and start over, treating
* the new tokens as a brand new command.
*/
nextIdx = match.getNextCommandIndex();
nextCommandTokens = toks.sublist(nextIdx);
/* if the pending command list is empty, make it nil */
if (nextCommandTokens.length() == 0)
nextCommandTokens = nil;
/*
* Get the part of the token list that the match doesn't use
* at all. These are the tokens following the tokens we
* matched.
*/
extraIdx = match.tokenList.length() + 1;
extraTokens = toks.sublist(extraIdx);
/*
* We now have the best match for the command tree.
*
* If the command has an actor clause, resolve the actor, so
* that we can direct the command to that actor. If the
* command has no actor clause, the command is to the actor
* who issued the command.
*
* Do NOT process the actor clause if we've already done so.
* If we edit the token list and retry the command after
* this point, there is no need to resolve the actor part
* again. Doing so could be bad - if resolving the actor
* required player interaction, such as asking for help with
* an ambiguous noun phrase, we do not want to go through
* the same interaction again just because we have to edit
* and retry a later part of the command.
*/
if (match.hasTargetActor())
{
local actorResults;
/*
* If we haven't yet explicitly specified a target
* actor, and the default target actor is different from
* the issuing actor, then this is really a new command
* from the issuing actor.
*
* First, an actor change mid-command is allowed only if
* the issuing actor waits for NPC commands to be
* carried out; if not, then we can't change the actor
* mid-command.
*
* Second, since the command comes from the issuing
* actor, not from the default target actor, we want to
* issue the orders on the issuing actor's turn, so put
* the command into the queue for the issuer, and let
* the issuer re-parse the command on the issuer's next
* turn.
*/
if (!actorSpecified && issuingActor != targetActor)
{
/*
* don't allow the command if the issuing actor
* doesn't wait for orders to be completed
*/
if (!issuingActor.issueCommandsSynchronously)
{
/* turn off any sense capturing */
senseContext.setSenseContext(nil, sight);
/* show the error */
issuingActor.getParserMessageObj()
.cannotChangeActor();
/* done */
return;
}
/* put the command into the issuer's queue */
issuingActor.addFirstPendingCommand(
firstInSentence, issuingActor, toks);
/* done */
return;
}
/* create an actor-specialized results object */
actorResults = new ActorResolveResults();
/* set up the actors in the results object */
actorResults.setActors(targetActor, issuingActor);
/* resolve the actor object */
match.resolveNouns(issuingActor, targetActor, actorResults);
/* get the target actor from the command */
targetActor = match.getTargetActor();
/* pull out the phrase specifying the actor */
actorPhrase = match.getActorPhrase();
/*
* Copy antecedents from the issuing actor to the target
* actor. Since the issuer is giving us a new command
* here, pronouns will be given from the issuer's
* perspective.
*/
targetActor.copyPronounAntecedentsFrom(issuingActor);
/* let the actor phrase know we're actually using it */
match.execActorPhrase(issuingActor);
/*
* Ask the target actor if it's interested in the
* command at all. This only applies when the actor was
* actually specified - if an actor wasn't specified,
* the command is either directed to the issuer itself,
* in which case the command will always be accepted; or
* the command is a continuation of a command line
* previously accepted.
*/
if (!targetActor.acceptCommand(issuingActor))
{
/*
* the command was immediately rejected - abandon
* the command and any subsequent commands on the
* same line
*/
return;
}
/* note that an actor was specified */
actorSpecified = true;
/*
* Pull out the rest of the command (other than the
* target actor specification) and start over with a
* fresh parse of the whole thing. We must do this
* because our tentative resolution pass that we used to
* pick the best structural interpretation of the
* command couldn't go far enough - since we didn't know
* the actor involved, we weren't able to resolve nouns
* in the rest of the command. Now that we know the
* actor, we can start over and resolve everything in
* the rest of the command, and thus choose the right
* structural match for the command.
*/
toks = match.getCommandTokens();
/* what follows obviously isn't first in the sentence */
firstInSentence = nil;
/* go back to parse the rest */
continue parseTokenLoop;
}
/* pull out the first action from the command */
action = match.resolveFirstAction(issuingActor, targetActor);
/*
* If the winning interpretation had any unknown words, run
* a resolution pass to resolve those interactively, if
* possible. We want to do this before doing any other
* interactive resolution because OOPS has the unique
* property of forcing us to reparse the command; if we
* allowed any other interactive resolution to happen before
* processing an OOPS, we'd likely have to repeat the other
* resolution on the reparse, which would confuse and
* irritate users by asking the same question more than once
* for what is apparently the same command.
*/
if (rankings[1].unknownWordCount != 0)
{
/*
* resolve using the OOPS results gatherer - this runs
* essentially the same preliminary resolution process
* as the ranking results gatherer, but does perform
* interactive resolution of unknown words via OOPS
*/
match.resolveNouns(
issuingActor, targetActor,
new OopsResults(issuingActor, targetActor));
}
/*
* If the command is directed to a different actor than the
* issuer, change to the target actor's sense context.
*
* On the other hand, if this is a conversational command
* (e.g., BOB, YES or BOB, GOODBYE), execute it within the
* sense context of the issuer, even when a target is
* specified. Target actors in conversational commands
* designate the interlocutor rather than the performing
* actor: BOB, YES doesn't ask Bob to say "yes", but rather
* means that the player character (the issuer) is saying
* "yes" to Bob.
*/
if (action != nil && action.isConversational(issuingActor))
senseContext.setSenseContext(issuingActor, sight);
else if (actorSpecified && targetActor != issuingActor)
senseContext.setSenseContext(targetActor, sight);
/* set up a transcript to receive the command results */
withCommandTranscript(CommandTranscript, function()
{
/*
* Execute the action.
*
* If a target actor was specified, and it's not the same
* as the issuing actor, this counts as a turn for the
* issuing actor.
*/
executeAction(targetActor, actorPhrase, issuingActor,
actorSpecified && issuingActor != targetActor,
action);
});
/*
* If we have anything remaining on the command line, insert
* the remaining commands at the start of the target actor's
* command queue, since we want the target actor to continue
* with the rest of this command line as its next operation.
*
* Since the remainder of the line represents part of the
* work the actor pulled out of the queue in order to call
* us, put the remainder back in at the START of the actor's
* queue - it was before anything else that might be in the
* actor's queue before, so it should stay ahead of anything
* else now.
*/
if (nextCommandTokens != nil)
{
/*
* Prepend the remaining commands to the actor's queue.
* The next command is the start of a new sentence if
* the command we just executed ends the sentence.
*/
targetActor.addFirstPendingCommand(
match.isEndOfSentence(), issuingActor, nextCommandTokens);
}
/*
* If the command was directed from the issuer to a
* different target actor, and the issuer wants to wait for
* the full set of issued commands to complete before
* getting another turn, tell the issuer to begin waiting.
*/
if (actorSpecified && issuingActor != targetActor)
issuingActor.waitForIssuedCommand(targetActor);
/* we're done */
return;
}
catch (ParseFailureException rfExc)
{
/*
* Parsing failed in such a way that we cannot proceed.
* Tell the target actor to notify the issuing actor.
*/
rfExc.notifyActor(targetActor, issuingActor);
/*
* the command cannot proceed, so abandon any remaining
* tokens on the command line
*/
return;
}
catch (CancelCommandLineException cclExc)
{
/*
* if there are any tokens remaining, the game might want to
* show an explanation
*/
if (nextCommandTokens != nil)
targetActor.getParserMessageObj().explainCancelCommandLine();
/* stop now, abandoning the rest of the command line */
return;
}
catch (TerminateCommandException tcExc)
{
/*
* the command cannot proceed - we can't do any more
* processing of this command, so simply return, abandoning
* any additional tokens we have
*/
return;
}
catch (RetryCommandTokensException rctExc)
{
/*
* We want to replace the current command's token list with
* the new token list - get the new list, and then go back
* and start over processing it.
*
* Note that we must retain any tokens beyond those that the
* match tree used. The exception only edits the current
* match tree's matched tokens, since it doesn't have access
* to any of the original tokens beyond those, so we must
* now add back in any tokens beyond the originals.
*/
toks = rctExc.newTokens_ + extraTokens;
/* go back and process the command again with the new tokens */
continue parseTokenLoop;
}
catch (ReplacementCommandStringException rcsExc)
{
local str;
/* retrieve the new command string from the exception */
str = rcsExc.newCommand_;
/*
* if the command string is nil, it means that the command
* has been fully handled already, so we simply return
* without any further work
*/
if (str == nil)
return;
/*
* Replace the entire command string with the one from the
* exception - this cancels any previous command that we
* had.
*/
toks = cmdTokenizer.tokenize(str);
/*
* we have a brand new command line, so we're starting a
* brand new sentence
*/
firstInSentence = true;
/* set the issuing and target actor according to the exception */
issuingActor = rcsExc.issuingActor_;
targetActor = rcsExc.targetActor_;
/*
* Put this work into the target actor's work queue, so that
* the issuer will carry out the command at the next
* opportunity. This is a brand new command line, so it
* starts a new sentence.
*/
targetActor.addPendingCommand(true, issuingActor, toks);
/* we're done processing this command */
return;
}
}
}
/* ------------------------------------------------------------------------ */
/*
* GlobalRemapping makes it possible to transform one action into another
* globally - as opposed to the remapTo mechanism, which lets an object
* involved in the command perform a remapping. The key difference
* between global remappings and remapTo is that the latter can't happen
* until after the objects are resolved (for fairly obvious reasons: each
* remapTo mapping is associated with an object, so you can't know which
* mapping to apply until you know which object is involved). In
* contrast, global remappings are performed *before* object resolution -
* this is possible because the mappings don't depend on the objects
* involved in the action.
*
* Whenever an action is about to be executed, the parser runs through
* all of the defined global remappings, and gives each one a chance to
* remap the command. If any remapping succeeds, we replace the original
* command with the remapped version, then repeat the scan of the global
* remapping list from the start - we do another complete scan of the
* list in case there's another global mapping that applies to the
* remapped version of the command. We repeat this process until we make
* it through the whole list without finding a remapping.
*
* GlobalRemapping instances are added to the master list of mappings
* automatically at pre-init time, and any time you construct one
* dynamically with 'new'.
*/
class GlobalRemapping: PreinitObject
/*
* Check for and apply a remapping. This method must be implemented
* in each GlobalRemapping instance to perform the actual remapping
* work.
*
* This routine should first check to see if the command is relevant
* to this remapping. In most cases, this means checking that the
* command matches some template, such as having a particular action
* (verb) and combination of potential objects. Note that the
* objects aren't fully resolved during global remapping - the whole
* point of global remapping is to catch certain phrasings before we
* get to the noun resolution phase - but the *phrases* involved will
* be known, so the range of potential matches is knowable.
*
* If the routine decides that the action isn't relevant to this
* remapping, it should simply return nil.
*
* If the action decides to remap the action, it must create a new
* Action object representing the replacement version of the command.
* Then, return a list, [targetActor, action], giving the new target
* actor and the new action. You don't have to change the target
* actor, of course, but it's included in the result so that you can
* change it if you want to. For example, you could use this to
* remap a command of the form "X, GIVE ME Y" to "ME, ASK X FOR Y" -
* note that the target actor changes from X to ME.
*/
getRemapping(issuingActor, targetActor, action)
{
/*
* this must be overridden to perform the actual remapping; by
* default, simply return nil to indicate that we don't want to
* remap this action
*/
return nil;
}
/*
* Remapping order - the parser applies global remappings in
* ascending order of this value. In most cases, the order shouldn't
* matter, since most remappings should be narrow enough that a given
* command will only be subject to one remapping rule. However, in
* some cases you might need to define rules that overlap, so the
* ordering lets you specify which one goes first. In most cases
* you'll want to apply the more specific rule first.
*/
remappingOrder = 100
/*
* Static class method: look for a remapping. This runs through the
* master list of mappings, looking for a mapping that applies to the
* given command. If we find one, we'll replace the command with the
* remapped version, then start over with a fresh scan of the entire
* list to see if there's a remapping for the *new* version of the
* command. We repeat this until we get through the whole list
* without finding a remapping.
*
* The return value is a list, [targetActor, action], giving the
* resulting target actor and new action object. If we don't find
* any remapping, this will simply be the original values passed in
* as our arguments; if we do find a remapping, this will be the new
* version of the command.
*/
findGlobalRemapping(issuingActor, targetActor, action)
{
/* get the global remapping list */
local v = GlobalRemapping.allGlobalRemappings;
local cnt = v.length();
/* if necessary, sort the list */
if (GlobalRemapping.listNeedsSorting)
{
/* sort it by ascending remappingOrder value */
v.sort(SortAsc, {a, b: a.remappingOrder - b.remappingOrder});
/* note that it's now sorted */
GlobalRemapping.listNeedsSorting = nil;
}
/*
* iterate through the list repeatedly, until we make it all the
* way through without finding a mapping
*/
for (local done = nil ; !done ; )
{
/* presume we won't find a remapping on this iteration */
done = true;
/* run through the list, looking for a remapping */
for (local i = 1 ; i <= cnt ; ++i)
{
local rm;
/* check for a remapping */
rm = v[i].getRemapping(issuingActor, targetActor, action);
if (rm != nil)
{
/* found a remapping - apply it */
targetActor = rm[1];
action = rm[2];
/*
* we found a remapping, so we have to repeat the
* scan of the whole list - note that we're not done,
* and break out of the current scan so that we start
* over with a fresh scan
*/
done = nil;
break;
}
}
}
/* return the final version of the command */
return [targetActor, action];
}
/* pre-initialization: add each instance to the master list */
execute()
{
/* add me to the master list */
registerGlobalRemapping();
}
/* construction: add myself to the master list */
construct()
{
/* add me to the master list */
registerGlobalRemapping();
}
/* register myself with the global list, making this an active mapping */
registerGlobalRemapping()
{
/* add myself to the global list */
GlobalRemapping.allGlobalRemappings.append(self);
/* note that a sort is required the next time we run */
GlobalRemapping.listNeedsSorting = true;
}
/*
* unregister - this removes me from the global list, making this
* mapping inactive: after being unregistered, the parser won't apply
* this mapping to new commands
*/
unregisterGlobalRemapping()
{
GlobalRemapping.allGlobalRemappings.removeElement(self);
}
/*
* Static class property: the master list of remappings. We build
* this automatically at preinit time, and manipulate it via our
* constructor.
*/
allGlobalRemappings = static new Vector(10)
/*
* static class property: the master list needs to be sorted; this is
* set to true each time we update the list, so that the list scanner
* knows to sort it before doing its scan
*/
listNeedsSorting = nil
;
/* ------------------------------------------------------------------------ */
/*
* Execute an action, as specified by an Action object. We'll resolve
* the nouns in the action, then perform the action.
*/
executeAction(targetActor, targetActorPhrase,
issuingActor, countsAsIssuerTurn, action)
{
local rm, results;
startOver:
/* check for a global remapping */
rm = GlobalRemapping.findGlobalRemapping(
issuingActor, targetActor, action);
targetActor = rm[1];
action = rm[2];
/* create a basic results object to handle the resolution */
results = new BasicResolveResults();
/* set up the actors in the results object */
results.setActors(targetActor, issuingActor);
/* catch any "remap" signals while resolving noun phrases */
try
{
/* resolve noun phrases */
action.resolveNouns(issuingActor, targetActor, results);
}
catch (RemapActionSignal sig)
{
/* mark the new action as remapped */
sig.action_.setRemapped(action);
/* get the new action from the signal */
action = sig.action_;
/* start over with the new action */
goto startOver;
}
/*
* Check to see if we should create an undo savepoint for the
* command. If the action is not marked for inclusion in the undo
* log, there is no need to log a savepoint for it.
*
* Don't save undo for nested commands. A nested command is part of
* a main command, and we only want to save undo for the main
* command, not for its individual sub-commands.
*/
if (action.includeInUndo
&& action.parentAction == nil
&& (targetActor.isPlayerChar()
|| (issuingActor.isPlayerChar() && countsAsIssuerTurn)))
{
/*
* Remember the command we're about to perform, so that if we
* undo to here we'll be able to report what we undid. Note that
* we do this *before* setting the savepoint, because we want
* after the undo to know the command we were about to issue at
* the savepoint.
*/
libGlobal.lastCommandForUndo = action.getOrigText();
libGlobal.lastActorForUndo =
(targetActorPhrase == nil
? nil
: targetActorPhrase.getOrigText());
/*
* set a savepoint here, so that we on 'undo' we'll restore
* conditions to what they were just before we executed this
* command
*/
savepoint();
}
/*
* If this counts as a turn for the issuer, adjust the issuer's busy
* time.
*
* However, this doesn't apply if the command is conversational (that
* is, it's something like "BOB, HELLO"). A conversational command
* is conceptually carried out by the issuer, not the target actor,
* since the action consists of the issuer actually saying something
* to the target actor. The normal turn accounting in Action will
* count a conversational command this way, so we don't have to do
* the extra bookkeeping for such a command here.
*/
if (countsAsIssuerTurn && !action.isConversational(issuingActor))
{
/*
* note in the issuer that the target is the most recent
* conversational partner
*/
issuingActor.lastInterlocutor = targetActor;
/* make the issuer busy for the order-giving interval */
issuingActor.addBusyTime(nil,
issuingActor.orderingTime(targetActor));
/* notify the target that this will be a non-idle turn */
targetActor.nonIdleTurn();
}
/*
* If the issuer is directing the command to a different actor, and
* it's not a conversational command, check with the target actor to
* see if it wants to accept the command. Don't check
* conversational commands, since these aren't of the nature of
* orders to be obeyed.
*/
if (issuingActor != targetActor
&& !action.isConversational(issuingActor)
&& !targetActor.obeyCommand(issuingActor, action))
{
/*
* if the issuing actor's "ordering time" is zero, make this take
* up a turn anyway, just for the refusal
*/
if (issuingActor.orderingTime(targetActor) == 0)
issuingActor.addBusyTime(nil, 1);
/*
* Since we're aborting the command, we won't get into the normal
* execution for it. However, we might still want to save it for
* an attempted re-issue with AGAIN, so do so explicitly now.
*/
action.saveActionForAgain(issuingActor, countsAsIssuerTurn,
targetActor, targetActorPhrase);
/*
* This command was rejected, so don't process it any further,
* and give up on processing any remaining commands on the same
* command line.
*/
throw new TerminateCommandException();
}
/* execute the action */
action.doAction(issuingActor, targetActor, targetActorPhrase,
countsAsIssuerTurn);
}
/* ------------------------------------------------------------------------ */
/*
* Try an implicit action.
*
* Returns true if the action was attempted, whether or not it
* succeeded, nil if the command was not even attempted. We will not
* attempt an implied command that verifies as "dangerous," since this
* means that it should be obvious to the player character that such a
* command should not be performed lightly.
*/
_tryImplicitAction(issuingActor, targetActor, msgProp, actionClass, [objs])
{
local action;
/* create an instance of the desired action class */
action = actionClass.createActionInstance();
/* mark the action as implicit */
action.setImplicit(msgProp);
/* install the resolved objects in the action */
action.setResolvedObjects(objs...);
/*
* For an implicit action, we must check the objects involved to make
* sure they're in scope. If any of the objects aren't in scope,
* there is no way the actor would know to perform the command, so
* the command would not be implied in the first place. Simply fail
* without trying the command.
*/
if (!action.resolvedObjectsInScope())
return nil;
/*
* catch the abort-implicit signal, so we can turn it into a result
* code for our caller instead of an exception
*/
try
{
/* in NPC mode, add a command separator before each implied action */
if (targetActor.impliedCommandMode() == ModeNPC)
gTranscript.addCommandSep();
/* execute the action */
action.doAction(issuingActor, targetActor, nil, nil);
/*
* if the actor is in "NPC" mode for implied commands, do some
* extra work
*/
if (targetActor.impliedCommandMode() == ModeNPC)
{
/*
* we're in NPC mode, so if the implied action failed, then
* act as though the command had never been attempted
*/
if (gTranscript.actionFailed(action))
{
/* the implied command failed - act like we didn't even try */
return nil;
}
/*
* In "NPC" mode, we display the results from implied
* commands as though they had been explicitly entered as
* separate actions. So, add visual separation after the
* results from the implied command.
*/
gTranscript.addCommandSep();
}
/* tell the caller we at least tried to execute the command */
return true;
}
catch (AbortImplicitSignal sig)
{
/* tell the caller we didn't execute the command at all */
return nil;
}
catch (ParseFailureException exc)
{
/*
* Parse failure. If the actor is in NPC mode, we can't have
* asked for a new command, so this must be some failure in
* processing the implied command itself; most likely, we tried
* to resolve a missing object or the like and found that we
* couldn't perform interactive resolution (because of the NPC
* mode). In this case, simply treat this as a failure of the
* implied command itself, and act as though we didn't even try
* the implied command.
*
* If the actor is in player mode, then we *can* perform
* interactive resolution, so we won't have thrown a parser
* failure before trying to solve the problem interactively. The
* failure must therefore be in an interactive response. In this
* case, simply re-throw the failure so that it reaches the main
* parser.
*/
if (targetActor.impliedCommandMode() == ModeNPC)
{
/* NPC mode - the implied command itself failed */
return nil;
}
else
{
/* player mode - interactive resolution failed */
throw exc;
}
}
}
/* ------------------------------------------------------------------------ */
/*
* Run a replacement action.
*/
_replaceAction(actor, actionClass, [objs])
{
/* run the replacement action as a nested action */
_nestedAction(true, actor, actionClass, objs...);
/* the invoking command is done */
exit;
}
/* ------------------------------------------------------------------------ */
/*
* Resolve and execute a replacement action. This differs from the
* normal replacement action execution in that the action we execute
* requires resolution before execution.
*/
resolveAndReplaceAction(newAction)
{
/* prepare the replacement action */
prepareNestedAction(true, nil, newAction);
/*
* resolve and execute the new action, using the same target and
* issuing actors as the original action
*/
executeAction(gActor, nil, gIssuingActor, nil, newAction);
/* the invoking command has been replaced, so it's done */
exit;
}
/* ------------------------------------------------------------------------ */
/*
* Run an action as a new turn. Returns the CommandTranscript describing
* the action's results.
*/
_newAction(transcriptClass, issuingActor, targetActor, actionClass, [objs])
{
local action;
/* create an instance of the desired action class */
action = actionClass.createActionInstance();
/* execute the command with the action instance */
return newActionObj(transcriptClass, issuingActor, targetActor,
action, objs...);
}
/*
* Run an action as a new turn. This is almost the same as _newAction,
* but should be used when the caller has already explicitly created an
* instance of the Action to be performed.
*
* If issuingActor is nil, we'll use the current global issuing actor; if
* that's also nil, we'll use the target actor.
*
* Returns a CommandTranscript object describing the result of the
* action.
*/
newActionObj(transcriptClass, issuingActor, targetActor, actionObj, [objs])
{
/* create the results object and install it as the global transcript */
return withCommandTranscript(transcriptClass, function()
{
/* install the resolved objects in the action */
actionObj.setResolvedObjects(objs...);
/*
* if the issuing actor isn't specified, use the current global
* issuing actor; if that's also not set, use the target actor
*/
if (issuingActor == nil)
issuingActor = gIssuingActor;
if (issuingActor == nil)
issuingActor = targetActor;
/*
* Execute the given action. Because this is a new action,
* execute the action in a new sense context for the given actor.
*/
callWithSenseContext(targetActor.isPlayerChar()
? nil : targetActor, sight,
{: actionObj.doAction(issuingActor, targetActor, nil, nil)});
/* return the current global transcript object */
return gTranscript;
});
}
/* ------------------------------------------------------------------------ */
/*
* Run a nested action. 'isReplacement' has the same meaning as in
* execNestedAction().
*/
_nestedAction(isReplacement, actor, actionClass, [objs])
{
local action;
/* create an instance of the desired action class */
action = actionClass.createActionInstance();
/* install the resolved objects in the action */
action.setResolvedObjects(objs...);
/* execute the new action */
execNestedAction(isReplacement, nil, actor, action);
}
/*
* Execute a fully-constructed nested action.
*
* 'isReplacement' indicates whether the action is a full replacement or
* an ordinary nested action. If it's a replacement, then we use the
* game time taken by the replacement, and set the enclosing action
* (i.e., the current gAction) to take zero time. If it's an ordinary
* nested action, then we consider the nested action to take zero time,
* using the current action's time as the overall command time.
*
* 'isRemapping' indicates whether or not this is a remapped action. If
* we're remapping from one action to another, this will be true; for
* any other kind of nested or replacement action, this should be nil.
*/
execNestedAction(isReplacement, isRemapping, actor, action)
{
/* prepare the nested action */
prepareNestedAction(isReplacement, isRemapping, action);
/* execute the new action in the actor's sense context */
callWithSenseContext(
actor.isPlayerChar() ? nil : actor, sight,
{: action.doAction(gIssuingActor, actor, nil, nil) });
}
/*
* Prepare a nested or replacement action for execution.
*/
prepareNestedAction(isReplacement, isRemapping, action)
{
/*
* if the original action is an implicit command, make the new
* command implicit as well
*/
if (gAction.isImplicit)
{
/*
* make the new action implicit, but don't describe it as a
* separate implicit command - it's effectively part of the
* original implicit command
*/
action.setImplicit(nil);
}
/* mark the new action as nested */
action.setNested();
/* a nested action is part of the enclosing action */
action.setOriginalAction(gAction);
/* if this is a remapping, mark it as such */
if (isRemapping)
action.setRemapped(gAction);
/*
* Set either the nested action's time or the enclosing (current)
* action's time to zero - we want to count only the time of one
* command or the other.
*
* If we're running an ordinary nested command, set the nested
* command's time to zero, since we want to consider it just a part
* of the enclosing command and thus to take no time of its own.
*
* If we're running a full replacement command, and we're replacing
* something other than an implied command, don't consider the
* enclosing command to take any time, since the enclosing command is
* carrying out its entire function via the replacement and thus
* requires no time of its own. If we're replacing an implied
* command, this doesn't apply, since the implied command defers to
* its enclosing command for timing. If we're replacing a command
* that already has zero action time, this also doesn't apply, since
* we're presumably replacing a command that's itself nested.
*/
if (isReplacement && !gAction.isImplicit && gAction.actionTime != 0)
gAction.zeroActionTime();
else
action.actionTime = 0;
}
/* ------------------------------------------------------------------------ */
/*
* Run a previously-executed command as a nested action, re-resolving
* all of its objects to ensure they are still valid.
*/
nestedActionAgain(action)
{
/* reset the any cached information for the new command context */
action.resetAction();
/* mark the action as nested */
action.setNested();
action.setOriginalAction(gAction);
/*
* do not count any time for the nested action, since it's merely
* part of the main turn and doesn't count as a separate turn of its
* own
*/
action.actionTime = 0;
/* execute the command */
executeAction(gActor, nil, gIssuingActor, nil, action);
}
/* ------------------------------------------------------------------------ */
/*
* Run some code in a simulated Action environment. We'll create a dummy
* instance of the given Action class, and set up a command transcript,
* then invoke the function. This is useful for writing daemon code that
* needs to invoke other code that's set up to expect a normal action
* processing environment.
*/
withActionEnv(actionClass, actor, func)
{
local oldAction, oldActor;
/* remember the old globals */
oldAction = gAction;
oldActor = gActor;
try
{
/* set up a dummy action */
gAction = actionClass.createInstance();
/* use the player character as the actor */
gActor = actor;
/*
* execute the function with a command transcript active; obtain
* and return the return value of the function
*/
return withCommandTranscript(CommandTranscript, func);
}
finally
{
/* restore globals on the way out */
gAction = oldAction;
gActor = oldActor;
}
}
/* ------------------------------------------------------------------------ */
/*
* Exit signal. This signal indicates that we're finished with the
* entire command execution sequence for an action; the remainder of the
* command execution sequence is to be skipped for the action. Throw
* this from within the command execution sequence in order to skip
* directly to the end-of-turn processing. This skips everything
* remaining in the action, including after-action notification and the
* like. This signal skips directly past the 'afterAction' phase of the
* command.
*
* Note that this doesn't prevent further processing of the same command
* if there are multiple objects involved, and it doesn't affect
* processing of additional commands on the same command line. If you
* want to cancel further iteration of the same command for additional
* objects, call gAction.cancelIteration().
*/
class ExitSignal: Exception
;
/*
* Exit Action signal. This signal indicates that we're finished with
* the execAction portion of processing the command, but we still want
* to proceed with the rest of the command as normal. This can be used
* when a step in the action processing wants to preempt any of the more
* default processing that would normally follow. This skips directly
* to the 'afterAction' phase of the command.
*
* Note that this doesn't prevent further processing of the same command
* if there are multiple objects involved, and it doesn't affect
* processing of additional commands on the same command line. If you
* want to cancel further iteration of the same command for additional
* objects, call gAction.cancelIteration().
*/
class ExitActionSignal: Exception
;
/*
* Abort implicit command signal. This exception indicates that we are
* aborting an implicit command without having tried to execute the
* command at all. This is thrown when an implied command is to be
* aborted before it's even attempted, such as when verification shows
* the command is obviously dangerous and thus should never be attempted
* without the player having explicitly requesting it.
*/
class AbortImplicitSignal: Exception
;
/* ------------------------------------------------------------------------ */
/*
* Action Remap signal. This signal can be thrown only during the noun
* phrase resolution phase of execution, and indicates that we want to
* remap the action to a different action, specified in the signal.
*
* This is useful when an object is always used in a special way, so
* that a generic verb used with the object must be mapped to a more
* specific verb on the object. For example, a game with a generic USE
* verb might convert USE PAINTBRUSH ON WALL to PAINT WALL WITH
* PAINTBRUSH by remapping the UseWith action to a PaintWith action
* instead.
*/
class RemapActionSignal: Exception
construct(action)
{
/* remember the new action */
action_ = action;
}
/* the new action that should replace the original action */
action_ = nil
;
/*
* Remap a 'verify' method for a remapped action. This is normally
* invoked through the remapTo() macro.
*/
remapVerify(oldRole, resultSoFar, remapInfo)
{
local newAction;
local objs;
local idx;
local newRole;
/* extract new action's object list from the remapping info list */
objs = remapInfo.sublist(2);
/*
* Create a new action object. We only perform verification
* remapping during the resolution phase of the command processing,
* because once we've finished resolving, we'll actually replace the
* action with the remapped action and thus won't have to remap
* verification (or anything else) at that point. So, pass true for
* the in-resolve flag to the action creation routine.
*/
newAction = remapActionCreate(true, oldRole, remapInfo);
/*
* Find the object that's given as a resolved object, rather than as
* a DirectObject (etc) identifier - the one given as a specific
* object is the one that corresponds to the original object.
*/
idx = objs.indexWhich({x: dataType(x) == TypeObject});
/* get the role identifier (DirectObject, etc) for the slot position */
newRole = newAction.getRoleFromIndex(idx);
/* if we don't yet have a result list object, create one */
if (resultSoFar == nil)
resultSoFar = new VerifyResultList();
/* if we found a remapping, verify it */
if (idx != nil)
{
/*
* Remember the remapped object in the result list. Note that we
* do this first, before calling the remapped verification
* property, so that our call to the remapped verification
* property will overwrite this setting if it does further
* remapping. We want the ultimate target object represented
* here, after all remappings are finished.
*/
resultSoFar.remapAction_ = newAction;
resultSoFar.remapTarget_ = objs[idx];
resultSoFar.remapRole_ = newRole;
/* install the new action as the current action while verifying */
local oldAction = gAction;
gAction = newAction;
try
{
/* call verification on the new object in the new role */
return newAction.callVerifyProp(
objs[idx],
newAction.getVerifyPropForRole(newRole),
newAction.getPreCondPropForRole(newRole),
newAction.getRemapPropForRole(newRole),
resultSoFar, newRole);
}
finally
{
/* restore the old gAction on the way out */
gAction = oldAction;
}
}
else
{
/* there's no remapping, so there's nothing to verify */
return resultSoFar;
}
}
/*
* Perform a remapping to a new action. This is normally invoked
* through the remapTo() macro.
*/
remapAction(inResolve, oldRole, remapInfo)
{
local newAction;
/* get the new action */
newAction = remapActionCreate(inResolve, oldRole, remapInfo);
/*
* replace the current action, using the appropriate mechanism
* depending on the current processing phase
*/
if (inResolve)
{
/*
* we're still resolving the objects, so we must use a signal to
* start the resolution process over for the new action
*/
throw new RemapActionSignal(newAction);
}
else
{
/*
* We've finished resolving everything, so we can simply use the
* new action as a replacement action.
*/
execNestedAction(true, true, gActor, newAction);
/*
* the remapped action replaces the original action, so
* terminate the original action
*/
exit;
}
}
/*
* Create a new action object for the given remapped action.
*/
remapActionCreate(inResolve, oldRole, remapInfo)
{
local newAction;
local newObjs;
local newActionClass;
local objs;
/* get the new action class and object list from the remap info */
newActionClass = remapInfo[1];
objs = remapInfo.sublist(2);
/*
* create a new instance of the replacement action, carrying forward
* the properties of the original (current) action
*/
newAction = newActionClass.createActionFrom(gAction);
/* remember the original action we're remapping */
newAction.setOriginalAction(gAction);
/* set up an empty vector for the match trees for the new action */
newObjs = new Vector(objs.length());
/* remap according to the phase of the execution */
if (inResolve)
{
/* translate the object mappings */
foreach (local cur in objs)
{
/* check what we have to translate */
if (dataType(cur) == TypeEnum)
{
/*
* it's an object role - if it's the special OtherObject
* designator, get the other role of a two-object
* command
*/
if (cur == OtherObject)
cur = gAction.getOtherObjectRole(oldRole);
/*
* get the match tree for this role from the old action
* and add it to our list
*/
newObjs.append(gAction.getMatchForRole(cur));
}
else
{
/* append the new ResolveInfo to the new object list */
newObjs.append(gAction.getResolveInfo(cur, oldRole));
}
}
/* set the object matches in the new action */
newAction.setObjectMatches(newObjs.toList()...);
}
else
{
/* translate the object mappings */
foreach (local cur in objs)
{
/* check what we have to translate */
if (dataType(cur) != TypeEnum)
{
/* it's an explicit object - use it directly */
newObjs.append(cur);
}
else
{
/* it's a role - translate OtherObject if needed */
if (cur == OtherObject)
cur = gAction.getOtherObjectRole(oldRole);
/* get the resolved object for this role */
newObjs.append(gAction.getObjectForRole(cur));
}
}
/* set the resolved objects in the new action */
newAction.setResolvedObjects(newObjs.toList()...);
}
/* return the new action */
return newAction;
}
/* ------------------------------------------------------------------------ */
/*
* Result message object. This is used for verification results and
* main command reports, which must keep track of messages to display.
*/
class MessageResult: object
/*
* Construct given literal message text, or alternatively a property
* of the current actor's verb messages object. In either case,
* we'll expand the message immediately to allow the message to be
* displayed later with any parameters fixed at the time the message
* is constructed.
*/
construct(msg, [params])
{
/* if we're based on an existing object, copy its characteristics */
if (dataType(msg) == TypeObject && msg.ofKind(MessageResult))
{
/* base it on the existing object */
messageText_ = msg.messageText_;
messageProp_ = msg.messageProp_;
return;
}
/*
* if the message was given as a property, remember the property
* for identification purposes
*/
if (dataType(msg) == TypeProp)
messageProp_ = msg;
/*
* Resolve the message and store the text. Use the action's
* objects (the direct object, indirect object, etc) as the
* sources for message overrides - this makes it easy to override
* messages on a per-object basis without having to rewrite the
* whole verify/check/action routines.
*/
messageText_ = resolveMessageText(gAction.getCurrentObjects(),
msg, params);
}
/*
* Static method: resolve a message. If the message is given as a
* property, we'll look up the message in the given source objects
* and in the actor's "action messages" object. We'll return the
* resolved message string.
*/
resolveMessageText(sources, msg, params)
{
/*
* If we have more than one source object, it means that the
* command has more than one object slot (such as a TIAction,
* which has direct and indirect objects). Rearrange the list so
* that the nearest caller is the first object in the list. If
* one of these source objects provides an override, we generally
* want to get the message from the immediate caller rather than
* the other object. Note that we only care about the *first*
* source object we find in the stack trace, because we only care
* about the actual message generator call; enclosing calls
* aren't relevant to the message priority because they don't
* necessarily have anything to do with the messaging.
*/
if (sources.length() > 1)
{
/* look through the stack trace for a 'self' in the source list */
local tr = t3GetStackTrace();
for (local i = 1, local trCnt = tr.length() ; i <= trCnt ; ++i)
{
/* check this 'self' */
local s = tr[i].self_;
local sIdx = sources.indexOf(s);
if (sIdx != nil)
{
/*
* it's a match - move this object to the head of the
* list so that we give its message bindings priority
*/
if (sIdx != 1)
sources = [s] + (sources - s);
/* no need to look any further */
break;
}
}
}
/*
* The message can be given either as a string or as a property
* of the actor's verb message object. If it's the latter, look
* up the text of the property from the appropriate object.
*/
findTextSource:
if (dataType(msg) == TypeProp)
{
local msgObj;
/*
* Presume that we'll read the message from the current
* actor's "action message object." This is typically
* playerActionMessages or npcActionMessages, but it's up to
* the actor to specify which object we get our messages
* from.
*/
msgObj = gActor.getActionMessageObj();
/*
* First, look up the message property in the action's
* objects (the direct object, indirect object, etc). This
* makes it easy to override messages on a per-object basis
* without having to rewrite the whole verify/check/action
* routine.
*/
foreach (local cur in sources)
{
/* check to see if this object defines the message property */
if (cur != nil && cur.propDefined(msg))
{
local res;
/*
* This object defines the property, so check what
* we have.
*/
switch (cur.propType(msg))
{
case TypeProp:
/*
* It's another property, so we're being
* directed back to the player action message
* object. The object does override the
* message, but the override points to another
* message property in the action object message
* set. Simply redirect 'msg' to point to the
* new property, and use the same action message
* object we already assumed we'd use.
*/
msg = cur.(msg);
break;
case TypeSString:
/* it's a simple string - retrieve it */
msg = cur.(msg);
/*
* since it's just a string, we're done finding
* the message text - there's no need to do any
* further property lookup, since we've obviously
* reached the end of that particular line
*/
break findTextSource;
case TypeCode:
/*
* Check the parameter count - we'll allow this
* method to take the full set of parameters, or
* no parameters at all. We allow the no-param
* case for convenience in cases where the method
* simply wants to return a string or property ID
* from a short method that doesn't need to know
* the parameters; in these cases, it's
* syntactically a lot nicer looking to write it
* as a "prop = (expresion)" than to write the
* full method-with-params syntax.
*/
if (cur.getPropParams(msg) == [0, 0, nil])
res = cur.(msg);
else
res = cur.(msg)(params...);
/*
* If that returned nil, ignore it entirely and
* keep scanning the remaining source objects.
* The object must have decided it didn't want to
* provide the message override in this case
* after all.
*/
if (res == nil)
continue;
/* we didn't get nil, so use the result */
msg = res;
/*
* if we got a string, we've fully resolved the
* message text, so we can stop searching for it
*/
if (dataType(msg) == TypeSString)
break findTextSource;
/*
* It's not nil and it's not a string, so it must
* be a property ID. In this case, the property
* ID is a property to evaluate in the normal
* action message object. Simply proceed to
* evaluate the new message property as normal.
*/
break;
case TypeNil:
/*
* it's explicitly nil, which simply means to
* ignore this definition; keep scanning other
* source objects
*/
continue;
default:
/*
* In any other case, this must simply be the
* message we're to use. For this case, the
* source of the message is this object, so
* forget about the normal action message object
* and instead use the current object. Then
* proceed to evaluate the message property as
* normal, which will fetch it from the current
* object.
*/
msgObj = cur;
break;
}
/*
* we found a definition, so we don't need to look
* at any of the other objects involved in the
* action - we just use the first override we find
*/
break;
}
}
/* look up the message in the actor's message generator */
msg = msgObj.(msg)(params...);
}
/*
* format the string and remember the result - do the formatting
* immediately, because we want to make sure we expand any
* substitution parameters in the context of the current
* command, since the parameters might change (and thus alter
* the meaning of the message) by the time it's displayed
*/
msg = langMessageBuilder.generateMessage(msg);
/*
* "quote" the message text - it's fully expanded now, so
* there's no need to further expand anything that might by
* coincidence look like substitution parameters in its text
*/
msg = langMessageBuilder.quoteMessage(msg);
/* return the resolved message string */
return msg;
}
/*
* set a new message, given the same type of information as we'd use
* to construct the object
*/
setMessage(msg, [params])
{
/* simply invoke the constructor to re-fill the message data */
construct(msg, params...);
}
/*
* Display a message describing why the command isn't allowed.
*/
showMessage()
{
/* show our message string */
say(messageText_);
}
/* the text of our result message */
messageText_ = nil
/* the message property, if we have one */
messageProp_ = nil
;
TADS 3 Library Manual
Generated on 5/16/2013 from TADS version 3.1.3