Any way to safely access Thing.desc() during preinit?

Adding adv3libPreinit to execBeforeMe has no effect.

My point is that having substitutions in the double-quoted text will mean that getMethod() won’t return a text string at all.

You’re right that if it did and you then replaced the “fancy” desc() with a static string you’d be removing all the “fancy” stuff. My point is that you won’t get a string at all in the first place. Unless I’m missing something. With pebble declared as:

+pebble: Thing 'small round pebble' 'pebble'
     "A small, round <<name>>. ";

Then something like:

                local txt = pebble.getMethod(&desc);
                txt = rexReplace('pebble', txt, 'rock', ReplaceAll);
                pebble.setMethod(&desc, txt);

Will produce a runtime error, because the return value of pebble.getMethod(&desc) won’t be a string, it will be a function.

This won’t work unless you just want to entirely replace, instead of modify/edit/append to/whatever the “base” description.

Did you double-check the spelling? Because now that I’m at a screen again I see it is adv3LibPreinit. I would be surprised if adv3 didn’t run first by default, but it seems worth verifying. If you already spelled it the correct way then I guess it’s a dead end.

This is what I was testing:

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

foozle() {
        "\n-----start capture-----\n ";
        local txt = mainOutputStream.captureOutput({: "Foo" });
        "\n-----end capture-----\n ";
        "txt = <q><<txt>></q>\n ";
}

DefineSystemAction(Foozle) execSystemAction() { foozle(); };
VerbRule(Foozle) 'foozle': FoozleAction verbPhrase = 'foozle/foozling';

PreinitObject
        execBeforeMe = [ adv3LibPreinit, outputManager, mainOutputStream ]
        execute() { 
                foozle();
        }
;

startRoom:      Room 'Void' "This is a featureless void. ";
+me: Person;
+pebble: Thing 'small round pebble' 'pebble' "A small, round <<name>>. ";

versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;

Behavior’s the same with the execBeforeMe line commented out.

Like I said, fiddling around with the adv3-specific output stuff leads me to believe that output during preinit is being handled by different code…that is, not by adv3 but by directly by T3 intrinsics, which makes some intuitive sense…and so this might not be something that’s fixable via TADS code.

1 Like

Okay. I just wanted to make sure it wasn’t something dumb like misspelling an identifier, since I remembered it wrong at first.

Yeah, makes sense. And I have posted questions where it turned out the answer was that I was spelling vocabLikelihood wrong.

1 Like

EDIT: Nnnnnnooope. What I just posted below is almost but not quite right; it works if you compile with the -d flag but segfaults at runtime if you don’t. I know what the problem is and I’ll supply a fix in a minute.

Anyway, digging around how output is set up at runtime, it looks like the issue is because the default output routine is set by calling t3SetSay(), and this doesn’t happen (by design) until runtime.

Digging around the source for t3SetSay(), there are different places it gets called depending on how initialization is happening (e.g., whether it’s a “normal” game startup or it’s happening after loading a savegame).

In among that stuff (in _main.t) there’s an apparently undocumented (except for the comments in the source) function called _outputCapture() that appears to do what I want.

The “gimmick” (which isn’t specific to the _outputCapture() method) is that you can replace the default output function via t3SetSay(), and if you want to do a “raw” capture—one that evaluates inline substitutions but which doesn’t apply any output filters—you can use a function that just appends the argument to a string buffer.

Anyway, this appears to work:

#include <adv3.h>
#include <en_us.h>

PreinitObject
        execute() {
                local txt = _outputCapture({: pebble.desc });
                txt = rexReplace('pebble', txt, 'rock', ReplaceAll);
                pebble.setMethod(&desc, txt);
        }
;

startRoom:      Room 'Void' "This is a featureless void. ";
+me: Person;
+pebble: Thing 'small round pebble' 'pebble' "A small, round <<name>>. ";

versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;

Transcript:

Void
This is a featureless void.

You see a pebble here.

>x pebble
A small, round rock.
1 Like

I take it back…I don’t know what’s causing the segfault.

My thought was that the problem was how _outputCapture() attempts to restore the old value of t3SetSay(), but that’s not it.

Weirdly:

  • _outputCapture() returns a string (T3 type TypeSString)
  • normal string operations work on the returned string
  • using the returned string as an argument for setMethod() will cause a segfault when the method is subsequently accessed
PreinitObject
        execute() {
                local txt0, txt1;

                txt0 = _outputCapture({: pebble.desc });
                txt1 = 'A small, round rock. ';

                txt0 = rexReplace('pebble', txt0, 'rock', ReplaceAll);

                if(txt0 == txt1)
                        "String match\n ";

                pebble.setMethod(&desc, txt1);
                rock.setMethod(&desc, txt0);
        }
;

This captures pebble.desc() to txt0. It then assigns A small, round rock. to txt1. It does a rexReplace() on txt0 which should make it identical to txt1. It verifies that the two strings are identical. It then assigns txt1 to pebble and txt0 to rock.

Compiled without -d this will output “String match” when compiled (verifying the two strings are identical within the bounds of == checking), but then >X PEBBLE will work as expected and >X ROCK will segfault:

Void
This is a featureless void.

You see a pebble and a rock here.

>x pebble
A small, round rock.

>x rockSegmentation fault (core dumped)

Compiled with -d both work fine:

String match
Void
This is a featureless void.

You see a pebble and a rock here.

>x pebble
A small, round rock.

>x rock
A small, round rock.

1 Like

I’m just ignorantly babbling… does it have anything to do with the fact that preinit happens at program launch in -d mode? Isn’t there a compiler flag to force when preinit happens? What would happen if you built for release but set preinit to happen on program launch the way debug does? Or is that even an option? Not that you would want the end product to preinit on game launch, but just to sort of narrow what’s happening…

1 Like

It absolutely does, and that’s why I was on this line of inquiry in the first place…the assumption that the reason why mainOutputStream.captureOutput() doesn’t just work is because output is handled differently at preinit, because it can be happening either during compilation or in the interpreter.

What doesn’t make sense to me is why two strings that test for equality end up producing different results. I assume that’s because the string generated/captured at preinit is actually getting derefed/deswizzled/something between compilation and runtime, but I haven’t figured out where/how it’s happening.

2 Likes

I was just curious if you used -nopre without -d if the results would be any different. Assuming that -nopre runs preinit at launch like debug?

Ehhh, it’s something even weirder. The string values are absolutely saved correctly (you can stuff them in an array somewhere and access them at runtime with no problems). It’s just using them in setMethod() that causes the breakage.

So:

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

testData: object
        data = perInstance(new Vector())
        add(obj, v) {
                obj.setMethod(&desc, v);
                data.append([ obj, v ]);
        }
        output() {
                data.forEach(function(o) {
                        "<<o[1].name>>:  <q><<o[2]>></q>\n ";
                });
        }
;

DefineSystemAction(Foozle) execSystemAction() { testData.output(); };
VerbRule(Foozle) 'foozle': FoozleAction verbPhrase = 'foozle/foozling';

PreinitObject
        execute() {
                local txt0, txt1;

                txt0 = _outputCapture({: pebble.desc });
                txt1 = 'A small, round rock. ';

                if(txt0 == txt1)
                        "String match\n ";

                testData.add(pebble, txt0);
                testData.add(rock, txt1);
        }
;

startRoom:      Room 'Void' "This is a featureless void. ";
+me: Person;
+pebble: Thing 'small round pebble' 'pebble' "A small, round <<name>>. ";
+rock: Thing 'small round rock' 'rock' "A small, round <<name>>. ";

versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;

This creates a test testData object. It has an add() method that takes two args: a Thing and a string. It makes the string the Thing’s desc via setMethod(), and it appends the Thing and string to an array. testData.output() lists all the Things and strings that have been passed to it as arguments.

The anonymous preinit object now captures the pebble’s description and then re-sets it on the pebble via testData.add() (theoretically leaving the description the same), and then sets the rock’s description to be the string literal 'A small, round rock.'.

Finally, there’s a new system action >FOOZLE that just calls testData.output().

When run:

Void
This is a featureless void.

You see a pebble and a rock here.

>foozle
pebble: "A small, round pebble.  "
rock: "A small, round rock.  "

>x rock  
A small, round rock.

>x pebble
[Runtime error: invalid type for call
]

If you update testData.output() to call setMethod() on each object again (with the same arguments):

        output() {
                data.forEach(function(o) {
                        "<<o[1].name>>:  <q><<o[2]>></q>\n ";
                        o[1].setMethod(&desc, o[2]);
                });
        }

…then everything “works”:

Void
This is a featureless void.

You see a pebble and a rock here.

>foozle
pebble: "A small, round pebble.  "
rock: "A small, round rock.  "

>x rock
A small, round rock.

>x pebble
A small, round pebble.
1 Like

I’m not sure setMethod is supposed to use sstrings… can you assign an anonymous method to setMethod that simply prints/returns the value of the string you end up with?

1 Like

It is. Or at least that’s what Learning TADS3 says; it’s the point of the examples in Section 4.2, which specifically uses:

local str = mainOutputStream.captureOutput({: desc });
setMethod(&desc, str);

It also works perfectly fine in other contexts; the same code that segfaults when run at preinit behaves as expected if executed at runtime.

1 Like

I’m hesitant to call this “the answer”, but this kinda/sorta works, for example:

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

descMunger: InitObject
        data = perInstance(new LookupTable())
        add(obj, v) { data[obj] = v; }
        execute() {
                data.forEachAssoc(function(k, v) { k.setMethod(&desc, v); });
        }
;

PreinitObject
        execute() {
                forEachInstance(MungedThing, function(o) {
                        local txt;

                        txt = _outputCapture({: o.desc });
                        txt = rexReplace('pebble', txt, 'rock', ReplaceAll);
                        descMunger.add(o, txt);
                });
        }
;

MungedThing: Thing;

startRoom:      Room 'Void' "This is a featureless void. ";
+me: Person;
+pebble: MungedThing 'small round pebble' 'pebble' "A small, round <<name>>. ";
+rock: MungedThing 'small round rock' 'rock' "A small, round <<name>>. ";

versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;

Here the anonymous preinit object iterates through all instances of MungedThing, which are just the pebble and rock. It captures each instance’s desc, does a regex replace of “rock” for “pebble” (affecting only the pebble’s description), and saves the string to an init (not preinit) object called descMunger.

descMunger runs as init time and takes its saved instances/strings and does the setMethod() thing with them.

This compiles, does preinit correctly, and doesn’t explode at runtime (as far as I can tell).

If you do exactly what descMunger does, only during preinit, all the objects that have setMethod(&desc, txt) run on them will cause a segfault when examined at runtime.

2 Likes

Interesting. I was just going by the LibRef here:

/*
     *   Set a method value.  Assigns the given function (which must be a
     *   function pointer value) to the given property of 'self'.  This
     *   effectively adds a new method to the object.
     *   
     *   The function can be an ordinary named function, or a method pointer
     *   retrieved from this object or from another object with getMethod().
     *   Anonymous functions are NOT allowed here.  
     */

But now that I read it, it doesn’t specifically mention anonymous methods either, and I have used those on several occasions with setMethod without problem.

1 Like

I’m not sure how important it is to you to get all the prep done in preinit, but I was able to run your descMunger as a PreinitObject while using anonymous methods with setMethod, getting no runtime errors.

descMunger: PreinitObject
        data = perInstance(new LookupTable())
        add(obj, v) { data[obj] = v; }
        execute() {
                data.forEachAssoc(function(k, v) { 
			local m = method{ say(descMunger.data[self]); };      ////////
			k.setMethod(&desc, m); });           ///////////////
        }
	execBeforeMe = [dm2]        ///////////
;

dm2: PreinitObject     //////////////
        execute() {
                forEachInstance(MungedThing, function(o) {
                        local txt;

                        txt = _outputCapture({: o.desc });
                        txt = rexReplace('pebble', txt, 'rock', ReplaceAll);
                        descMunger.add(o, txt);
                });
        }
           // possibly extraneous, but at least we know initializeThing will have run
	execBeforeMe = [adv3LibPreinit]   /////////
;
2 Likes

The hidden gotcha with explicitly calling say() is that now the object’s description is handling output differently than any object that hasn’t been twiddled this way. So, for example:

PreinitObject
        execute() {
                local txt0 = _outputCapture({: pebble.desc });
                pebble.setMethod(&desc, method{ say(txt0); });

                txt0 = _outputCapture({: pebble.desc });
                pebble.setMethod(&desc, method{ say(txt0); });
        }
;

…will output the description when the code is executed at preinit, and afterward >X PEBBLE will produce Nothing obvious happens. (or whatever commandResultsEmpty currently is).

1 Like

I was just going by the examples in Learning TADS3, but getMethod()/setMethod() are part of TadsObject, and the documentation in the System Manual says it can be:

  • A regular (named) function pointer, which becomes a method with the same arguments as the function. The function itself isn’t changed by this; you can also still call it directly as an ordinary function.
  • A floating method pointer, which becomes a method with the same arguments.
  • An anonymous function, which becomes a method with the same arguments as the anonymous function. The anonymous function itself isn’t changed in any way by this; you can still call it directly, too.
  • An anonymous method, which becomes a method with the same arguments as the anonymous method.
  • A DynamicFunc, which becomes a method with the same arguments as the dynamically compiled code.
  • A single-quoted string value, which will be displayed on evaluating the property, as though it had been initially defined as a double-quoted string property of the object.
  • Any value retrieved by a call to getMethod(), on this object or any other object.

So TypeSString → method is theoretically an orthodox usage. I assume this is semantic sugar intended to avoid having to juggle output methods, and it’s just crapping out in the specific situation we’re hammering on here because it’s such a weird corner case.

1 Like

Turns out there’s a almost comically straightforward solution here:

PreinitObject
        execute() {
                local txt0 = _outputCapture({: pebble.desc });
                pebble.setMethod(&desc, {: "<<txt0>>" });
        }
;

…works as expected, and anything later in the object’s lifecycle can still use output filtering/capturing techniques like _outputCapture(). So:

PreinitObject
        execute() {
                local txt0 = _outputCapture({: pebble.desc });
                pebble.setMethod(&desc, {: "<<txt0>>" });
                txt0 = _outputCapture({: pebble.desc });
                pebble.setMethod(&desc, {: "<<txt0>>" });
        }
;

…will work fine.

This from looking through the T3 source (specifically tads3/tct3stm.cpp, which is where the setMethod() stuff lives), which handles this as a special case.

2 Likes