Output filters not working in action handlers

It appears that output filters don’t work in a non-SystemAction action’s execAction() or action() method.

Am I just not understanding how output filters are supposed to work?

A trivial example to illustrate. We create an output filter called foozleOutputFilter. When it’s active it blocks all output (by replacing it with an empty string). We define two new actions, >FOO, a system action and >BAR an intransitive action. Both output a string, enable the filter, output another string, and then disable the filter. The behavior I’d expect is that in both cases only the first string would be output. But that only happens with the system action. The intransitive action outputs both strings, suggesting that the filter isn’t working for some reason. It doesn’t appear to be because the filter is on the wrong output stream, because if you just don’t deactivate the filter all output is blocked.

The code:

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

DefineSystemAction(Foo)
        execSystemAction() {
                "FOO 1\n ";
                foozleOutputFilter.activate();
                "FOO 2\n ";
                foozleOutputFilter.deactivate();
        }
;
VerbRule(Foo) 'foo': FooAction verbPhrase = 'foo/fooing';

DefineIAction(Bar)
        execAction() {
                "BAR 1\n ";
                foozleOutputFilter.activate();
                "BAR 2\n ";
                foozleOutputFilter.deactivate();
        }
;
VerbRule(Bar) 'bar': BarAction verbPhrase = 'bar/barring';

foozleOutputFilter: OutputFilter
        isActive = nil
        activate() { isActive = true; }
        deactivate { isActive = nil; }
        filterText(ostr, val) { return(isActive ? '' : inherited(ostr, val)); }
;

foozlePreinit: PreinitObject
        execute() {
                mainOutputStream.addOutputFilter(foozleOutputFilter);
        }
;

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

versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;

The transcript:

Void
This is a featureless void.

You see a pebble here.

>foo
FOO 1

>bar
BAR 1
BAR 2

If you edit the declaration of Bar to be:

DefineIAction(Bar)
        execAction() {
                "BAR 1\n ";
                foozleOutputFilter.activate();
                "BAR 2\n ";
                //foozleOutputFilter.deactivate();
        }
;

…and then recompile, the never-disabled filter blocks all output—including the string output before the filter is enabled:

Void
This is a featureless void.

You see a pebble here.

>bar

…so it isn’t just that the filter is on the wrong output stream or something.

I feel like I must be missing something obvious here.

I haven’t checked and certainly don’t know why the code doesn’t work as is. Have you tried CaptureFilter at all? I notice that CaptureFilters return nil instead of empty strings, but I don’t know why that would change your test behavior…

There’s a stock SwitchablecaptureFilter that seems to do what you’re trying to do?

I think it has to do with the output buffering which TADS3 does in its “transcript” mechanism: Manipulating the Transcript

In the Technical Manual, on the page “The Command Execution Cycle”, under “execAction”, the docs note for SystemAction:

The transcript is then flushed and disabled to allow the SystemAction to prompt for interactive responses. Then execSystemAction() is executed, and finally the transcript is activated again.

So this probably accounts for the difference between the IAction and the SystemAction in the output you’re describing.

“Learning TADS3” says on page 313f.:

[…] there are occasions when your fancy output (or input) can be defeated by the text-buffering that the transcript is performing. In such cases you need to deactivate the (text-buffering) transcript before doing your fancy stuff and reactivate it afterwards, with a coding pattern than typically looks like:

gTranscript.deactivate();
/* do fancy stuff here */
gTranscript.activate();

And indeed, if we insert those calls to deactivate and activate gTranscript into Bar’s execAction from your example, like this:

DefineIAction(Bar)
        execAction() {
            gTranscript.deactivate();
            "BAR 1\n ";
            foozleOutputFilter.activate();
            "BAR 2\n ";
            foozleOutputFilter.deactivate();
            gTranscript.activate();
        }

… then the output will be:

Void

This is a featureless void.

You see a pebble here.

>foo
FOO 1

>bar
BAR 1

I don’t know if this has undesirable side effects or drawbacks, but it seems to address the immediate issue.

2 Likes

Ah, perfect. “Manipulating the Transcript” was in fact one of the pieces of documentation I had open, but I was trying to copy an example from Some Common Input/Output Issues, which is one of the “most complete” examples of using OutputFilter I could find, and it doesn’t twiddle the transcript (or at least it doesn’t in any of the code in the example). I think that’s one of the weaknesses in the TADS3 documentation: a relative paucity of completely worked, standalone examples.

Anyway, thanks. That is exactly what I was missing.

1 Like

FWIW, in my quest to implement a quick-and-dirty “FFW” capability to execute a canned series of commands bypassing output (to quickly zip to the gamepoint under debug/development), I could not find a master “output off” switch. I also struggled to get OutputFiltering/gTranscript to work the way I wanted. The below seemed an effective alternative.

modify mainOutputStream
    /* the new enable/disable capability */
    outputOn = true
    enable() { outputOn = true; }
    disable() { outputOn = nil; }

    writeFromStream(txt) {
        /* if an input event was interrupted, cancel the event */
        inputManager.inputEventEnd();

        /* write the text to the console - NEW:  if output enabled */
        if (outputOn) aioSay(txt);
    }
;

invokingMethod() {
        local cmdLst = ['get foo', 'get bar', 'get baz', 'e', 'e'];  //commands to run, hardcoded for now
        mainOutputStream.disable();
        foreach(local x in cmdLst)
            executeCommand(gPlayerChar, gPlayerChar, Tokenizer.tokenize(x), true);
        mainOutputStream.enable();
}

Yes, I was so lazy I did not want to replay a command file and have to spam space bar. This big hammer is probably not what you were initially looking for, but helped me!

1 Like

I’m going to come back to this when I get back to my computer…