Changing verb grammar at runtime

I’m trying to change the VerbRule grammar for an action at runtime. For this, it seems like I need to get to the GrammarProd object defined by the VerbRule macro? I can then apparently use the addAlt(), clearAlt(), etc, methods to modify the grammar.

However, I’m unable to find any examples. Has anyone delved in this before?

Issue is solved, but I forgot to post the solution for anyone landing here through a search:

First, we need to create a “match object”, which should inherit from the action we want to modify, as well as from DynamicProd. Here, we’ll use WaitAction:

local matchObj = TadsObject.createInstanceOf(WaitAction, DynamicProd);

Then, we set its “grammarTag” property:

matchObj.grammarTag = 'predicate(ModifiedWait)';

“predicate(Foo)” is what is used normally as the tag when you write “VerbRule(Foo)” rules, so we did the same here.

Next, we clear the grammar for the existing “Wait” rule. This is done through the “predicate” object:

predicate.deleteAlt('Wait');

We then add the new grammar:

predicate.addAlt( ''' 'z' | 'wait' | ('kill' | 'waste') ( | 'some') 'time' ''', matchObj, cmdDict, t3GetGlobalSymbols());

The new grammar is now in effect (entering KILL SOME TIME will perform a Wait action.)

For transitive actions that apply to objects/topics/etc, you can’t use the “dobj”, “dobjList”, etc macros. You need to use their expansions instead (defined in en_us.h).

There’s a caveat though: Mike Roberts pointed out a T3 VM bug where the above might not work correctly in all cases. It will be fixed in the next TADS 3 release.

2 Likes

Thanks for sharing the example Nikos. Two observations:

Is deleting the original alternative necessary (in your example)? Couldn’t you just add a new one that omits “wait” and “z”?

About transitive actions: my impression is that there is nothing special about those except that they use macros such as dobjList, singleIobj, etc, so I think you should check what those macros expand to and write the grammar definition string accordingly.

Yes, that works. The old grammar will still be active.

That works fine. I updated my post above.

(My use case is a bit different though. The above code is compiled at runtime, it does not exist in the static code of the game. The symbols of the expanded macro are not found by the dynamic compiler. I’m sure there’s a solution, but in my case I only needed to alter the grammar for a non-transitive action, so it’s fine.)

Continuing my journey into the past, I became fascinated with this topic. After floundering for a while in the documentation, I found this which seemed to close a few gaps I had not closed myself. Trying to implement this for my case did not work, so I tried to implement the above exactly. It also did not work.

In one spot:

       local matchObj = TadsObject.createInstanceOf(WaitAction, DynamicProd);
       matchObj.grammarTag = 'predicate(ModifiedWait)';
       predicate.deleteAlt('Wait');

       //  Above line works!  The below one does not

       predicate.addAlt( ''' 'wait' | ('kill' | 'waste') ( | 'some') 'time' ''', matchObj, cmdDict, t3GetGlobalSymbols());

The documentation explained that t3GetGlobalSymbols() will not find the symbol table in a release
build, requiring more handholding, but my -d compilation should have included it (though I think that was more relevant to Transitive Verbs?). I see the caveats:

I supppose is it possible I drove into this bug given I tried static code from within a switch-case-break construct? I don’t think there was a TADS release after the 2017 date of this thread.

In any case, I am not enamored of this functionality any more, since it is potentially deceptive to the player. Before the new grammar was available it would have the effect of saying

>WASTE SOME TIME
The story doesn't understand that command.

Making it unlikely the player would try it later without some lubricating text. A kludgey but runtime way would be to modify the VerbRule statically, then trap and handle the new syntax with getEnteredVerbPhrase until the precondition flag is set.

EDIT: or maybe a more elegant way would be to create a new Verb instance of WaitAction, whose execAction() explains the fail without precondition flag, and inherits behavior otherwise.

So I don’t actually want this anymore, but it is an affront to my knowledge base that I cannot make it work anyway. Anyone else played with this?

1 Like

What’s the actual desired behavior and the typical use case?

I assume that this isn’t sufficient:

DefineIAction(WasteTime) execAction() { replaceAction(Wait); };
VerbRule(WasteTime)
        ( 'waste' 'time' | 'waste' 'some' 'time'
                | 'kill' 'time' | 'kill' 'some' 'time' )
        : WasteTimeAction
        verbPhrase = 'wait/waiting'
;

…because that doesn’t do anything at runtime. But if the desire to do it at runtime is because you only want the usage available to the player at specific times, I don’t know why you’d prefer runtime grammar munging instead of just putting the logic in the action instance’s verifyAction() or something like that.

In my case, it was for fixing a bug in a game after release without invalidating people’s saved games. The game would look for a patch file containing T3 code, and if it exists, would compile and execute it. That way the game could be updated by updating the patch file.

In this instance, the patch file would replace the existing wrong verb grammar with the correct one.

3 Likes

Lol, in my case it was a bit more misguided. I was looking to add a synonym for a verb midway through a game. So ‘wait’ and ‘z’ would work from time 0, but later, suddenly ‘kill some time’ would also work. (Using the original example)

As you say, creating a new WaitAction class verb with special verify handling is a less convoluted way to do this.

But it just made me mad that I couldn’t figure out how to make it work that convoluted way. @RealNC, I know its been 5 years - would you have expected your example to work without runtime compilation?

@RealNC, I take it this was used in Thaumistry? If you don’t mind, can you explain how this was done, and if it is possible to implement without modifying the interpreter?

1 Like

Ah, interesting. I’d also be interested in hearing about the mechanics for accomplishing this.

It should work. Runtime complication was simply an additional complication in my case.

It makes use of the DynamicFunc class:

http://tads.org/t3doc/doc/sysman/dynfunc.htm

You can feed it Tads source code, compile it and execute it while the game is running. If your initial release of the game contains code that looks for a patch file in the game directory and reads, compiles and executes the code in the file after every startup and after every restore, then you can ship bug fixes in that patch file.

In my case, I’ve put the patch loader in its own patch_loader.t source file that gets compiled in the game. This isn’t necessary, but due to paranoia I went with a sort of “bootstrap” system where the patcher itself resides in its own runtime-compiled source file (patch_bootstrap.t) which is shipped with the game. The game itself only compiles and executes patch_bootstrap.t, which then in turn loads the final patch files. This means you can update the runtime patcher itself in the future.

patch_loader.t which is compiled with the game itself:

#include <file.h>
#include <strbuf.h>
#include <tadsgen.h>
#include <dynfunc.h>

transient patchObj: object {
    bootstrapFunc = nil;
    compilePatches = nil;
    applyPatches = nil;

    bootstrap()
    {
        try {
            local codeFile = File.openTextFile('patch_bootstrap.t', FileAccessRead);
            local stringBuf = new StringBuffer(codeFile.getFileSize());
            local line = codeFile.readFile();
            
            while (line != nil) {
                stringBuf.append(line);
                line = codeFile.readFile();
            }
            codeFile.closeFile();
            
            patchObj.setMethod(&bootstrapFunc, Compiler.compile(toString(stringBuf)));
        }
        catch (Exception e) {
            "\b[Could not bootstrap patcher: <<e.displayException()>>]\b";
        }
    }
}

// Re-apply all patches after every restore.
postRestorePatcher: PostRestoreObject {
    execute()
    {
        patchObj.applyPatches();
    }
}

// Apply all patches on every game init.
initPatcher: InitObject {
    execute()
    {
        patchObj.bootstrap();
        patchObj.bootstrapFunc();
        patchObj.compilePatches();
        patchObj.applyPatches();
    }
}

The above looks for patch_bootstrap.t which is shipped with the game files:

function()
{
    patchObj.setMethod(&compilePatches, method()
    {
        try {
            local codeFile = File.openTextFile('patch.t', FileAccessRead, 'utf8');
            local stringBuf = new StringBuffer(codeFile.getFileSize());
            local line = codeFile.readFile();

            while (line != nil) {
                stringBuf.append(line);
                line = codeFile.readFile();
            }
            codeFile.closeFile();

            patchObj.setMethod(&applyPatches, Compiler.compile(toString(stringBuf)));
        }
        catch (Exception e) {
            "\b[Could not build patches: <<e.displayException()>>]\b";
        }
    });
}

And that in turn looks for patch.t which also is shipped with the game files and contains the actual game fixes and updates. Here, I’ll include only few of the fixes to the game as an example.

Pay attention to the warnings in the comments.

/*
 * WARNING: Due to a TADS bug, string expressions within "if", "for", "while", and "?:" statements
 * can result in nil object reference runtime errors or wrong results. All such comparisons must be
 * performed outside of such statements and their result stored in a local variable.
 *
 * "switch" statements cannot be used. They must be converted to their if-else equivalent.
 */

function()
{
    // Update the  game's version.
    versionInfo.version = '1.4 January 21, 2019';
    versionInfo.revision = 4;

    // Fixes a statusline bug.
    statusLine.initBannerWindow(statuslineBanner);

    // Fixes some bugs in me.beforeAction(). We create a new method containing
    // the correct code and replace the existing me.beforeAction().
    me.setMethod(&beforeAction, method() {
        local cmp = gAction.getOrigText().toLower().startsWith('examine');
        if (cmp) {
            if (me.usedExamine == 14) {
                reportAfter('<.p>[Just a reminder that you can use <b>>x</b> as an abbreviation for <b>>examine</b>.
                    For other useful abbreviations, type <b>>help</b>.]<.p>');
                ++me.usedExamine;
            } else if (me.usedExamine < 14) {
                ++me.usedExamine;
            }
        }

        // [more fixes here, but omitted in this example]

        inherited();
    });

    // Make HIT KEYBOARD KEYS mean TYPE ON KEYBOARD.
    th_keyboard_keys.setMethod(&actionDobjAttack, method() {
        local cmp = gAction.getOrigText().toLower().startsWith('hit');
        if (cmp) {
            replaceAction (TypeOn, th_investor_keyboard);
        }
        else {
            inherited();
        }
    });

    // Fix typo in vocabWords of the tWiring Thing.
    tWiring.vocabWords = 'wiring wire wires';
    tWiring.initializeVocab();

    // Fix typos in room descriptions.
    rm_glick.setMethod(&desc, method() {
        if (ch_sarah.phase is in (0, 1, 2)) {
            "This intimate room is Henry Glick's actual garage as it was at the time of his death. At one end are models of his 
            famous inventions. At the other end are several odd-looking machines that he was working on before he died. One of these
            bears the label `AquaMotor.\' Another sits inside a steel cage and is labeled `Thaumeter.\'
            \bPresiding over it all is a friendly looking docent who sits in a rocking chair, happily knitting away from 
            the basket of yarn next to her. The only exit is <<roomdirsouth>>. ";
        }
        else if (ch_sarah.phase == 3) {
            "You are <<me.posture.participle>> in Henry Glick\'s actual workshop. At one end are models 
            of his inventions. At the other is a collection of odd-looking machines. One of these
            bears the label `AquaMotor.\' Another sits inside a steel cage and is labeled `Thaumeter.\'
            \bPresiding over it all is Mrs. Henry Glick, who sits in a rocking chair, happily knitting away from the basket 
            of yarn next to her, and confidently awaiting the arrival of the investors. 
            The only exit is <<roomdirsouth>>. ";
        } else {
            // [...]
        }
    });

    /*
     * Verb grammar fixes.
     */
    {
        local matchObj = TadsObject.createInstanceOf(MenuAction, DynamicProd);
        matchObj.grammarTag = 'predicate(v2Menu)';
        predicate.deleteAlt('Menu');
        predicate.deleteAlt('v2Menu');
        predicate.addAlt(''' ('main' | ) 'menu' | 'mainmenu' | 'options' ''', matchObj, cmdDict, t3GetGlobalSymbols());
    }

    /*
     * Hint fixes.
     */
    for (local i = firstObj(Goal); i != nil; i = nextObj(i, Goal)) {
        local cmp = i.menuContents[1] == 'You have quarter but you still need to fix the machine. ';
        if (cmp) {
            i.menuContents[4] = 'You can get the spell you need from solving the puzzle in the Quantum Leaps booth. ';
            continue;
        }
        cmp = i.title == 'Where can I find the spell I need for the vending machine? ';
        if (cmp) {
            i.menuContents[2] = 'You can get the spell you need from solving the puzzle in the Quantum Leaps booth. ';
            continue;
        }
        cmp = i.title == 'How do I solve the envelope puzzle?';
        if (cmp) {
            i.menuContents[2] = 'Did you notice that the envelope was a little moist when it arrived? ';
            continue;
        }
        cmp = i.title == 'How do I solve the envelope puzzle? '; // note the trailing space
        if (cmp) {
            i.menuContents[4] = 'Did you notice that the envelope was a little moist when it arrived? ';
            continue;
        }
    }

    return 0;
}

Hope that helps. However, I’m not sure this will work reliably with the existing release of the TADS Workbench or the Windows HTML terp. This game was shipped using a terp based on T3VM source code containing updates that don’t exist in the current releases of Workbench or the official terp. We built the game with FrobTADS which has those compiler fixes, and shipped it with the QTads terp which has the T3VM fixes.

I can’t remember anymore if those fixes were about dynamic compilation or not. I guess you’ll have to try it and see :stuck_out_tongue:

Also, even though it didn’t occur to me at the time, it’s probably best to obfuscate the contents of the patch files (like with a simple rot13 or similar) and de-obfuscate them in the patchObj loader to avoid spoilers if people decide to read the patch files.

And obviously your game becomes editable this way. People can add new objects and new rooms in the game by simply editing the patch files. It’s hard to do, since object names are not visible, but people can use the TADS reflection system to get object names. Hackable/moddable IF games, anymore? :smiley:

5 Likes

I don’t know why but I just got the idea of an Aemon-style RPG game written in TADS, where the adventures are just encoded TADS source which gets added in by the patcher. Something for my brain to chew on for a bit, I guess.

This is awesome, thanks for sharing!!

Coooooool.

Does this exist as a module/library anywhere, and if not is there a particular reason (beyond the compatibility questions you mentioned)? Do you have any objections to other people using this (or a modified form of it) in their own games, and if not how would you like to be credited and under what license if any should the code be included?

Reason being I absolutely am worried about post-release support and the fact that there isn’t a built-in patching mechanism is one of the design deficiencies I’m surprised about in T3. So unless this already exists as a module or you’ve got objections about redistribution I’m absolutely going to roll that code into a l’il separately compilable module for use in my WIP.

1 Like

No, I don’t think so.

But you can make your own if you want. You (or anyone else) can use and adapt the example code however you want, even without attribution.

2 Likes

Awesome, done. For anyone else interested, I’ve thrown it up as a git repo.

A couple modifications:

  • The module has a default patch bootloader if none is present at runtime.
  • The patch bootloader file it looks for is patchBootstrap.t instead of patch_bootstrap.t
  • The patch is also optional. If it’s not present, the patching process will be silently skipped (unless debugging output is enabled)
  • By default both the bootloader (if present) and the patch itself are expected to be base64 encoded. This behavior can be disabled by commenting out #define PATCH_LOADER_USE_BASE64 in the header file (patchLoader.h)

I’ve only done the simplest of testing with this, and only against FrobTADS, so no telling how robust/portable any of these changes are.

I’m also planning on adding some kind of rudimentary code signing and/or some kind of simple symmetric key encryption for patch files, but none of that is implemented currently.

The code is also lightly commented but otherwise the documentation is nonexistent.

2 Likes