I6: A door's before/after checked for ##Go action, but only if the command was >ENTER?

[Note that I’m using Inform 6.31 (10th Feb 2006), which may matter]

Is it expected behavior that a door object’s .before() routine will only be checked if the command was >ENTER DOOR and not if the command was >GO EAST (in cases where the door is to the east)?

I put together a little scenario to look at this, with .before() and .after() routines that just announce they’ve been run but return false to allow things to continue. These routines were placed on both room and door objects. For a >GO EAST command, the output looks like:

>e
<room.before(##Go)>
<room.after(##Go)>

Ending Point
An uninteresting room.

>

but if the command is instead >ENTER TUNNEL (the door object), the output looks like:

>enter tunnel
<room.before(##Enter)>
<tunnel.before(##Enter)>
<room.before(##Go)>
<tunnel.before(##Go)>  <-- door object's before checked for ##Go
<room.after(##Go)>
<tunnel.after(##Go)>  <-- door object's after checked for ##Go

Ending Point

>

As a final point of comparison, I set up a simple pushable object, and pushing the ball through the tunnel doesn’t check the door object’s .before() or .after() routines for the ##Go action, either:

>push ball east
<room.before(##PushDir)>
<ball.before(##PushDir)>
<room.after(##PushDir)>
<ball.after(##PushDir)>
<room.before(##Go)>
<room.after(##Go)>

Ending Point

>

Is there a reason for the inconsistency? If not, which is the correct behavior, i.e. should an ##Enter action not look at a door’s before/after routines for the automatic ##Go action, or should ##Go and ##PushDir actions look at doors’ before/after routines like ##Enter does?

Also, is the behavior different than the above for later versions of Inform 6?

1 Like

In case anyone wants the exact code used for the tests:

Class Room
    with    before
                [;
                    Enter:
                        print "<room.before(##Enter)>^";
                    Go:
                        print "<room.before(##Go)>^";
                    PushDir:
                        print "<room.before(##PushDir)>^";
                ],
            after
                [;
                    Enter:
                        print "<room.after(##Enter)>^";
                    Go:
                        print "<room.after(##Go)>^";
                    PushDir:
                        print "<room.after(##PushDir)>^";
                ],
    has light;

Class Doorway
    has static door;

Room Start "Starting Point"
    with    description
                "An uninteresting room.",
            e_to tunnel,
            u_to End;

Object -> ball "heavy ball"
    with    name    'heavy' 'exercise' 'ball',
            initial
                "A heavy exercise ball sits nearby.",
            before
                [;
                    Remove, Take:
                        "It's too heavy to carry. You could probably push it about.";
                    PushDir:
                        print "<ball.before(##PushDir)>^";
                        AllowPushDir();
                        return true;
                ],
            after
                [;
                    PushDir:
                        print "<ball.after(##PushDir)>^";
                ];

Doorway tunnel
    with    name    'tunnel',
            door_dir
                [;
                    if (location == Start)
                        return e_to;
                    else
                        return w_to;
                ],
            door_to
                [;
                    if (location == Start)
                        return End;
                    else
                        return Start;
                ],
            before
                [;
                    Enter:
                        print "<tunnel.before(##Enter)>^";
                    Go:
                        print "<tunnel.before(##Go)>^";
                    PushDir:
                        print "<tunnel.before(##PushDir)>^";
                ],
            after
                [;
                    Enter:
                        print "<tunnel.after(##Enter)>^";
                    Go:
                        print "<tunnel.after(##Go)>^";
                    PushDir:
                        print "<tunnel.after(##PushDir)>^";
                ],
            found_in Start End
    has     scenery open;

Room End "Ending Point"
    with    description
                "An uninteresting room.",
            w_to tunnel,
            d_to End;


[ Initialise ;

    location = Start;

];

It’s been a while since I used I6 seriously, but yes – you’re expected to catch the Go action for the room, not the door. Alternatively, you can put code in the door’s door_to property routine.

@zarf, OK. Thanks.

Clearly, it’s possible to get a particular effect of interference from a door in more than one way, and I can accept skipping the door’s .before() routine for ##Go as a generic design principle, but why is the behavior of the Standard Rules different in the case of an action that starts as an ##Enter and gets translated into a ##Go? Is there some positive motivation for it, or is it a leftover of the evolution of Inform to that point, or what?

I’m going with “evolution of Inform”.

The BeforeRoutines() function calls before properties on the noun and the room. For the command ENTER TUNNEL, the noun is the tunnel; for GO EAST, the noun is the compass direction. So that’s what gets called. This happens before the EnterSub() / GoSub() processing which converts ENTER TUNNEL into a GO EAST action.

I think the other half of the explanation is that enter tunnel is not converted into go east (as you seem to be assuming), but rather into go tunnel. This is why you’re seeing before/after still be called for the ##Go action on the tunnel, because that’s still the noun. (Conversely, in go east it doesn’t call those for the tunnel because it’s not the noun.)

Note that this is an assumption (I don’t have an I6 compiler handy at the moment), but you can easily try it out: compare it with the output of go tunnel.

GO TUNNEL is parsed as ##Enter tunnel, not ##Go tunnel.

The ##Go action can only apply to a door during the library’s internal processing – when EnterSub converts it that way. This is, again, a historical peculiarity.

So then why does tunnel.before(##Go) get called, if the tunnel is no longer the noun at that point?

Like I just said, at that point in the internal processing, the ##Go action does apply to the door (the tunnel).

You can see that EnterSub starts with this line:

    if (noun has door || noun in compass) <<Go noun>>;

If the noun is tunnel, this generates an “artificial” ##Go action which applies to the tunnel. The game grammar doesn’t allow that, but this line does.

1 Like

@zarf and @mirality, Thanks to both of you for the details here. I was, indeed, thinking that the ##Enter action was converted directly to a ##Go action with the noun changed to the door’s door_dir() result. As zarf points out, the cause for the unexpected behavior is right there at the start of EnterSub(), even if the reason for the cause isn’t clear.

So, if the command is >GO DOOR, the sequence of execution is:

  1. parser generates action
    1a. room.before(##Enter) with noun door
    1b. door.before(##Enter) with noun door
  2. EnterSub() transforms this to
    2a. origin room.before(##Go) with noun door
    2b. door.before(##Go) with noun door
  3. GoSub() handles transition to new room
    3a. destination room.after(##Go) with noun door
    3b. door.after(##Go) with noun door

This is borne out by adjustment of the test code to also print the noun, and the behavior appears to be in contradiction with what’s stated in the DM4 at the bottom of p. 123 and the top of p. 124:

“In the Corridor above, the player might type ‘s’ or ‘go south’, causing the action Go s_obj. Or might ‘enter stone door’ or ‘go through door’, causing Enter StoneDoor. Provided the door is actually open, the Enter action then looks at the door’s door_dir property, finds that the door faces south and generates the action Go s_obj. Thus, provided that the door is open, the outcome is the same and you need only write code to trap the Go action.” (emphasis in original)

The tunnel has attribute open in this example, so I can see why I was expecting the noun being switched to the associated door_dir instead of the door. Should this be considered to be an Inform 6 bug in the structure of EnterSub(), given that the DM4 text seems to support the naive (and less confusing to work with) interpretation informed by the text itself?

If EnterSub() is to stay as is, it seems worthwhile to add this item to the list of published errata (and also the fact that class inheritance priority is the reverse of what’s plainly stated on p. 61). Is anybody maintaining that list at the moment?

Ok, but that’s what I said. Perhaps wrong on the testing by hand part, but the rest was correct.

I’m not sure how easy it would be to verify this, but it’s possible that this also invokes s_obj.before(##Go) as well, which would make the DM4 statement correct, albeit omitting an intermediate step.

But I suspect that it’s usually intended to intercept ##Go at the room-level anyway, which is what DM4 probably really meant.

@mirality, I’m pretty sure that <Go (dir)> is never executed from a <Go (door)> action. Looking directly at the definition of GoSub() in verblibm.h (and hoping that I’m reading it correctly):

[ GoSub i j k df movewith thedir old_loc;

    ! first, check if any PushDir object is touchable
    if (second ~= 0 && second notin Compass && ObjectIsUntouchable(second)) return;

    old_loc = location;
    movewith = 0;
    i = parent(player);
    if ((location ~= thedark && i ~= location) || (location == thedark && i ~= real_location)) {
        j = location;
        if (location == thedark) location = real_location;
        k = RunRoutines(i, before); if (k ~= 3) location = j;
        if (k == 1) {
           movewith = i; i = parent(i);
        }
        else {
            if (k == 0) L__M(##Go,1,i);
            rtrue;
        }
    }

    thedir = noun.door_dir;  **<-- checks for presence of door_dir property in noun**
    if (ZRegion(thedir) == 2) thedir = RunRoutines(noun, door_dir); **<-- runs door_dir if it is a routine**

    j = i.thedir; k = ZRegion(j); **<-- gets room.dir_to based on door_dir result**
    if (k == 3) { print (string) j; new_line; rfalse; }
    if (k == 2) {
        j = RunRoutines(i,thedir);
        if (j==1) rtrue;
    }

    if (k == 0 || j == 0) {
        if (i.cant_go ~= 0 or CANTGO__TX) PrintOrRun(i, cant_go);
        else L__M(##Go,2);
        rfalse;
    }

    if (j has door) { **<-- if object in loc.dir_to is a door...**
        if (j has concealed) return L__M(##Go, 2);
        if (j hasnt open) {
            if (noun == u_obj) return L__M(##Go, 3, j);
            if (noun == d_obj) return L__M(##Go, 4, j);
            return L__M(##Go, 5, j);
        }
        k = RunRoutines(j,door_to); **<-- execute door's door_to to find destination (stored in k)**
        if (k == 0) return L__M(##Go, 6, j);
        if (k == 1) rtrue;
        j = k; <-- if door_to didn't stop things, now k result placed in j
    }
    if (movewith == 0) move player to j; else move movewith to j; **<-- relocate player/vehicle to j, no <Go (dir)> used!**

    location = j; MoveFloatingObjects();
    df = OffersLight(j);
    if (df ~= 0) { location = j; real_location = j; lightflag = 1; }
    else {
        if (old_loc == thedark) {
            DarkToDark();
            if (deadflag ~= 0) rtrue;
        }
        real_location = j;
        location = thedark; lightflag = 0;
    }
    if (AfterRoutines() == 1) rtrue;
    if (keep_silent == 1) rtrue;
    LookSub(1);
];

The way this is structured now, intervention at the level of the room requires extra vigilance, as the noun can be either the direction object or the door, depending on the action resulting from parsing the command. As zarf mentioned initially, intervention at the point of door_to() would be reliable regardless of how the door was reached.

I noticed today that in Table 6B (“ACTIONS PROVIDED BY THE LIBRARY : GROUP 2”) on p. 525, there is a row:

Enter           "enter cage"            KS  can become Go if into a door

so there is technically some documentation of EnterSub()'s behavior, though it should probably say “will become Go…”, since it seems like an ##Enter action on a door will always become a ##Go action.