# remapcmd.t | documentation

    #charset "us-ascii"

    #pragma once

    /*
     *   Remap 'cmd' to another 'cmd'
     *
     *  Accepts in the source remap a string that has "|" and "(" for grouping.
     *  It does NOT support "*" or "?" or any wildcards nor does it accept words
     *  that use special characters outside of apostrophe (') or comma ','.
     *
     *  Several forms of the mapping occur:
     *      1. Provide the actual text for the new command
     *  OR  2. Do a normal doInstead command with the appropriate values [via execute()]
     *      3. Or put out a message with "" that ends the command [via execute()]
     *
     *
     *  Contributed by Mitchell Mlinar
     *  Copyright (c) 2025
     *
     *  Licensed using MIT defintion
     *
     *
     *  v1.00: 14 Oct 2025
     *  v1.02: 15 Oct 2025 (thanks to advice/feedback from Eric Eve)
     *  v1.03: 16 Oct 2025 (thanks to additional advice/feedback from Eric Eve)
     *
     */

    #include "advlite.h"
    #include <lookup.h>

    /*
     *   The object put into your code

     */

    /*
     *   RemapCmd is similar a Doer in that it remaps one command into another command, but
     *   there the differences end.  What are the differences?
     *
     *   RemapCmd parses up the cmd and does not make any attempt to map them to existing
     *   objects in the game.  Rather, it builds all the possible variations (by the use of
     *   "|", "(" and ")" operators) and then matches that against the user input FIRST before
     *   any other routines get a whack at it.  You can also provide two or more disjoint
     *   phrases that are separated by semi-colon ";" to provide additional phrases that
     *   match.
     *
     *   If a match is found against a RemapCmd, it will then act on it.  There are three
     *   different ways top operate on it.
     *
     *   - provide a complete text phrase replacement (remappedCmd, or use template)
     *   - if no replacement text phrase is provided, the execute() routine is run for that
     *     object; within that execute routine, you have two possibilities
     *     * emit some messages to the console via say() or "..."
     *     * use doInstead(action[,dobj[,iobj[,aobj]]]) within to execute that resolved
     *       command instead
     *   - NOTE: If text phrase is present, execute() will NOT be run!
     *
     *   Note that RemapCmd does not accept wildcards or punctuation of any sort other than
     *   "," and "'" -- and ";" to separate distinct input phrases.
     *
     */

    class RemapCmd: object
        /* The command text to be recognized (see above) */
        cmd = ''

        /*
         *   The remapped (replacement) text (if provided). This should be in the form of a command the
         *   parser can parse and exectute
         */
        remappedCmd = nil

        /*
         *   If remapped command is NOT provided, game code should override this method to do
         *   something (or display some text).
         */
        execute() {}

        /*
         *   where this can happen (nil if everywhere); can be Room/Region or list of Rooms and/or
         *   Regions.
         */
        where = nil

        /* An expression defining under what circumstances this RemapCmd is matched. */
        when = true

        /*
         *   A scene that must be happening, or list one of scenes of which must be happening for
         *   this to happen (nil if no scene is required)
         */
        during = nil

        /*
         *   By default, you generally want any non-system command to take a turn.
         *   However, there may also be other circumstances where a turn should not be
         *   consumed.  Change to 0 if we want execute() to NOT count as a turn.
         *   This property is ignored if doInstead(...) is called within execute()
         */
        turnsTaken = 1

        //////////////////////////////////////////
        // Internals
        //

        /* Execute our custom method and then our turn sequence. For internal use only. */
        execute_()
        {
            "<.p0>";
            execute();

            turnSequence();
        }

        /*
         *   If doInstead has been used in our execute() method, then call the standard turn sequence
         *   routine to execute any Events and update the turn counter. For internal use only.
         */
        turnSequence() {
            if(doInsteadItems == nil)
                delegated Action;
        }

        /*
         *   A list containing the actoin and objects defined by a call to doInstead in our execute
         *   routine, or nil if doInstead() wasn't used. For internal use only
         */
        doInsteadItems = nil

        /*
         *   Populate our doInsteadItems from any call to doInstead() in our execute() routine. For
         *   internal use only.
         */
        doInstead(action,[args]) {
            /* get the optional items */
            local dobj = args.element(1);
            local iobj = args.element(2);
            local aobj = args.element(3);

            local deftext = '';
            local len = args.length();

            if(doInsteadItems != nil) {
                "ERROR in doInstead(...) for remapCmd:\nYou cannot have more than one
                doInstead\b";
                abort;
            }
            if(action == nil)
                deftext += 'action is undefined!\n';
            if(dobj == nil && len > 0)
                deftext += 'direct object is set, but not defined/understood!\n';
            if(iobj == nil && len > 1)
                deftext += 'indirect object is set, but not defined/understood!\n';
            if(aobj == nil && len > 2)
                deftext += 'auxiliary object is set, but not defined/understood!\n';
            if(deftext != '') {
                "ERROR in doInstead(...) for remapCmd:\n<<deftext>>\b";
                abort;
            }
            doInsteadItems = [action,dobj,iobj,aobj];
        }

        /* The strings that are the command(s) to match. For internal use only. */
        cmdTerms = []

        /* The hash for these strings (faster compare later). For internal use only. */
        cmdTermHash = []
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   This gathers up all of the RemapCmd items and gets them ready for execution in the
     *   actual game
     *
     *  This is persistent and there should only be one of these
     *
     */

    remapCmdDicts: PreinitObject
        /* Lookup table with hash and then list of objects that match it */
        remapTbl = nil

        /* The pre-init execution routine */
        execute() {
            remapTbl = new LookupTable(50,50);
            local scmp = new StringComparator(nil,true,nil);    // case-sensitive as lower anyway

            for(local obj = firstObj(RemapCmd); obj != nil; obj = nextObj(obj,RemapCmd)) {
                // process the command
                local res, toks;
                local lst = [];
                foreach(local str in obj.cmd.split(';')) {
                    try {
                        toks = remapCmdTokenizer.tokenize(str);
                        match = remapCmdGrammar.parseTokens(toks,nil);
                        if(match.length > 0) {
                            res = match[1].lstval();
                            lst = remapCmdGrammarOr(lst,res);
                        } else
                            throw new EvalToksError(toks);
                    }
                    catch (TokErrorNoMatch err)
                    {
                        "Unrecognized punctuation: <<err.remainingStr_.substr(1, 1)>>";
                        break;
                    }
                    catch (EvalToksError err)
                    {
                        "Command phrase cannot be processed: <<str>>";
                        break;
                    }
                }
                // have the parsed up items -- now join them up into individual possible commands
                foreach(toks in lst) {
                    // always only allow ONE space between words
                    res = toks.join(' ').findReplace(R'<Space><Space>+',' ',ReplaceAll);
                    res = res.findReplace(R'(^<Space>+)|(<Space>+$)','',ReplaceAll);
                    obj.cmdTerms = obj.cmdTerms.append(res);
                    res = scmp.calcHash(res);
                    obj.cmdTermHash = obj.cmdTermHash.append(res);
                    addHashMap(res,obj);
                }
            }
        }

        /* add to the remapTbl for hash lookup */
        addHashMap(hash,obj) {
            if(remapTbl.isKeyPresent(hash)) {
                remapTbl[hash] = remapTbl[hash].append(obj);
            } else {
                remapTbl[hash] = [obj];
            }
        }

        /* process a tokenized string: return new string, obj if deferred, or nil if no match */
        processCmd(toks,tokcnt) {
            local scmp = new StringComparator(nil,true,nil);    // everything is lower case
            local acmd = '', ahash, obj, v;    // the string and its hash

            /* build the command */
            if(tokcnt > 0)
                toks = toks.sublist(1,tokcnt);
            acmd = toks.mapAll({x:x[1]});
            acmd = acmd.join(' ');
            ahash = scmp.calcHash(acmd);

            if(!remapTbl.isKeyPresent(ahash))
                return nil;

            /* scan the items that fit */
            foreach(obj in remapTbl[ahash]) {
                // support list of locations???
                if(obj.where != nil && valToList(obj.where).indexWhich({x:gLocation.isOrIsIn(x)}) == nil ) continue; // ECSE mod
                if(!obj.when) continue; // ECSE mod
                if(obj.during != nil && valToList(obj.during).indexWhich({s:s.isHappening}) == nil) continue; // ECSE mod
                for(v = 1; v <= obj.cmdTermHash.length(); ++v) {
                    if(obj.cmdTermHash[v] == ahash &&
                       scmp.matchValues(obj.cmdTerms[v],acmd) != 0) {
                        // found it!
                        obj.doInsteadItems = nil;  // clear it out before we move on
                        if(obj.remappedCmd != nil)
                            return obj.remappedCmd;
                        return obj;
                    }
                }
            }
            return nil;
        }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   remapCmd tokenizer for US English.  Other language modules should
     *   provide their own tokenizers to allow for differences in punctuation
     *   and other lexical elements.
     *
     */
    enum token tokOp;

    /* Exception to handle exceptions occurring in the remapCmd tokenizer */
    class EvalToksError: Exception
        displayException() { "Evaluate tokenizer exception -- unable to parse"; }
    ;

    /* Tokenizer for use with RemapCmd */
    remapCmdTokenizer: Tokenizer
        rules_ = static
        [
            /* skip whitespace */
            ['whitespace', R'<Space>+', nil, &tokCvtSkip, nil],

            /*
             *   Words - note that we convert everything to lower-case.  A
             *   word can start with any letter or number and then
             *   alphabetics, digits, hyphens, and apostrophes after that. (tokWord,tokString)
             */
            ['word', R'<AlphaNum>(<AlphaNum>|[-\'])*', tokWord, &tokCvtLower, nil],

            // handle , as a separate item for conversations
            ['comma', ',', tokWord, nil, nil],

            /*
             *   Single-quoted strings also allowed and only way to get empty string
             */
            ['string', R'\\?\'(<AlphaNum>|[-\'])*\\?\'', tokWord, &tokCvtStripSingle, nil],

            // operators
            ['emptystring',R'(<vbar><Space>*<rparen>)|(<lparen><Space>*<vbar>)', tokOp, &tokDoEmptyString, nil],
            ['operator', R'[|()]', tokOp, nil, nil]
        ]

        /* strip the leading and trailing single-quote -- and then push to lower-case */
        tokCvtStripSingle(txt, typ, toks)
        {
            if(txt.startsWith('\\'))
                txt = txt.substr(2);
            if(txt.endsWith('\\'))
                txt = txt.substr(1,txt.length() - 1);
            local newlen = txt.length() - 2;
            toks.append([txt.substr(2,newlen).toLower(), typ, txt]);
        }

        /* handle the |) or the (| sequence */
        tokDoEmptyString(txt, typ, toks)
        {
            local len = txt.length();
            toks.append([txt.substr(1,1),typ,txt.substr(1,1)]);
            toks.append(['',tokWord,'']);
            toks.append([txt.substr(len,1),typ,txt.substr(len,1)]);
        }
    ;

    //////////////////////////////////////////////////////////
    // Define the remapCmd grammar for the parser

    /* The most basic left level grammar map */
    grammar remapCmdGrammar(lit): tokWord->txt_: Production
        lstval() {
            lst_ = [[txt_]];
            return lst_;
        }
    ;

    /* handle concatenation of two words into the list
     note the use of badness to prioritize the correct conversions */
    grammar remapCmdGrammar(concat): [badness 50]
        remapCmdGrammar->pp_  remapCmdGrammar->pp2_ : Production
        lstval() {
            return remapCmdGrammarConcat(pp_.lstval(),pp2_.lstval());
        }
    ;

    grammar remapCmdGrammar(or): [badness 30]
        remapCmdGrammar->pp_ '|' remapCmdGrammar->pp2_ : Production
        lstval() {
            return remapCmdGrammarOr(pp_.lstval(),pp2_.lstval());
        }
    ;

    grammar remapCmdGrammar(tail): [badness 40]
        remapCmdGrammar->pp_  remapCmdGrammar->pp2_ '|' remapCmdGrammar->pp3_: Production
        lstval() {
            local lst = remapCmdGrammarOr(pp2_.lstval(),pp3_.lstval());
            return remapCmdGrammarConcat(pp_.lstval(),lst);
        }
    ;

    grammar remapCmdGrammar(grp): [badness 20]
        '(' remapCmdGrammar->pp_ ')' : Production
        lstval() {
            return pp_.lstval();
        }
    ;

    /*
     *   Helper routines for the grammar text expansion
     */
    remapCmdGrammarConcat(lstleft,lstright) {
        local lft, rght;
        local lst = [];
        for(local i = 1; i <= lstleft.length(); ++i) {
            for(local j = 1; j <= lstright.length(); ++j) {
                lft = lstleft[i];
                rght = lstright[j];
                for(local k = 1; k <= rght.length(); ++k)
                    lft = lft.append(rght[k]);
                lst = lst.append(lft);
            }
        }
        return lst;
    }

    remapCmdGrammarOr(lst,lstright) {
        for(local j = 1; j <= lstright.length(); ++j) {
            lst = lst.append(lstright[j]);
        }
        return lst;
    }

    /////////////////////////////////////////////////////////////////////////////////

    /* Modifications to the Parser to accommodate RemapCmd */
    modify Parser

        // return nil if an error; otherwise, return token list that could be empty
        parseToksOnly(str)
        {
            /* Make sure our current SpecialVerb is set to nil before we start parsing a new command. */
            specialVerbMgr.currentSV = nil;

            /* tokenize the input */
            local toks;

            try
            {
                /* run the command tokenizer over the input string */
                toks = cmdTokenizer.tokenize(str);

                /* Dispose of any unwanted terminal punctuation */
                while(toks.length > 0 && getTokType(toks[toks.length]) == tokPunct)
                    toks = toks.removeElementAt(toks.length);

            }
            catch (TokErrorNoMatch err)
            {
                /*
                 *   The tokenizer found a character (usually a punctuation
                 *   mark) that doesn't fit any of the token rules.
                 */
                DMsg(token error, 'I don\'t understand the punctuation {1}',
                     err.curChar_);

                /* give up on the parse */
                return nil;
            }
            return toks;
        }

        parse(str)
        {
            /* tokenize the input */
            local toks = parseToksOnly(str);
            if(toks == nil)
                return;

            /*
             *   Assume initially that the actor is the player character, but only
             *   if we don't have a question, since if the player is replying to a
             *   question the actor may already have been resolved.
             */
            if(question == nil)
                gActor = gPlayerChar;

            /* no spelling corrections have been attempted yet */
            local history = new transient SpellingHistory(self);

            /* we're starting with the first command in the string */
            local firstCmd = true;

            /* parse the tokens */
            try
            {
                /* if there are no tokens, simply perform the empty command */
                if (toks.length() == 0)
                {
                    /*
                     *   this counts as a new command, so forget any previous
                     *   question or typo information
                     */
                    question = nil;
                    lastTokens = nil;

                    /* process an empty command */
                    emptyCommand();

                    /* we're done */
                    return;
                }

                /* check for an OOPS command */
                local lst = oopsCommand.parseTokens(toks, cmdDict);
                if (lst.length() != 0)
                {
                    /* this only works if we have an error to correct */
                    local ui;
                    if (lastTokens == nil
                        || (ui = spellingCorrector.findUnknownWord(lastTokens))
                            == nil)
                    {
                        /* OOPS isn't available - throw an error */
                        throw new CantOopsError();
                    }

                    /* apply the correction, and proceed to parse the result */
                    toks = OopsProduction.applyCorrection(lst[1], lastTokens, ui);
                }

                 /* Update the vocabulary of any game objects with alternating/changing vocab. */
                updateVocab();

                 /* Allow the specialVerb Manager to adjust our toks */
                toks = specialVerbMgr.matchSV(toks);

                /*
                 *   Parse each predicate in the command line, until we run out
                 *   of tokens.  The beginning of a whole new command line is
                 *   definitely the beginning of a sentence, so start parsing
                 *   with firstCommandPhrase.
                 */
                for (local root = firstCommandPhrase ; toks.length() != 0 ; )
                {
                    /* we don't have a parse list yet */
                    local cmdLst = nil;
                    local remapCmdItem = nil;   // for later processing
                    local remapCmdTokCnt = 0;

                    /*
                     *   we haven't found a resolution error in a non-command
                     *   parsing yet
                     */
                    local qErr = nil, defErr = nil;

                    /*
                     *   If we have an outstanding question, and it takes
                     *   priority over interpreting input as a new command, try
                     *   parsing the input against the question.  Only do this
                     *   on the first command on the line - a question answer
                     *   has to be the entire input, so if we've already parsed
                     *   earlier commands on the same line, this definitely
                     *   isn't an answer to a past question.
                     */
                    if (firstCmd && question != nil && question.priority)
                    {
                        /* try parsing against the Question */
                        local l = question.parseAnswer(toks, cmdDict);

                        /* if it parsed and resolved, this is our command */
                        if (l != nil && l.cmd != nil)
                            cmdLst = l;

                        /* if it parsed but didn't resolved, note the error */
                        if (l != nil)
                            qErr = l.getResErr();
                    } else {
                        remapCmdTokCnt = 0;
                        while(++remapCmdTokCnt <= toks.length()) {
                            if(toks[remapCmdTokCnt][1] == ';' ||
                               toks[remapCmdTokCnt][1] == '.') {
                                break;
                            }
                        }
                        // if i is 1, then semicolon is the (empty) command!
                        if(--remapCmdTokCnt > 0) {
                            if(remapCmdTokCnt == toks.length())
                                remapCmdTokCnt = 0;
                            remapCmdItem = remapCmdDicts.processCmd(toks,remapCmdTokCnt);
                            if(dataType(remapCmdItem) == TypeSString) {
                                local remaptoks = parseToksOnly(remapCmdItem);
                                if(remaptoks == nil)
                                    return;
                                local i = remapCmdTokCnt;
                                remapCmdTokCnt = remaptoks.length();
                                // rip out old tokens and replace with new ones!
                                if(i > 0) {
                                    while(++i <= toks.length())
                                        remaptoks = remaptoks.append(toks[i]);
                                }
                                // readjust the token set
                                toks = remaptoks;
                                remapCmdItem = nil;
                            }
                        }
                    }

                    /*
                     *   if the question didn't grab it, try parsing as a whole
                     *   new command against the ordinary command grammar
                     */
                    if (cmdLst == nil || cmdLst.cmd == nil)
                    {
                        if(remapCmdItem == nil) {
                            cmdLst = new CommandList(
                                root, toks, cmdDict, { p: new Command(p) });
                        } else {
                            remapCmdItem.execute_(); // ECSE mod
                            if(remapCmdItem.doInsteadItems != nil) {
                                // create the artificial command
                                local cobj = remapCmdItem.doInsteadItems;
                                local c2;
                                if(cobj[2] == nil)
                                    c2 = new Command(cobj[1]);
                                else if(cobj[3] == nil)
                                    c2 = new Command(cobj[1],cobj[2]);
                                else if(cobj[4] == nil)
                                    c2 = new Command(cobj[1],cobj[2],cobj[3]);
                                else
                                    c2 = new Command(cobj[1],cobj[2],cobj[3],cobj[4]);
                                // fix c2 for items needed later here
                                c2.endOfSentence = true;   // remapped commands are stand-alone
                                c2.nextTokens = remapCmdTokCnt > 0?
                                    toks.sublist(remapCmdTokCnt+2) : [];
                                cmdLst = new CommandList(c2);
                            } else {
                                firstCmd = nil;

                                /* start over with a new spelling correction history */
                                history = new transient SpellingHistory(self);

                                // since we remapped, it will always be end-of-sentence
                                root = firstCommandPhrase;
                                /*
                                 *   Set the root grammar production for the next
                                 *   predicate.  If the previous command ended the
                                 *   sentence, start a new sentence; otherwise, use the
                                 *   additional clause syntax.
                                 */

    //                            root = cmd.endOfSentence
    //                                ? firstCommandPhrase : commandPhrase;
    //

                                /* go back and parse the remainder of the command line */
                                /* start index is 1 and have to skip the semi-colon as well */
                                if(remapCmdTokCnt > 0)
                                    toks = toks.sublist(remapCmdTokCnt+2);
                                else
                                    toks = [];
                                continue;
                            }
                        }
                    }

                    /*
                     *   If we didn't find any resolvable commands, and this is
                     *   the first command, check to see if it's an answer to
                     *   an outstanding query.  We only check this if the
                     *   regular grammar parsing fails, because anything that
                     *   looks like a valid new command overrides a past query.
                     *   This is important because some of the short, common
                     *   commands sometimes can look like noun phrases, so we
                     *   explicitly give preference to interpreting these as
                     *   brand new commands.
                     */
                    if (cmdLst.cmd == nil
                        && firstCmd
                        && question != nil
                        && !question.priority)
                    {
                        /* try parsing against the Question */
                        local l = question.parseAnswer(toks, cmdDict);

                        /* if it parsed and resolved, this is our command */
                        if (l != nil && l.cmd != nil)
                            cmdLst = l;

                        /* if it parsed but didn't resolved, note the error */
                        if (l != nil)
                            qErr = l.getResErr();
                    }

                    /*
                     *   If we don't have a command yet, and this is the first
                     *   command on the line, handle it as a conversational command
                     *   if conversation is in progress; otherwise if default
                     *   actions are enabled, check to see if the command looks like
                     *   a single noun phrase.  If so, handle it as the default
                     *   action on the noun.
                     */
                    if (cmdLst.cmd == nil
                        && firstCmd)
                    {
                        local l;

                        /*
                         *   If a conversation is in progress parse the command line
                         *   as the single topic object phrase of a Say command,
                         *   provided that the first word on the command line
                         *   doesn't match a possible action.
                         */

                        if(gPlayerChar.currentInterlocutor != nil
                           && cmdLst.length == 0
                           && Q.canTalkTo(gPlayerChar,
                                          gPlayerChar.currentInterlocutor)
                           && str.find(',') == nil
                           && gPlayerChar.currentInterlocutor.allowImplicitSay())
                        {
                             l = new CommandList(
                                topicPhrase, toks, cmdDict,
                                { p: new Command(SayAction, p) });

                            libGlobal.lastCommandForUndo = str;
                            savepoint();
                        }
                        /*
                         *   If the player char is not in conversation with anyone,
                         *   and the first word of the command doesn't match a possible
                         *   command verb, then try parsing the command line as a
                         *   single direct object phrase for the DefaultAction verb,
                         *   provided defaultActions are enabled (which they are
                         *   by default).
                         */
                        else if(defaultActions)
                            l = new CommandList(
                                defaultCommandPhrase, toks, cmdDict,
                                { p: new Command(p) });

                        /* accept a curable reply */
                        if (l != nil && l.acceptCurable() != nil)
                        {
                            cmdLst = l;

                            /* note any resolution error */
                            defErr = l.getResErr();
                        }
                    }

                    /*
                     *   If we've applied a spelling correction, and the
                     *   command match didn't consume the entire input, make
                     *   sure what's left of the input has a valid parsing as
                     *   another command.  This ensures that we don't get a
                     *   false positive by excessively shortening a command,
                     *   which we can sometimes do by substituting a word like
                     *   "then" for another word.
                     */
                    if (cmdLst.length() != nil
                        && history.hasCorrections())
                    {
                        /* get the best available parsing */
                        local c = cmdLst.getBestCmd();

                        /* if it doesn't use all the tokens, check what's left */
                        if (c != nil && c.tokenLen < toks.length())
                        {
                            /* try parsing the next command */
                            local l = commandPhrase.parseTokens(
                                c.nextTokens, cmdDict);

                            /*
                             *   if that didn't work, invalidate the command by
                             *   substituting an empty command list
                             */
                            if (l.length() == 0)
                                cmdLst = new CommandList();
                        }
                    }

                    /*
                     *   If we didn't find a parsing at all, it's a generic "I
                     *   don't understand" error.  If we found a parsing, but
                     *   not a resolution, reject it if it's a spelling
                     *   correction.  We only want completely clean spelling
                     *   corrections, without any errors.
                     */
                    if (cmdLst.length() == 0
                        || (history.hasCorrections()
                            && cmdLst.getResErr() != nil
                            && !cmdLst.getResErr().allowOnRespell))
                    {
                        /*
                         *   If we were able to parse the input using one of
                         *   the non-command interpretations, use the
                         *   resolution error from that parsing.  Otherwise, we
                         *   simply can't make any sense of this input, so use
                         *   the generic "I don't understand" error.
                         */
                        local err = (qErr != nil ? qErr :
                                     defErr != nil ? defErr :
                                     new NotUnderstoodError());

                        /* look for a spelling correction */
                        local newToks = history.checkSpelling(toks, err);
                        if (newToks != nil)
                        {
                            /* parse again with the new tokens */
                            toks = newToks;
                            continue;
                        }

                        /*
                         *   There's no spelling correction available.  If we've
                         *   settled on an auto-examine or question error, skip
                         *   that and go back to "I don't understand" after
                         *   all.  We don't want to assume Auto-Examine unless we
                         *   actually have something to examine, since we can
                         *   parse noun phrase grammar out of practically any
                         *   input.
                         */
                        if (err is in (defErr, qErr))
                        {
                            /* return to the not-understood error */
                            err = new NotUnderstoodError();

                            /* check spelling again with this error */
                            newToks = history.checkSpelling(toks, err);
                            if (newToks != nil)
                            {
                                /* parse again with the new tokens */
                                toks = newToks;
                                continue;
                            }

                            /*
                             *   We didn't find any spelling corrections this time
                             *   through.  Since we're rolling back to the
                             *   not-understood error, discard any spelling
                             *   corrections we attempted with other
                             *   interpretations.
                             */
                            history.clear();
                        }

                        /* fail with the error */
                        throw err;
                    }

                    /* if we found a resolvable command, execute it */
                    if (cmdLst.cmd != nil)
                    {
                        /* get the winning Command */
                        local cmd = cmdLst.cmd;

                        /*
                         *   We next have to ensure that the player hasn't entered
                         *   multiple nouns in a slot that only allows a single noun
                         *   in the grammar. If the player has entered two objects
                         *   like "the bat and the ball" in such a case, the
                         *   badMulti flag will be set on the command object, so we
                         *   first test for that and abort the command with a
                         *   suitable error message if badMulti is not nil (by
                         *   throwing a BadMultiError
                         *
                         *   Unfortunately the badMulti flag doesn't get set if the
                         *   player enters a multiple object as a plural (e.g.
                         *   "bats"), so we need to trap this case too. We do that
                         *   by checking whether there's multiple objects in the
                         *   direct, indirect and accessory object slots at the same
                         *   time as the grammar tag matching the slot in question
                         *   is 'normal', which it is only for a single noun match.
                         */

                        if(cmd && cmd.verbProd != nil &&
                            (cmd.badMulti != nil
                           || (cmd.verbProd.dobjMatch != nil &&
                               cmd.verbProd.dobjMatch.grammarTag == 'normal'
                               && cmd.dobjs.length > 1)
                           ||
                           (cmd.verbProd.iobjMatch != nil &&
                               cmd.verbProd.iobjMatch.grammarTag == 'normal'
                               && cmd.iobjs.length > 1)
                            ||
                           (cmd.verbProd.accMatch != nil &&
                               cmd.verbProd.accMatch.grammarTag == 'normal'
                               && cmd.accs.length > 1)
                               ))
                            cmd.cmdErr = new BadMultiError(cmd.np);

                        /* if this command has a pending error, throw it */
                        if (cmd.cmdErr != nil)
                            throw cmd.cmdErr;

                        /*
                         *   Forget any past question and typo information.
                         *   The new command is either an answer to this
                         *   question, or it's simply ignoring the question; in
                         *   either case, the question is no longer in play for
                         *   future input.
                         */
                        question = nil;
                        lastTokens = nil;

                        /* note any spelling changes */
                        history.noteSpelling(toks);

                        /* execute the command */
                        cmd.exec();

                        /* start over with a new spelling correction history */
                        history = new transient SpellingHistory(self);

                        /*
                         *   Set the root grammar production for the next
                         *   predicate.  If the previous command ended the
                         *   sentence, start a new sentence; otherwise, use the
                         *   additional clause syntax.
                         */
                        root = cmd.endOfSentence
                            ? firstCommandPhrase : commandPhrase;

                        /* we're no longer on the first command in the string */
                        firstCmd = nil;

                        /* go back and parse the remainder of the command line */
                        toks = cmd.nextTokens;
                        continue;
                    }

                    /*
                     *   We weren't able to resolve any of the parse trees.  If
                     *   one of the errors is "curable", meaning that the
                     *   player can fix it by answering a question, pick the
                     *   first of those, in predicate priority order.
                     *   Otherwise, just pick the first command overall in
                     *   predicate priority order.  In either case, since we
                     *   didn't find any working alternatives, it's time to
                     *   actually show the error and fail the command.
                     */
                    local c = cmdLst.acceptAny();

                    /*
                     *   If the error isn't curable, check for spelling errors,
                     *   time permitting.  Don't bother doing this with a
                     *   curable error, since that will have its own way of
                     *   solving the problem that reflects a better
                     *   understanding of the input than considering it a
                     *   simple typo.
                     */
                    if (!c.cmdErr.curable)
                    {
                        /*
                         *   For spelling correction purposes, if this is an
                         *   unmatched noun error, but the command has a misc
                         *   word list and an empty noun phrase, treat this as
                         *   a "not understood" error.  The combination of noun
                         *   phrase errors suggests that we took a word that
                         *   was meant to be part of the verb, and incorrectly
                         *   parsed it as part of a noun phrase, leaving the
                         *   verb structure and other noun phrase incomplete.
                         *   This is really a verb syntax error, not a noun
                         *   phrase error.
                         */
                        local spellErr = c.cmdErr;
                        if (c.cmdErr.ofKind(UnmatchedNounError)
                            && c.miscWordLists.length() > 0
                            && c.missingNouns > 0)
                            spellErr = new NotUnderstoodError();

                        /* try spelling correction */
                        local newToks = history.checkSpelling(toks, spellErr);

                        /* if that worked, try the corrected command */
                        if (newToks != nil)
                        {
                            /* parse again with the new tokens */
                            toks = newToks;
                            continue;
                        }
                    }

                    /* re-throw the error that caused the resolution to fail */
                    throw c.cmdErr;
                }
            }
            catch (ParseError err)
            {
                /*
                 *   roll back any spelling changes to the last one that
                 *   improved matters
                 */
                local h = history.rollback(toks, err);
                toks = h.oldToks;
                err = h.parseError;

                /*
                 *   if this is a curable error, it poses a question, which the
                 *   player can answer on the next input
                 */
                if (err.curable)
                    question = new ParseErrorQuestion(err);

                /*
                 *   If the current error isn't curable, and unknown word
                 *   disclosure is enabled, and there's a word in the command
                 *   that's not in the dictionary, replace the parsing error
                 *   with an unknown word error.
                 */
                local ui;
                if (!err.curable
                    && showUnknownWords
                    && (ui = spellingCorrector.findUnknownWord(toks)) != nil)
                {
                    /* find the misspelled word in the original tokens */
                    err = new UnknownWordError(getTokOrig(toks[ui]));
                }

                /*
                 *   If the new error isn't an error in an OOPS command, save
                 *   the token list for an OOPS command next time out.
                 */
                if (!err.ofKind(OopsError))
                    lastTokens = toks;

                /* log any spelling changes we kept */
                history.noteSpelling(toks);

                /* display the error we finally decided upon */
                err.display();
            }
            catch (CommandSignal sig)
            {
                /*
                 *   On any command signal we haven't caught so far, simply
                 *   stop processing this command line.
                 */
            }
        }
    ;

    ////////////////////////////////////////////////////////////////////////////////

    ////////////////////////////////////////////////////

    /*
     *   This is only emabled for testing the tokenizer and parser if you want to see how it
     *   operates.  You will need to run tok_test2() from somewhere (I usually put it in
     *   showIntro for quick/easy/immediate access
     */

    #if 0
    tok_test2()
    {
        local tmap = new LookupTable([tokWord,'word',tokString,'string',tokOp,'operator']);

        // get grammar info
    //    local ginfo = remapCmdGrammar.getGrammarInfo();
    //    local val = spelledToInt('ninety-six');   // to see how it is done

        "Enter text to tokenize.  Type Q or QUIT when done. ";
        for (;;)
        {
            local str, toks, tok, match, res, c, lsthold;

            /* read a string */
            "\b>";
            str = inputLine().split(';');

            lsthold = [];
            foreach(c in str) {
                try {
                    toks = remapCmdTokenizer.tokenize(c);
                    //getTokVal(tok) returns the parsed value of the token.
                    //getTokType(tok) returns the type of the token.
                    //getTokOrig(tok) returns the original source text the token matched.
                    /* display the tokens */
                    for (local i = 1, local cnt = toks.length() ; i <= cnt ; ++i) {
                        tok = toks[i];
                        "(<<getTokVal(tok)>>,<<tmap[getTokType(tok)]>>)";
                    }
                    "\n";
                    // this phrase is set -- parse it up
                    match = remapCmdGrammar.parseTokens(toks,nil);
                    if(match.length > 0) {
                        res = match[1].lstval();
                        dumpList(res); "\n";
                        lsthold = remapCmdGrammarOr(lsthold,res);
                    }
                    else
                        "?????\n";
                    "-----\n";
                }
                catch (TokErrorNoMatch err)
                {
                    "Unrecognized punctuation: <<err.remainingStr_.substr(1, 1)>>";
                }
            }
            "------ Final -----\n";
            dumpList(lsthold);
        }
    }

    // simple list dumper
    dumpList(lst) {
        if(lst == nil)
            "<nil>";
        else {
            "[";
            local prefix = '';
            foreach(local item in lst) {
                "<<prefix>>";
                prefix = ',';
                if(dataType(item) == TypeList)
                    dumpList(item);
                else if(item == ',')
                    "(comma)";
                else {
                    item = toString(item);
                    item = item.findReplace(',','(comma)',ReplaceAll);
                    "<<item>>";
                }
            }
            "]";
        }
    }

    getDataType(data) {
        local s = '?';
        switch(dataType(data)) {
            case TypeObject: s='object'; break;
            case TypeList: s='list'; break;
            case TypeSString: s='single-string'; break;
            case TypeInt: s='integer'; break;
            case TypeFuncPtr: s='func-ptr'; break;
            case TypeProp: s='property'; break;
            case TypeNil: s='nil'; break;
            case TypeTrue: s='true'; break;
            case TypeEnum: s='enum'; break;

            // grammars
            case GramTokTypeProd: s='gramProd'; break;
            case GramTokTypeSpeech: s='gramSpeech'; break;
            case GramTokTypeNSpeech: s='gramNSpeech'; break;
            case GramTokTypeLiteral: s='gramLiteral'; break;
            case GramTokTypeTokEnum: s='gramEnum'; break;
            case GramTokTypeStar: s='gramStar'; break;
        }
        return s;
    }

    #endif

Adv3Lite Library Reference Manual
Generated on 08/12/2025 from adv3Lite version 2.2.2