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

Maybe a weird corner case not worth worrying about, but is there any way to safely interact (test, modify) a Thing’s desc() in a PreinitObject?

Specific case I’m actually worried about is some lightweight procgen-ish stuff that depends on compile-time preprocessor flags, but here’s a barebones example:

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

PreinitObject
        execute() {
                forEachInstance(Thing, function(o) {
                        // DON'T DO THIS IT WILL A SPLODE
                        if(dataType(o.desc) == TypeNil)
                                return;
                });
        }
;

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

versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;

This will throw an error during during compilation (if compiled without the -d flag) or at runtime (if compiled with the -d flag) because:

  • Thing.desc() uses defaultDescReport()
  • defaultDescReport() is a macro that adds a report to gTranscript
  • Adding the report creates a MessageResult instance
  • MessageResult.construct() will call gAction.getCurrentObjects() if the message isn’t an object or property (which a double-quoted string will never be)
  • And gAction won’t be defined during preinit

Just wondering if there’s a known solution/approach to this.

1 Like
propType(prop)

should in theory only test the type of the property without evaluating it. I can’t verify it right now but I believe I’ve used it before.

3 Likes

True, propType() does return the type without evaluating.

But is there any way to actually parse/modify desc() (and possibly other double-quoted strings) during preinit?

One approach would be to just declare objects something like:

pebble: Thing 'small round pebble' 'pebble'
        "<<kludge()>>"

        _someProp = nil

        kludge() {
                if(_someProp == nil) {
                        return('A small, round pebble. ');
                } else {
                        return('A very bad example. ');
                }
        }
;

But this seems a very messy way to approach it if you want to twiddle a lot of objects at preinit.

It’s also possible to handle this sort of thing “externally”…just have a build script that generates the T3 code, instead of relying on the T3 code to do it itself. But that seems like kinda a kludge as well, since doing this general kind of thing is more or less what preinit objects are for.

1 Like

I have no idea. A suggestion would be to mimic the existing behaviour and just try:

//ForEachInstance(o…

  //selecting criteria here

  o.setMethod(&desc, {: gTranscript.addReport(new    DefaultDescCommandReport(‘procgen desc’, self))) })

}

Not sure if it works in the long run with what you are trying to accomplish though. (It’s still a bit unclear.)

1 Like

Mostly I’m just trying to feel around to figure how (or I guess if) I can update properties on objects during preinit. For most things, it’s straightforward enough:

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

modify Thing foozle = '[This space intentionally left blank]';

PreinitObject
        execute() {
                forEachInstance(Thing, function(o) {
                        if(o.foozle == 'foo') o.foozle = 'bar';
                });
        }
;

startRoom:      Room 'Void' "This is a featureless void. ";
+me: Person;
+pebble: Thing 'small round pebble' 'pebble'
        "A small, round pebble marked <q><<foozle>></q>. "
        foozle = 'foo'
;

versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;

And that works as expected:

Void
This is a featureless void.

You see a pebble here.

>x pebble
A small, round pebble marked "bar".

But for object descriptions? Not so much.

Just trying to get a feel for what I can and can’t do here.

1 Like

Does anyone know enough about the lifecycle of mainOutputStream to know why something like local txt = mainOutputStream.captureOutput({: "Foo" }); fails (in the sense the output isn’t captured, just displayed) during preinit? For example:

#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
        execute() {
                foozle();
        }
;

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

versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;

Produces:

-----start capture-----
Foo
-----end capture-----
txt = ""
Void
This is a featureless void.

You see a pebble here.

>foozle
---start capture---
---end capture---
txt = "Foo"

That is, during preinit captureOutput() doesn’t prevent the display of the passed function and it doesn’t return the output.

Edit: And for clarity: the above doesn’t include execBeforeMe = [ mainOutputStream ] on the anonymous PreinitObject, because it doesn’t appear to make any difference. Just calling it out because all OutputStream instances are PreinitObjects as well.

1 Like

Don’t forget that a double-quoted string is nothing but syntactic sugar for calling say(). So:

desc = "Description. "

Just means:

desc()
{
    say('Description. ');
}

So your question really should be “how to safely access object methods during preinit.”

1 Like

It’s true that TypeDStrings are methods under the hood, but I don’t think that’s where the “gotcha” is. Or at least I don’t think it’s where I’m currently getting hung up (no idea if there are additional, hidden gotchas that I just haven’t run into yet because I haven’t gotten that far).

Because this kind of thing:

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

modify Thing foozle = '[This space intentionally left blank]';

PreinitObject
        execute() {
                if(pebble.foozle == 'foo') pebble.foozle = 'bar';
        }
;

startRoom:      Room 'Void' "This is a featureless void. ";
+me: Person;
+pebble: Thing 'small round pebble' 'pebble'
        "A small, round pebble marked <q><<foozle>></q>. "
        foozle() { return('foo'); }
;

versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;

…more or less works as expected. There is breakage possible if the return value of pebble.foozle() is different at preinit than it would be at runtime (for example). And the fact that the preinit twiddling is replacing a method with a single-quoted string. But it “works” in that it compiles and runs and produces the expected output.

And I can’t figure out a way to do something similar with double-quoted strings, specifically worrying about Thing.desc() because it’s the most common place double-quoted strings are used.

So say I’m writing a T3 module that does procgen stuff, and one of the things I want to be able to do is append some stuff to room descriptions if the game is compiled with the module. One way to do that would be to require authors to write descriptions in some entirely different way (use some other property instead of desc, using single-quoted strings instead of double-quoted strings, append some macro or <<>> tag, or something like that). But then it’s a pain to use.

Or say I’ve got a bunch of stuff I want to precompute at compile time and then bake into object/room descriptions so that the expensive stuff doesn’t get called when looking at things. Again, something that can be handled via the methods described above (that is, requiring special care in writing double-quoted stuff), but that seems ugly considering pre-baking stuff like this is more or less what preinit is for in the first place.

If that all makes sense. It’s certainly possible that all of this is just me thinking about things wrong or missing something obvious.

1 Like

I’m sorry, I haven’t read all this thread… does getMethod do anything for your cause?

1 Like

Any chance that what you’re trying to do can be handled in basicExamine or mainExamine instead of desc?

3 Likes

I don’t think twiddling the “examine” methods get me where I want to be because they all use desc under the hood.

And now that I’m onto it, I’m looking more into something like kludging together a preinit-safe (or preinit-specific) OutputStream so captureOutput() works, because there are plenty of other things that habitually use double-quoted strings.

2 Likes

Okay so my ignorance might be showing here, but…

Have a class with a template that gets a single-quoted string assigned to a descSource property. desc() then is just { "<<descSource>>"; }.

If you want to poll or modify desc() for this class of object, then you just need to focus on descSource, which contains actual data.

Unless I’ve completely misunderstood the entire situation here.

EDIT: This is all assuming that desc() was only printing a string, and not performing other additional actions.

2 Likes

Yeah, that’s an alternative (that’s what I meant in my post above about “use some other property instead of desc”).

The downside is it requires writing code in an ideosyncratic/non-standard way and/or re-writing existing code. What I want to be able to do (beyond solving the abstract technical problem, now that I’ve started thinking about it) is to implement things as a drop-in module that doesn’t require writers to learn a special way to write objects in order to use. If that makes sense.

1 Like

Ohhhhhhhhh yeah that’s a big engineering constraint, then.

Were you not able to use getMethod/tweakOrAppend/setMethod?

2 Likes

getMethod()/setMethod() are hauntingly close to what I want, but they break if the double-quoted string contains any fancy stuff (conditionals, <<>> expressions, and so on).

So something like:

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

PreinitObject
        execute() {
                local txt;

                txt = pebble.getMethod(&desc);
                if(txt == 'A small, round pebble.') {
                        "Matched desc()\n ";
                        pebble.setMethod(&desc, 'A small, round rock.');
                }
        }
;

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

versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;

Will appear to work (that is, at preinit it will report matching the desc and at runtime >X PEBBLE will output A small, round rock.). But then if you try to define the pebble as:

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

It won’t match.

That’s why I was trying things like local txt = mainOutputStream.captureOutput({: "Foo" }) Because Section 4.2 of Learning TADS 3 suggests using captureOutput() for precisely this sort of thing…but it doesn’t work at preinit time.

2 Likes

Is your PreinitObject set to run after adv3 preinit when all of the standard filters should be in place?

2 Likes

Well, I’ve tried adding execBeforeMe = [ mainOutputStream ] to the declaration and it doesn’t appear to have any effect, which I mentioned above. I’ve also tried adding outputManager, and then eventually every transient object in output.t, and none seemed to have any effect. I’d certainly be willing to try something else if you’ve got any ideas.

Having done some debug-via-printf, it looks like output during preinit doesn’t use aioSay(), which the same methods/functions do at runtime. I assume this is because the assumption is that preinit stuff will usually be happening at compile time and only infrequently in the interpreter, but I haven’t done enough source diving through the compiler to see what’s actually happening under the hood.

1 Like

Perhaps I’m missing something here, but the way your examples are written (capture output at preinit time, then rewrite the descriptions based on the captured text) you’re trampling all over any ‘fancy stuff’ anyways.

If you don’t plan to account for change anyways, then requiring descriptions to be plain double-quoted strings with no embedded expressions might be reasonable?

If you do want to account for change, I don’t see how you can avoid doing the entire substitution thing at runtime.

1 Like

I’m away from the computer, so you might need to double-check this spelling. execBeforeMe should be adv3libPreinit. It’s a PreinitObject that sets the output filters (and a whole lot of other stuff). IF your object is getting executed before adv3, that would be why the output filters don’t work.

2 Likes