A TADS3/adv3 module allowing per-action failure messages for commands involving "all"

THE ISSUE

It’s easy to configure an Action to permit or prohibit use with “all”, via the actionAllowsAll flag. It’s easy to change the failure message displayed when the player tries to use “all” with an action that doesn’t allow it. But by default there isn’t a way to change the failure message for specific actions.

This is another one of those things that seems like it would be an easy fix. Maybe a one-liner. But changing the behavior turns out to require a bit of a safari through the library.

WHY IT ISN’T A SIMPLE FIX

The stock message is output by a method on playerMessages, so the simple fix would be something like:

modify playerMessages
        // THIS DOESN'T WORK
        allNotAllowed(actor) {
                // Check for the action we want to display a custom message.
                if(gAction.ofKind(SomeAction)) {
                        "<.parser>This is a placeholder custom
                        message.<./parser> ";
                } else {
                        // This is the stock message.
                        "<.parser><q>All</q> cannot be used with that
                        verb.<./parser> ";
                }
        }
;

The problem here is that by the time the method is called and the output is displayed gAction will not be the resolved action from the player’s input. It will be an instance of EventAction generated inside the parser, and by default there is not (as far as I can tell) any way to recover the resolved action from the failure message method.

A SOLUTION

The approach I use in the module is to define a number of properties on Action to hold pointers to libMessages methods:

  • allNotAllowedMsg for when the command uses “all” but this is prohibited by the action
  • noMatchForAllMsg for when the command uses “all” but no matching objects were found in scope
  • noMatchForAllButMsg for when the command uses “all” with exceptions (i.e. >TAKE ALL BUT PEBBLE) and this results in no matching objects
  • uniqueObjectRequiredMsg for when the action requires a single object but noun resolution matched multiple objects (>KICK WALLS)
  • singleObjectRequiredMsg the action requires a single object, but the command contains a list (>KICK PEBBLE, ROCK).

Each of these properties on the Action class/instance should be a pointer to a method on the appropriate parser message object (playerMessages or npcMessages), which has the same usage as the stock failure message method and should output a double-quoted string. Example:

modify playerMessages
        cantSmellAll(actor) {
                "<.parser>{You/He} can only smell one thing at a
                time.<./parser> ";
        }
;
modify SmellAction
        allNotAllowedMsg = &cantSmellAll
        actionAllowsAll = nil
;

This will produce:

>smell all
You can only smell one thing at a time.

THE MESSY DETAILS

Under the hood all of these messages are generated by BasicResolveResults throwing an exception when one of its methods (allNotAllowed(), for example) is called. The caller in this case will be one of the noun phrase productions (i.e. EverythingProd), usually in its resolveNouns() method.

The noun productions have access to the Action instance held by the resolver, but by default it isn’t passed to the results object. If parsing succeeded the Action would be available as a global (gAction), but in this case it isn’t.

The module’s solution to this is to tweak the relevant productions to pass the Action instance as an argument to the failure method (allNotAllowed() or whatever), and then to also tweak BasicResolveResults (and the relevant subclasses of it) so the methods accept the additional argument. The new argument is marked optional, so existing code shouldn’t need any modification to work with the new methods.

THE REPO

The code is here: perActionAllErrors github repo.

This is another one where the actual code is pretty minimal (it’s only around two hundred lines with comments) but it required waaay more digging than I was expecting it to, so I definitely wanted to stuff it into its own module with an explanation of how it all works. Because by this time next month I’m going to have forgotten all the details of how it works.

4 Likes