Message parameter substitution in room names

This is a bit of an odd one, and I can work around it easily enough, but I’m curious about the underlying problem.

Let’s say the player gets conked on the head, passes out, and wakes up in a strange location. We want the room name to initially appear as something “Unknown Location”, then (after the player looks around a bit) perhaps “Abandoned Warehouse” and eventually (after discovering a clue or something) “Villain’s Lair”. Or whatever.

Now, I had originally handled this with something like:

badguyRoom: Room '<<gBadguyRoomName()>>'
     [...]

…where gBadguyRoomName() is a mass of conditionals testing various gRevealed() values to figure out how much the playe currently knows about the location in question.

That works, but I got to thinking that <<gBadguyRoomName()>> could be replaced with something like {BadGuyRoom} and handled via TADS3’s native message parameter substitution logic.

To jump to the punchline, here’s the problem in a simple transcript:

{Foozle}
This is a featureless {foozle}.

>i
You are empty-handed.

>l
Foo
This is a featureless foo.

That is, when the game starts and displays the name and description of the starting room it apparently does not handle any message parameter substitutions. If the player looks at anything else (like an empty inventory listing), subsequent views handle the substitution correctly.

On the other hand, if the only thing viewed is another room description (either by just typing >L or by moving to another room) the problem persists. Another transcript-from-game-startup:

{Foozle}
This is a featureless {foozle}.

>n
Other {foozle}
This is a different {foozle}.

>l
Other {foozle}
This is a different {foozle}.

>i
You are empty-handed.

>l
Other Foo
This is a different foo.

So what’s going on here? It’s easy enough to work around (indeed, in the game I’m working on the mystery room isn’t the room the player starts out in, and so the problem never manifests except in testing). But I’m trying to figure out what’s going on under the hood here.

Here’s some sample code that illustrates the situation, along with some commented-out bits associated with some other things I’ve tried (for example, if there are any objects in the room, then it “fixes” itself after the first look, so the initial room description is borked but an immediate >L produces the “correct” output).

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>

/*
modify MessageBuilder
        filterText(ostr, txt) {
                return(generateMessage('[' + txt + ']'));
        }
;
*/

modify Thing
        foozle() { return('foo'); }
;

modify MessageBuilder
        execBeforeMe = [ foozlePreinit ]
;

foozlePreinit: PreinitObject
        execute() {
                langMessageBuilder.paramList_
                        = langMessageBuilder.paramList_.append(
                                [ 'foozle', &foozle, nil, nil, nil ]);
        }
;

startRoom:      Room '{Foozle}'
        "This is a featureless {foozle}. "
        north = otherRoom
;
+me:    Person;
/*
+pebble: Thing 'small round pebble' 'pebble'
        "A small, round {foozle}."
;
*/

otherRoom:      Room 'Other {Foozle}'
        "This is a different {foozle}."
        south = startRoom
;

versionInfo:    GameID
        name = 'sample'
        byline = 'nobody'
        authorEmail = 'nobody <foo@bar.com>'
        desc = '[This space intentionally left blank]'
        version = '1.0'
        IFID = '12345'
;
gameMain:       GameMainDef
        initialPlayerChar = me
        newGame() {
                //statuslineBanner.removeBanner();
                runGame(true);
        }
;

Why use substitution in the first place? Just define the room name as ‘Unknown Location’, then at some point in the game, change the room’s name directly through a script of some sort.

The property you need to change is roomName in adv3 and roomTitle in adv3lite.

As for exactly why it’s not working under the hood, I can’t say. But really in 90% of cases you should only use inline expressions in double-quoted prose anyway, as using them in property strings can lead to messy / unmaintainable code.

Yes, as I said it’s easy enough to work around. But I’m interested in understanding what’s causing the underlying behavior.

The same behavior also occurs in “normal” double quoted strings, like in an object’s specialDesc. For example, if you define startRoom using something like:

startRoom:      Room
        desc = "This is a featureless {foozle}. "
        roomName = foozle
        north = otherRoom
;
+me:    Person;
+pebble: Thing 'small round pebble' 'pebble'
        "A small, round {foozle}."
        specialDesc = "You see a {foozle} here."
;

…you end up with the same problem…

{Foozle}
This is a featureless {foozle}.

You see a {foozle} here.
1 Like

Answering my own question here.

Digging through output.t, starting with MessageBuilder.filterText() back to MessageBuilder.generateText() and doing some debuging-via-printif (made somewhat tricky since the thing being debugged is output logic), it turns out that problem is that even if you define the message parameter substitution to not refer to any object(s), generateText() will still attempt to pair all param strings with a corresponding object.

If no object is specified (either explicitly by the caller or implicitly by the type of message parameter being substituted) then MessageBuilder just uses the last-resolved object. If there is none, then generateText() assumes there’s been an error, and outputs the param string as a literal (starting around line 1348):

            /*
             *   if the target object wasn't found, treat the whole thing
             *   as a failure - put the entire parameter string back in
             *   the result stream literally
             */
            if (targetObj == nil)
            {
                /* add it to the output literally, and go back for more */
                result += processResult('{' + paramStr + '}');
                continue;
            }

In the example in the OP, literally any object will work (because the output is not object-dependent). This means the substitution will always fail on game startup because the substitution itself doesn’t supply any objects (because it needs none) and it will work on all subsequent turns if any other message substitution has taken place because then MessageBuilder will have a cached object to use, which will get past generateText()'s internal error-checking.

There are a couple of non-kludge workarounds here. In the case of the specific thing described in the OP (that is, if we actually care about the room’s properties) then we can do something like:

modify MessageBuilder
        execBeforeMe = [ foozlePreinit ]
;

foozlePreinit: PreinitObject
        execute() {
                langMessageBuilder.paramList_
                        = langMessageBuilder.paramList_.append(
                                [ 'foozle', &foozle, 'room', nil, nil ]);
        }
;

foozleInit: InitObject
        execute() {
                langMessageBuilder.nameTable_['room'] =
                        {: gActor != nil ? gActor.location
                                : gPlayerChar.location };
        }
;

This will call Room.foozle() on the current location to get the literal text to substitute for “{foozle}” in output strings.

We do three things to accomplish this: create a PreinitObject that adds our message parameter string; add our PreinitObject to MessageBuilder.execBeforeMe to make sure the change is applied before MessageBuilder does its startup bookkeeping; and (the new bit) we also create an InitObject that runs after MessageBuilder’s startup stuff, where we add a global object mapping rule for our message param to use. The last bit needs to happen after MessageBuilder.execute() is called, because that’s when the table we’re adding an entry to is created.

This will also work in the example code from the OP (which is not actually dependent on the room), assuming a player character is defined and their location is defined:

Foo
This is a featureless foo.

You see a foo here.
1 Like