[I6] ObjectIsUntouchable() returning incorrect results?

While playing around with the information in DM4 Section 32 “Scope and what you can see”, I ran into some odd behavior that took me a while to track down. The root cause seems to be that the routine ObjectIsUntouchable() doesn’t really function in the way that its name implies. I’m working with Standard Library 6/11 in Inform 6.31, but the code appears to be the same in the current release (6.12.4).

Here’s a scenario to demonstrate:

Constant Story "Hat Trick";
Constant Headline "^(unexpected I6 behavior)^";

Include "Parser";
Include "VerbLib";
Include "Grammar";

Class Room
    has light;

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

Room End "Ending Point"
    with    description
                "An uninteresting room.",
            w_to Start
    has     ~light;

Object hat "hat" End
    with    name 'hat'
    has     clothing;

Object monitor "monitor" selfobj
    with    name 'monitor',
            curr_obj hat,
            description
                [;
                    print "^*****^";
                    print "<TestScope(", (name) self.curr_obj, ", player) = ", (TorF) TestScope(self.curr_obj, player), ">^";
                    print "<CommonAncestor(player, ", (name) self.curr_obj, ") = ", (name) CommonAncestor(player, self.curr_obj), ">^";
                    print "<*ObjectIsTouchable(", (name) self.curr_obj, ") = ", (TorF) ~~ObjectIsUntouchable(self.curr_obj), ">^";
                    print "<*ObjectIsTouchable(", (name) self.curr_obj, ", true, true) = ",
                        (TorF) ~~ObjectIsUntouchable(self.curr_obj, true, true), ">";
                    print "^*****^";
                ],
            react_before
                [;
                    Jump: return self.description(); ! for use in darkness
                ];

[ TorF p ;
    if (p)
        print "TRUE";
    else
        print "FALSE";
];


[ Initialise ;

    location = Start;

];

Note that the monitor’s code inverts the result of ObjectIsUntouchable() to get the equivalent of an ObjectIsTouchable() routine.

As can be seen with >X MONITOR at the start, the hat, despite being in a different room, registers as touchable.

This may not seem like a big deal, for two reasons:
1) Trying to interact with the hat fails anyway, because it’s not in scope.
2) The correct result appears if a second flag (which is unmentioned in DM4) is supplied.

However, the counter arguments are that:
A) Nearly every use of ObjectIsUntouchable() in the Standard Library (e.g. ActionSub routines) calls the routine with only the object parameter, and no flags.
B) The routine not doing “what it says on the tin” and in accordance with the provided documentation seems problematic.
C) Changing scope results in an apparently touchable (if not takeable) object at a distance.

To demonstrate C, add:

[ InScope person ;
    PlaceInScope(hat);
]; ! place before Include "Grammar"; line

then, from the starting position, try >X HAT (which works as expected) and >TOUCH HAT (which doesn’t).

Is there something I’m missing to explain this behavior? If not, does anyone know if the current version of I6 shows the same behavior? If the behavior is the same, does this count as a bug, or is this known but just accepted that that’s how things are in I6?

1 Like

I believe things that are in scope are generally assumed to be touchable unless Inform can prove that there’s a barrier (such as a closed transparent container) in the way.

Every use of ObjectIsUntouchable() in the library is in an action routine, and thus can only be called on objects in scope.

If you’ve put objects in other rooms in scope (by messing with scope), are they potentially touchable as well? I suppose there’s no general answer. You could use InScope to add your head to scope – that’s always touchable. Objects in other rooms might be in scope because you are psychically aware of them, or because a wormhole is open.

The DM4 definition says

Determines whether any solid barrier, that is, any container that is not open , lies between the player and obj.

The pedant would note that this doesn’t say anything about rooms, only about containers.

Rooms don’t intrinsically count as solid barriers – two rooms might be part of a longer hallway, or an open field, or a large laboratory divided into smaller sub-areas. But in general they constrain touchability by constraining scope.

I7 has means to say that something is additionally in scope but not touchable; I’m not sure what the I6 equivalent would be.

Yes, I can see the logic. The underlying issue seems to be that the I6 concept of “in scope” doesn’t really differentiate between visibility and touchability.

Thank you for the clarification.

@zarf, I’ve been pondering your explanation above. While it makes sense, I’m finding myself with a new question: If there’s no inherent difference between being in scope and being touchable, why does ObjectIsUntouchable() have its own logic for determining touchability instead of making use of TestScope()?

It’s the other way around – for an object to be touchable, it must be both in scope and ObjectIsUntouchable must return false.

To put that another way, ObjectIsUntouchable assumes that its input is already in scope – it will not return the correct answer if you pass it an out-of-scope parameter.

To put that a third way, ObjectIsUntouchable's job is to report which of the in-scope objects are not touchable.

1 Like

@mirality, thank you for that – it illuminates the sense behind how things are working in the lower layers. It looks like what I’m searching for with respect to an I6 touchability test is best obtained (at least for the player object) by ( TestScope(obj) && (~~(ObjectIsUntouchable(obj, true, true))) ), which is easy enough.

@zarf, I’m still thinking about this, and I have another question: Given your logic about the non-differentiation of “in scope” vs. “touchable”, what is the reasoning behind ObjectIsUntouchable(obj) and ObjectIsUntouchable(obj, true, true) returning different results (for a normally takeable object, i.e. one that is not a component part, not held by other person, etc.) when the object is in another room?

They are not the same concept. As mirality said, touchability is only considered for objects in scope.

The second flag for ObjectIsUntouchable() is commented as “also apply Take/Remove restrictions”. Since it’s undocumented, that’s all the explanation I’ve got.

@zarf, I appreciate your responses so far, and I hope that you have the patience to bear with me for one more post on this matter. (And note that I’m not asking about the same question as at the top of the thread, which was based on a misunderstanding of the purpose of ObjectIsUntouchable(). This question is about whether the routine is working for its intended purpose consistently in its two modes – with and without take_flag being set.)

First, I should have included “… in cases where there is no intervening closed container” in the sentence that you quoted above.

Second, it turns out that the second flag is not undocumented, after all. it’s discussed on p. 95 of the DM4. (I had forgotten.) The passage there says:

"But an alternative is to call the library’s routine:

ObjectIsUntouchable(item, silent_flag, take_flag)

This determines whether or not the player can touch item, returning true if there is
some obstruction. If silent_flag is true, or if there’s no obstruction anyway, nothing
will be printed. Otherwise a suitable message will be printed up, such as ‘The barred
cage isn’t open.’"

and also

“If you set take_flag, then a further restriction will be imposed: the item must not belong to something or someone already: specifically, it must not be in the possession
of an animate or a transparent object that isn’t a container or supporter. For
instance, the off button on a television set can certainly be touched, but if take_flag
is true, then ObjectIsUntouchable will print up ‘That seems to be a part of the
television set.’ and return true to report an obstacle.”

But, for the example code above, the interaction is:

>X HAT
You see nothing special about the hat.

>TOUCH IT
You feel nothing unexpected.

>TAKE IT
That isn't available.

So… I’m trying to follow the logic of the system as the Standard Library seems to work now:

  1. “Untouchable” in the context of ObjectIsUntouchable() means only “a closed container is between the player object and the object in question”.

  2. “Untouchable” in the context of ObjectIsUntouchable(true, true) means either “a closed container is between the player object and the object in question” or “there is no closed container between the player object and the object in question, but said object would not be takeable normally because it is a component part, is being held by somebody, or (implicitly) it’s in another room (or maybe off-stage)”.

What I don’t understand is how the implicit part in #2 squares with the idea of “reaching through a wormhole” that you mentioned. I mean, the “hat in another room” example is pretty much that scenario – there’s no good reason for the Standard Library to prohibit taking it if it has been deliberately added to scope, by your convincing argument. But it is prohibited, because TakeSub() makes use of AttemptToTakeObject(), which includes a call to ObjectIsUntouchable(item, false, true), which runs afoul of the implicit part of #2 above. Following is (I think) the block where a ##Take action on the hat (in another room but in scope) is stopped:

! Second, a barrier between the item and the ancestor.  The item can
! be carried by someone, part of a piece of machinery, in or on top
! of something and so on.

i = parent(item);
if (item ~= ancestor && i ~= player) {
    while (i ~= ancestor) {
        if (flag2 && i hasnt container && i hasnt supporter) { ! flag2 is the "take_flag" argument
            if (i has animate) {
                if (flag1) rtrue;
                return L__M(##Take, 6, i, noun);
            }
            if (i has transparent) {
                if (flag1) rtrue;
                return L__M(##Take, 7, i, noun);
            }
            if (flag1) rtrue;  ! *HERE* is where the taking of the hat is prohibited (I think)
            return L__M(##Take, 8, item, noun); ! "That isn't available."
        }
        if (i has container && i hasnt open) {
            if (flag1) rtrue;
            return L__M(##Take, 9, i, noun);
        }
        i = parent(i);
    }
}

If being in another room isn’t supposed to matter for touchability when scope is deliberately manipulated (again, assuming no intervening closed containers, and in line with your argument that being in a separate room doesn’t count as a closed container), then why does setting flag2 cause the routine to decide that something in scope but in another room is untouchable? (Note that the DM4 doesn’t mention the target object being in another room in the discussion above.) Why does the code in this case conflate being in another room with the plainly-stated factors that are intended to prohibit taking, like being a component part or being an animate’s possession?

As @mirality concisely phrased it: “To put that a third way, ObjectIsUntouchable’s job is to report which of the in-scope objects are not touchable.” If I rely on ObjectIsUntouchable() to decide whether the hat is takeable, it will say yes. But when the ##Take action is issued, ObjectIsUntouchable() (as used by AttemptToTakeObject()) will say no, not really. Is this behavior even intentional? If so, what’s the logic of it?

It seems like the only way to let the PC get the hat through a wormhole is to intercept the ##Take action and move it directly, which bypasses things like carrying capacity check, react_after(Take:), etc. Shouldn’t the Standard Library support a normal ##Take attempt if reaching through a wormhole is supposed to be allowed?