Implicit door opening (adv3Lite)

I’ve tried to sort through the library code, but this one baffles me. Here’s the game output:

>pull wagon w
(first trying to open the door)
The door is locked. 
The little red wagon opens the door. (first trying to open the door)
The door is locked.

Not much point blurring that. The fact that there’s a little red wagon in the game is not a spoiler. The problem is that the library is trying to do an implicit Open action using each traveler as the source of the action. But I’m unable to find any library code that tries an implicit open action on a door. There’s something about implicit opening of a closed container, but it seems to be handled by checkPreCond, and I’m not finding any precondition object that asks for a door to be open.

The list of preconditions in the Action Results page of the Library Manual lists containerOpen as a precondition, but there’s no objOpen or doorOpen precondition, as far as I can see.

How can I defeat the parser’s attempt to let the wagon open the door?

I think when there’s a door in the way, then the action will be PushTravelThrough. In this case, the door is the indirect object for that action. In thing.t, it’s implemented like this:

iobjFor(PushTravelThrough)
{
    preCond = [touchObj]
    verify() 
    {  
        if(!canGoThroughMe || destination == nil)
            illogical(cannotPushThroughMsg);
    }
    
    check() { checkPushTravel(); }       
}

Looking at checkPushTravel():

/* Check the travel barriers on the indirect object of the action */
checkPushTravel()
{
    checkTravelBarriers(gDobj);
    checkTravelBarriers(gActor);
}

This seems to be responsible for the double check. The gDobj is the wagon, in this case.

So, we can override checkPushTravel() on the door and take out the check for the gDobj, like this:

+ lab2door: Door 'lab door'
    otherSide = lab2doorOtherside
    isLocked = true
    
    checkPushTravel()
    {
        checkTravelBarriers(gActor);
    }
;

This results in:

The Lab

This is a testing lab. The corridor is to the north. Lab 2 is to the west, through a door.

You can see a cart and a trolley here.

>push trolley w
(first trying to open the lab door)
The lab door is locked.

(Which is now just the one attempt by the player character to open the door, as desired.)

2 Likes

Perfect – many thanks! I do wish TADS had the nice “actions” command for testing that’s available in Inform. That would have got me further along in the analysis, at least.

Can you describe what the actions command in Inform does?

It prints out debugging information. After the player types ACTIONS, each player command will show what action was attempted, and also the result (that it succeeded or failed). The rules command gives even more information, but since TADS isn’t rule-based, it’s hard to see how that could be adopted into TADS.

I use a pretty simple toggleable testing routine (in adv3), that can print something like this after every command:

gPlayerChar.lastAction = {obj:predicate(Examine)}
gAction = {obj:predicate(MoveTo)}
isImplicit = nil
isRemapped = true
originalAction = {obj:predicate(Take)}
parentAction = {obj:predicate(Take)}
gDobj = yardGate
gIobj = me
getEnteredVerbPhrase = 'take (dobj)'
gAction.grammarInfo()[1] = 'predicate(MoveTo)'
gTranscript.isFailure = true

Is that anything like what the ACTIONS command does? If you want it, I can send you my source…

It’s different, but I think it could be very useful. If you could email the source to me (midiguru23@sbcglobal.net), I’ll give it a try.

Does Lite use the same action processing methods as adv3? afterActionMain, doActionOnce etc? My code is all geared for adv3…

I don’t know. Maybe not. I was planning to analyze the algorithm and try to work that out.

I’ll email it as is, and you can let me know if it’s unintelligible or portable…

I thought that would work, but it doesn’t. It eliminates the message, but what happens is that the player is still in the former location (as is the wagon, of course). Now I get this:

>pull wagon sw
(first opening the shop door)

There has been no travel, just an implicit door-opening.

Hi Jim, good catch! Sorry for that; I was happy to find a (sort of) solution for the immediate issue when the door is locked, but simply didn’t think to test the case when it’s not locked.

The behaviour you describe (that neither the player nor the pushed/pulled object actually travel after having done the implicit door-opening) also happens in unmodified adv3Lite, without my suggestion above in this thread.

I used the debugger and breakpoints to trace the execution, and I think I know what’s causing this, but I don’t know enough about TADS and adv3Lite to suggest a good solution.

When we’re attempting to push/pull the object through the door, we’re landing, after quite a long chain of function calls, in the function check(obj, checkProp) (defined in action.t), with obj at that moment being the door and checkProp being checkIobjPushTravelThrough. Inside of that function, we encounter this:

/* Run the check method on the object and capture its output */
try
{
    checkMsg = gOutStream.captureOutputIgnoreExit({: obj.(checkProp)});
}

In the course of running that check method on the object, TADS will see that the door is closed, and will open it implicitly. In adv3Lite without my modification above, this will result in the pushed object opening the door. With my modification, it will result in the player character opening the door.

Since this is just a sort of “test run”, the output is not displayed yet, but captured, just as the code comment says.

The output will either be something like “The trolley opens the lab door.” if it was the pushed object which opened the door, or the classic “(first opening the lab door)” if it was the player character. In any case, the output will not be empty in this constellation, and consequently checkMsg will not be empty.

Now comes the crucial part: Further down in the check(obj, checkProp) function, we arrive at

/* 
 *   If the check method tried to display something then it wants to
 *   block the action, so we display the failure message and stop the
 *   action.
 */
if(checkMsg not in (nil, ''))
{
    [...]
    
    /* Display our failure message */
    say(checkMsg);
    
    /* Note that the action failed. */
    actionFailed = true;
    
    /* 
     *   Return nil to tell our caller this action failed the check
     *   stage
     */
    return nil;
}

So the output that was built by obj.(checkProp) and assigned to checkMsg earlier, in the line checkMsg = gOutStream.captureOutputIgnoreExit({: obj.(checkProp)});, will now make TADS think that the action failed and should be blocked.

And so the innocuous information that’s stored in checkMsg (“The trolley opens the lab door.” or “(first opening the lab door)”) is now considered to be a failure message. The interpreter then displays the message, and the rest of the action is not executed; that is why neither the player character nor the pushed object actually travel.

One can verify that the problem lies in the output during that check phase by turning off implicit actions reporting like this:

modify Open
    reportImplicitActions = nil
;

and also inserting the modification from above on our door object

checkPushTravel()
    {
         checkTravelBarriers(gActor);
    }

because otherwise “The trolley opens the door.” (resulting from the trolley’s visible door-opening) will be counted as check phase output, causing the action to be considered a failure.

So, with both of those modifications in place, the player and the pushed object will actually travel (after opening the closed-but-not-locked door without a message being displayed):

>push trolley w
You push the trolley through the lab door.

Lab 2

The trolley comes to a halt.

But of course, this is just for testing, and is not a good solution in general, because we want to keep the implicit action reports.

It seems like a bug for Eric to look into, or maybe other adv3Lite users can help.

Ah, well. I don’t know if he’s keeping up with activity on the forum, but I’ll suggest that he take a look at it.

1 Like