inconsistent results intercepting GiveTo command (adv3Lite)

Harry and Max are in the kitchen. Harry takes a fork then gives it to Max. I want to intercept the action and provide my own text rather than accept the system-generated “Max does not respond,” without having to implement a GiveTopic.

So, I have a Doer that works as desired when the command is entered as give fork to max but not when the command is give max the fork even though both forms equate to the GiveTo command, according to the action reference table in the Adv3Lite Library Manual.

Here’s a transcript…

…and the code that produced it…

#charset "us-ascii"

#include <tads.h>
#include "advlite.h"

versionInfo: GameID
    IFID = '445C38A3-AD1B-4729-957A-F584600DE5C1'
    name = 'test'
    byline = 'by Jerry Ford'
    htmlByline = 'by <a href="mailto:jerry.o.ford@gmail.com">
                  Jerry Ford</a>'
    version = '1'
    authorEmail = 'Jerry Ford <jerry.o.ford@gmail.com>'
    desc = 'Testing give to.'
    htmlDesc = 'Testing give to.'

;

gameMain: GameMainDef
    initialPlayerChar = harry
    paraBrksBtwnSubcontents = nil
   
;

kitchen: Room 'Kitchen' 'kitchen'
    "The kitchen <.p>"
    
;
+ fork: Fork 'fork;;silverware'
    "A row of forks, tines of silver, handles of carved ivory, joined
    knives, forks and plates on the table next to the chafing dishes.
    <.p>"
    isListed = nil
    dobjFor(Drop)
    {
        verify()
        {
            if(Fork.isIn(harry))
                abort;
        }
    }
;
class Fork: Thing 'fork'
    "The fork tines were silver, the handled carved ivory. <.p>"
;

//*********** Max Character *********************************

harrysSonMax: Actor 'Max;teenager teen age ager;son;him' @kitchen
    ""
        
    globalParamName = 'max'
    person = 3   
    bulkCapacity = 5000
    attentionSpan = 3
;
// harry, main character
harry: Actor 'Harry;;man self;him' @kitchen
    ""
    globalParamName = 'harry'
    person = 3   
    bulkCapacity = 5000
    
;

Doer 'give Thing to Actor'
    execAction(cmd)
    {
        "text from 'give Thing to Actor' Doer";
    }
;
Doer 'give Actor Thing'
    execAction(cmd)
    {
        "text from 'give Actor Thing' Doer";
    }
;

Any insights appreciated.

Jerry

Is there some reason for not simply using a DefaultGiveTopic to provide your custom response?

Another possibility, if you want to override the default no response message for all conversational commands, is to override Max’s noResponseMsg property.

I don’t want to override the no response command used everywhere in the game with a reply that is suitable only for the offering of a fork in the kitchen.

I want flexibility to say one thing in the kitchen and quite a different thing for something else in some other location.

Also, there are several NPCs in the game in addition to Max. I’d like to manage the no response text for all of them in a single, central location such as a Doer rather than have to override messages for each character individually.

jerry

Then perhaps the easiest way to handle it would be in the roomBeforeAction() method of the kitchen:

kitchen: Room 'Kitchen' 'kitchen'
    "The kitchen <.p>"
    
    roomBeforeAction()
    {
        if(gActionIs(GiveTo))            
        {
            "Text from Give <<gDobj.theName>> to <<gIobj.theName>>";
            exit;
        }
    }
    
;

I don’t think associating the Give action with the room will work. In my test bed environment cited above, it’s just Harry and Max in the kitchen, and the only thing available to give is a fork.

But in the game, Harry is actually at a pool party, the buffet table has knives, forks, spoons, plates and hors d’ouerves. And Max isn’t even present yet. Harry has to go upstairs to get him.

It’s not much of a stretch to imagine a game player picking up one or all of the items, carrying it upstairs, and then offering it to Max.

So the action has to transcend locations.

If I can’t do it in a Doer, then perhaps your suggestion is the way to go, just not a roomBeforeAction() but instead in a function like this…

giveKitchenwareTo()
{
    say(gIobj.name);
    " did not take the ";
    say(gDobj.name);
    if(gIobj.isHim)
        ", he just stared sullenly at the ground. ";
    else
        ". <.p>";
    abort;
}

…called from beforeAction() in the Harry object…

    beforeAction()
    {
        if(gActionIs(GiveTo))
        {
            if(gDobj.ofKind(Knife) ||
               gDobj.ofKind(Fork) ||
               gDobj.ofKind(Spoon) ||
               gDobj.ofKind(Plate))
            {
                giveKitchenwareTo();
            }
        }
    }

This not only takes care of the response in any room in the game, it also covers the perhaps unlikely but not inconceivable possibility of Harry carrying a plate around with him until he meets up with Grace and offering it to her at some distant location.

I still think it might be going more with the grain of the system to do this with a GiveTopic. If this means you effectively need the same GiveTopic on several actors then you could just subclass GiveTopic accordingly and add an instance of your subclass to each NPC that needs it, e.g.:

class UtensilGiveTopic: GiveTopic [knife, fork, spoon, plate]
   "Blah blah"
   isActive = (whateverConditionYouLike)
;

Then you just add

+ UtensilGiveTopic
;

To all the relevant NPCs.

That said, I’ve now had a chance to investigate why the Doer approach didn’t work (since it clearly should have done) and come up with a fix. First off, you shouldn’t need the following Doer at all:

//Doer 'give Actor Thing'
//    execAction(cmd)
//    {
//        "text from 'give Actor Thing' Doer";
//    }
//;

Secondly, I found that it does work as expected if you change the other Doer to:


Doer 'give Thing to Thing'
    execAction(cmd)
    {
        "text from 'give Thing to Actor' Doer";
    }
;

You shouldn’t have to do this, of course, but it does provide a clue to what’s going wrong. The problem is that when you issue the command in the form GIVE MAX FORK the list that’s passed to the Doer to match has the direct and indirect objects in the wrong order; the routine that’s looking for a matching Doer is effectively trying to match GIVE MAX TO FORK.

This clearly needs fixing, and a fix that seems to work (but see the revised fix in the next post) is to amend the execDoer(lst) method of the Command class (in command.t, around line 449) so that it reads as follows:

execDoer(lst)
    {
        /* first ensure that lst is correctly sorted in predicate role order */
        local sortedLst = [lst[1]];
        
        if(dobj)
            sortedLst += dobj;
        if(iobj)
            sortedLst += iobj;
        if(acc)
            sortedLst += acc;
                
        lst = sortedLst;
        
        
        /* find the list of matching Doers */
        local dlst = DoerCmd.findDoers(lst);
      
        IfDebug(doers, oSay('''[Executing Doer; cmd = '<<dlst[1].cmd>>']\n'''));       
        dlst[1].exec(self);
        
    }

Then your original Doer should work fine, i.e.:

Doer 'give Thing to Actor'
    execAction(cmd)
    {
        "text from 'give Thing to Actor' Doer";
    }
;

I’m just a little nervous of this change since it appears to make the lst parameter and the whole means by which it’s constructed further back in the call chain virtually redundant. This is part of the library I inherited from Mike Roberts’s Mercury code, not part of adv3Lite I wrote myself, so although I can see what it’s doing, I don’t know why it’s doing it the way it is, so it’s just possible I’m breaking something else with this fix (although I can’t immediately see why I should be). At a guess, I’d say that Mike’s code is trying to be as general as possible while mine assumes the existence of specific predicate roles. I may therefore go back and see if a more generalized fix is either necessary or desirable.

EDIT: I’ve now done this. See next post for a preferred solution.

I’ve now taken a further look at this and come up with a fix I’m a bit more comfortable with. It effectively does the same thing, but it’s a bit more generalized and rather more in the spirit of Mike Roberts’s original code.

Instead of the change to Command.execDoer() given in the previous post, make the following change to the execCombos() method of Command in command.t at around line 372:

 execCombos(predRoles, n, lst)
    {
        /* get this slot's role */
        local role = predRoles[n];

        /* iterate over the objects in the slot at this index */
        foreach (local obj in self.(role.objListProp))
        {
            /* set the current object and selection flags for this role */
            self.(role.objProp) = obj.obj;
            self.(role.objMatchProp) = obj;
            
            /* 
             *   get the index at which the new object needs to be placed in the
             *   list.
             */
            local idx = role.order + 1;
            
            /* 
             *   create a new list that includes the new object at the
             *   appropriate place.
             */            
            local nlst = lst;
            
            /* Pad out nlst to the length required */
            while(nlst.length < idx)
                nlst += nil;
            
            /* insert the new object at the index appropriate to its role */
            nlst[idx] = obj.obj;

            /* 
             *   if there are more noun roles, recursively iterate over
             *   combinations of the remaining roles 
             */
            if (n < predRoles.length())
            {
                /* we have more roles - iterate over them recursively */
                execCombos(predRoles, n+1, nlst);
            }
            else
            {
                /* 
                 *   this is the last role - we have a complete combination
                 *   of current objects now, so execute the action with the
                 *   current set 
                 */
                execIter(nlst);
            }
        }
    }

Great. Thanks. Your new execCombo() method appears to fix the Doer problem, and it seems to work as expected now (I hedge with “appears” and “seems” only because I have just now implemented your fix and have done all of two minutes of testing, but it does look good).

I also appreciate your advice about using a subclass of GiveTopic to get the job done the TADS way. You make a good case and some further tinkering on my part appears warranted.

Jerry