# messages.t | documentation

    #charset "us-ascii"
    #include "advlite.h"

    /*
     *   ***************************************************************************
     *   messages.t
     *
     *   This module forms part of the adv3Lite library (c) 2012-13 Eric Eve, but is
     *   based substantially on the Mercury Library (c) 2012 Michael J. Roberts
     */

    /* ------------------------------------------------------------------------ */
    /*
     *   Global narration parameters.  This object's properties control the way
     *   parameter-based messages are generated.  The message generator
     *   consults the narration parameters each time it generates a message, so
     *   you can change the settings on the fly, and subsequent output will
     *   automatically adapt to the latest settings.
     */
    Narrator: object
        /*
         *   The verb tense of the narration.  This is one of the VerbTense
         *   objects (Present, Past, Perfect, Past Perfect, Future, Future
         *   Perfect).  This controls the way {verb} substitution parameters
         *   are generated, which in turn affects most library messages.  The
         *   default is Present, which is the conventional tense for most IF.
         *
         *   Examples of the English tenses:
         *
         *.    Present: Bob opens the box.
         *.    Past: Bob opened the box.
         *.    Perfect: Bob has opened the box.
         *.    Past Perfect: Bob had opened the box.
         *.    Future: Bob will open the box.
         *.    Future Perfect: Bob will have opened the box.
         *
         *   (Language modules are free to add their own tenses if the target
         *   language has others that would be of interest to IF authors.
         *   They're also free to ignore any of the "standard" tenses we
         *   define.  At a minimum, though, some form of present tense should
         *   always be provided, since most IF is narrated in the present.  If
         *   you need to differentiate among different present tenses, you
         *   might prefer to define your own IDs instead of using the generic
         *   Present, but you should still support *some* present tense that's
         *   suitable for narration.  Some type of past tense usable in
         *   narration is also nice to have.  The others are probably of
         *   marginal value; in English, at least, other tenses are rare in any
         *   kind of narrative fiction, and are mostly limited to experimental
         *   or novelty use.)
         */
        tense = (gameMain.usePastTense ? Past : Present)
    ;

    /*
     *   Root class for verb tenses
     */
    class VerbTense: object
        /*
         *   Tense name.  This is just an identifier for internal and degugging
         *   purposes, so we don't bother farming it out to the language module
         *   for translation.
         */
        name = nil
    ;

    Present: VerbTense
        name = 'present'
    ;
    Past: VerbTense
        name = 'past'
    ;
    Perfect: VerbTense
        name = 'perfect'
    ;
    PastPerfect: VerbTense
        name = 'past perfect'
    ;
    Future: VerbTense
        name = 'future'
    ;
    FuturePerfect: VerbTense
        name = 'future perfect'
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Show a message.
     *
     *   This looks for a customized version of the message text, as defined in
     *   CustomMessages objects.  If we don't find one, we use the provided default
     *   message text.
     *
     *   Substitution parameters take the form {x y z} - curly braces with one or
     *   more space-delimited tokens.  The first token is the parameter name, and
     *   any additional tokens are arguments.  The parameter names and their
     *   arguments are up to the language module to define.
     *
     *   In addition to the parameters, the string itself can have two sections,
     *   separated by a vertical bar, '|'.  The first section (before the bar) is
     *   the "terse" string, which is for a straightforward acknowledgment of a
     *   simple, ordinary action: "Taken", "Dropped", etc. The terse string is used
     *   only if the Command argument's actor is the player character, AND the
     *   command doesn't have any disambiguated objects.  If these conditions aren't
     *   met, the second half of the string, the "verbose" version, is used.
     *
     *   Once we have the message text, we perform parameter substitutions.
     *   Parameters can be provided as strings, which are substituted in literally;
     *   or as objects, whose names are inserted according to the grammar in the
     *   template text.
     */

    message(id, txt, [args])
        {

            txt = buildMessage(id, txt, args...);

            /*
             *   use the oSay function to output the text to avoid an infinite
             *   recursive call through say.
             */
            oSay(txt);
        }

    /*
     *   Build a message to be shown by message()
     *
     *   We put this in a separate function to make it easy to obtain the text of a
     *   message for subsequent use without first displaying it.
     */
    buildMessage(id, txt, [args])
    {

        /* look for a customized version of the message */
        local cm = nil;
        foreach (local c in CustomMessages.all)
        {
            /*
             *   if this customizer is active and defines the message, and it
             *   has a higher priority than any previous customizer we've
             *   already found, remember it as the best candidate so far
             */
            if (c.active && c.msgTab[id] != nil
                && (cm == nil || c.priority > cm.priority))
                cm = c;
        }

        /* show debugging information, if desired */
        IfDebug(messages, debugMessage(id, txt, cm, args));

        /* if we found an override, use it instead of the provided text */
        if (cm != nil)
            txt = cm.msgTab[id];

        /* if txt is a function pointer, retrieve the value it returns */
        if(dataType(txt) == TypeFuncPtr)
            txt = txt();

        /* set up a sentence context for the expansion */
        local ctx = new MessageCtx(args);

        /*
         *   Carry out any language-specific adjustments to the text prior to any
         *   further processing
         */
        txt = langAdjust(txt);

        /*
         *   First look for a tense-swithing message substitution of the form
         *   {present-string|past-string} and choose whichever is appropriate to the
         *   tense of the game.
         */
        local bar, openBrace = 0, closeBrace = 0, newTxt = txt;
        for(;;)
        {
            /* Find the next opening brace */
            openBrace = txt.find('{', closeBrace + 1);

            /* If there isn't one, we're done, so leave the loop. */
            if(openBrace == nil)
                break;

            /* Find the next vertical bar that follows the opening brace */
            bar = txt.find('|', openBrace);

            /* If there isn't one, we're done, so leave the loop. */
            if(bar == nil)
                break;

            /* Find the next closing brace that follows the opening brace */
            closeBrace = txt.find('}', openBrace);

            /* If there isn't one, we're done, so leave the loop. */
            if(closeBrace == nil)
                break;

            /*
             *   If the bar doesn't come before the closing brace, then it's not
             *   between the two braces, so we don't want to process it in this
             *   circuit of the loop. Instead we need to see if there's another
             *   opening brace on the next iteration.
             */
            if(bar > closeBrace)
                continue;

            /*
             *   Extract the string that starts with the opening brace we found and
             *   ends with the closing brace we found.
             */
            local pString = txt.substr(openBrace, closeBrace - openBrace + 1);

            /*
             *   If the game is in the past tense, extract the second part of this
             *   above string (that following the bar up to but not including the
             *   closing brace). Otherwise extract the first part (that following
             *   but not including the opening brace up to but not including the
             *   bar)
             */
            local subString = (gameMain.usePastTense || Narrator.tense == Past) ?
                txt.substr(bar + 1, closeBrace - bar - 1) : txt.substr(openBrace +
                    1, bar - openBrace - 1);

            /*
             *   In the copy of our original text string, replace the string in
             *   braces with the substring we just extracted from it.
             */
            newTxt = newTxt.findReplace(pString, subString, ReplaceOnce);

        }

        /* Copy our adjusted string back to our original string */
        txt = newTxt;

        /* check for separate PC and NPC messages */
        bar = txt.find('|');
        if (bar != nil)
        {
            /* there's a bar - check to see if the terse format can be used */
            if (ctx.cmd != nil && ctx.cmd.terseOK())
                txt = txt.left(bar - 1);
            else
                txt = txt.substr(bar + 1);
        }

        /* get the message object */
        local mo = MessageParams.langObj;

        /* apply substitutions */
        for (local i = 1 ; i <= txt.length() ; )
        {
            /* find the end of the current sentence */
            local eos = txt.find(R'<.|!|?><space>', i) ?? txt.length() + 1;

            /*
             *   Build a list of the parameters in the sentence, and preprocess
             *   each one.  We need to preprocess the entire sentence before we
             *   can expand any of it, because some parameters can depend upon
             *   other parameters later in the sentence.  For example, some
             *   languages generally place the subject after the verb; to
             *   generate the verb with proper agreement, we need to know the
             *   subject when we expand the verb, which means we need to have
             *   scanned the entire sentence before we expand the verb.
             */
            ctx.startSentence();
            local plst = new Vector(10);
            for (local j = i ; ; )
            {
                /* find the next parameter */
                local lb = txt.find('{', j);
                if (lb == nil || lb >= eos)
                    break;

                /* find the end of the parameter */
                local rb = txt.find('}', lb + 1);
                if (rb == nil)
                    break;

                /* pull out the parameter string */
                local param = txt.substr(lb + 1, rb - lb - 1);

                /* turn it into a space-delimited token list */
                param = param.trim().split(' ');

                /*
                 *   do the preliminary expansion, but discard the result -
                 *   this gives the expander a chance to update any effect on
                 *   the sentence context
                 */
                mo.expand(ctx, param);

                /* add it to the list */
                plst.append([lb, rb, param]);

                /* move past this item */
                j = rb + 1;
            }

            /* restart the sentence */
            ctx.endPreScan();

            /* do the actual expansion */
            local delta = 0;
            for (local j = 1 ; j <= plst.length() ; ++j)
            {
                /* pull out this item */
                local cur = plst[j];
                local lb = cur[1], rb = cur[2], param = cur[3];
                local paramLen = rb - lb + 1;

                /* get the expansion */
                local sub = mo.expand(ctx, param);

                /*
                 *   if it starts with a "backspace" character ('\010', which
                 *   is a ^H character, the standard ASCII backspace), delete
                 *   any spaces immediate preceding the substitution parameter
                 */
                if (sub.startsWith('\010'))
                {
                    /* count spaces immediately preceding the parameter */
                    local m = lb + delta - 1, spCnt = 0;
                    while (m >= 1 && txt.toUnicode(m) == 32)
                        --m, ++spCnt;

                    /* if we found any spaces, splice them out */
                    if (spCnt != 0)
                    {
                        /* splice out the spaces */
                        txt = txt.splice(m + 1, spCnt, '');

                        /* adjust our delta for the deletion */
                        delta -= spCnt;
                    }

                    /* remove the backspace from the replacement text */
                    sub = sub.substr(2);
                }

                /* splice the replacement text into the string */
    //            txt = txt.splice(lb + delta, paramLen, sub);
                txt = txt.substr(1, lb + delta -1) + sub + txt.substr(lb + delta
                    + paramLen );

                /* adjust our delta to the next item for the splice */
                delta += sub.length() - paramLen;
            }

            /* move to the end of the sentence */
            i = eos + delta + 1;
        }

        return txt;
    }

    /*
     *   Message debugging.  This shows the message before processing: the ID,
     *   the default source text with the {...} parameters, the overriding
     *   custom source text, and the arguments.
     */
    #ifdef __DEBUG
    debugMessage(id, txt, cm, args)
    {
        if(id is in (nil,'','command results prefix', 'command prompt', 'command
            results suffix')
           || outputManager.curOutputStream != mainOutputStream)
            return;

        local idchk = [id, libGlobal.totalTurns];

        if(DebugCtl.messageIDs[idchk] != nil)
            return;
        else
            DebugCtl.messageIDs[idchk] = true;

        oSay('\nmessage(id=<<id>>, default text=\'<<txt>>\' ');
        if (cm != nil)
            oSay('custom text=\'<<cm.msgTab[id]>>\'');

        if (args.length() != 0)
        {
            oSay(', args={ ');
            for (local i = 1 ; i <= args.length() ; ++i)
            {
                local a = args[i];
                if (i > 1)
                    oSay(', ');
                if (dataType(a) == TypeSString)
                    oSay(''''<<args[i]>>'''');
                else
                    oSay('object(name=<<a.name>>)');
            }
            oSay(' }');
        }
        oSay(')\n');
    }
    #endif

    /* ------------------------------------------------------------------------ */
    /*
     *   Noun parameter roles for MessageCtx.noteObj()
     */

    /* the noun is the subject of the sentence */
    enum vSubject;

    /* the noun is an object of the verb */
    enum vObject;

    /*
     *   the role is ambiguous (it's not marked clearly in the case or
     *   position, for example)
     */
    enum vAmbig;

    /* the noun is a possessive, not the subject or the object */
    enum vPossessive;
    /* ------------------------------------------------------------------------ */
    /*
     *   Message expansion sentence context.  This keeps track of the parts of
     *   the sentence we've seen so far in the substitution parameters.
     *
     *   The sentence context is important for expanding certain items.  For
     *   verbs, it tells us which object is the subject, so that we can
     *   generate the agreeing conjugation of the verb (in number and
     *   grammatical person).  For direct and indirect objects, it lets us
     *   generate a reflexive when the same object appears in a second role
     *   ("You can't put the box in itself").
     */
    class MessageCtx: object
        construct(args)
        {
            /* remember the message arguments */
            self.args = args;

            /* if there's a Command among the arguments, note it */
            cmd = gCommand;

            /* if there's no command, use the placeholder */
            if (cmd == nil)
                cmd = messageDummyCommand;
        }

        /* start a new sentence */
        startSentence()
        {
            /* forget any subject/object information */
            subj = nil;
            vobj = nil;
            gotVerb = nil;
            reflexiveAnte.clear();

            /* note that we're on the initial scan */
            prescan = true;

            /* we're starting a new scan, so reset the previous parameter */
            lastParam = nil;
        }

        /*
         *   End the pre-expansion scan.  The expander makes two passes over
         *   each sentence.  The first scan doesn't actually do any
         *   substitutions, but merely invokes each parameter to give it a
         *   chance to exert its side effects on the sentence context.  The
         *   second scan actually applies the substitutions.  At the end of the
         *   first pass, the expander calls this to let us finalize the initial
         *   scan and prepare for the second scan.
         */
        endPreScan()
        {
            /*
             *   Forget the reflexive antecedent.  Reflexives are generally
             *   anaphoric (i.e., they refer back to earlier clauses in the
             *   same sentence), so we generally don't care about anything we
             *   found later in the same sentence.
             */
            reflexiveAnte.clear();

            /* we're no longer on the initial scan */
            prescan = nil;

            /* we're starting a new scan, so reset the previous parameter */
            lastParam = nil;
        }

        /*
         *   Note a parameter value.  Some parameters refer back to the
         *   immediately preceding parameter, so it's useful to have the most
         *   recent value stashed away.  Returns the parameter value as given.
         */
        noteParam(val)
        {
            /* remember the value */
            return lastParam = val;
        }

        /*
         *   Convert a parameter value to a string representation suitable for
         *   message substitution.
         */
        paramToString(val)
        {
            switch (dataType(val))
            {
            case TypeSString:
                return val;

            case TypeInt:
                return toString(val);

            case TypeObject:
                if (val.ofKind(Mentionable) || val.ofKind(Pronoun))
                    return val.name;
                else if (val.ofKind(List) || val.ofKind(Vector))
                    return val.mapAll({ x: paramToString(x) }).join(', ');
                else if (val.ofKind(BigNumber))
                    return toString(val);
                else
                    return '(object)';

            case true:
                return 'true';

            case nil:
                return '';

            default:
                return '(?)';
            }
        }

        /*
         *   Convert a parameter value to a numeric representation.  If the
         *   value is an integer or BigNumber, we return it as is; if a list or
         *   vector, we return the number of elements; if nil, 0; if a string,
         *   the parsed numeric value of the string; otherwise we simply return
         *   1.
         */
        paramToNum(val)
        {
            switch (dataType(val))
            {
            case TypeSString:
                return toInteger(val);

            case TypeInt:
                return val;

            case TypeObject:
                if (val.ofKind(BigNumber))
                    return val;
                if (val.ofKind(List) || val.ofKind(Vector))
                    return val.length();
                return 1;

            case TypeList:
                return val.length();

            case nil:
                return 0;

            default:
                return 1;
            }
        }

        /*
         *   Note an object being used as a parameter in the given sentence
         *   role.  The role is one of the noun role enums defined above:
         *   vSubject, vObject, or vAmbig.  If the object is a subject, we'll
         *   save it as the sentence subject, so that we can generate an
         *   agreeing verb.  Regardless of role, we'll also save it as a
         *   reflexive antecedent, so that we can generate a reflexive pronoun
         *   if we see the same object again in another role in the same
         *   sentence.
         */
        noteObj(obj, role)
        {
            /* remember the object as the last parameter value */
            noteParam(obj);

            /*
             *   If the role is ambiguous, guess at the role based on the
             *   general sentence order.
             */
            if (role == vAmbig && MessageParams.langObj.sentenceOrder != nil)
            {
                /* get the sentence order flags */
                local so = MessageParams.langObj.sentenceOrder;

                /* figure whether the subject/object is before/after the verb */
                local ssv = (so.find(R'S.*V') != nil ? -1 : 1);
                local osv = (so.find(R'O.*V') != nil ? -1 : 1);

                /* figure whether the subject or object comes first */
                local fo = (so.find(R'S.*O') != nil ? vSubject : vObject);

                /* figure which side of the verb we're on (-1 before, 1 after) */
                local sv = (gotVerb ? 1 : -1);

                /*
                 *   If we're on the right side of the verb for both subject
                 *   and object: if we have an object, this is the subject; if
                 *   we have a subject, this is the object; if we have neither
                 *   this is the first role in sentence order.
                 *
                 *   Otherwise, if we're on the subject side of the verb, and
                 *   we don't have a subject, this is the subject.
                 *
                 *   Otherwise, if we're on the object side of the verb, and we
                 *   don't have an object, this is an object.
                 */
                if (ssv == sv && osv == sv)
                    role = (subj != nil ? vObject :
                            vobj != nil ? vSubject : fo);
                else if (ssv == sv && subj == nil)
                    role = vSubject;
                else if (osv == sv && vobj == nil)
                    role = vObject;
            }

            /* if it's the subject, remember it as such */
            if (role == vSubject)
                subj = obj;
            else if (role == vObject)
                vobj = obj;

            /*
             *   Only record the reflexive antecent for the subject, so that the
             *   reflexive pronoun is used when an actor or object tries to act on
             *   itself.
             */
            if(role != vSubject)
                return;

            /*
             *   If there's nothing in the reflexive antecedent list that uses
             *   the same pronoun, add this to the list.  Otherwise, replace
             *   the object that used the same antecedent.
             */
    //        local p = obj.pronoun();
    //        local idx = reflexiveAnte.indexWhich({ o: o.pronoun() == p });

            /*
             *   The foregoing rule seems to generate false positives (that is, it
             *   can result in reflexive pronouns where they're not appropriate), so
             *   we'll try the different strategy of looking for anything in the
             *   reflexive ante list. The reason for this is that if a new
             *   subject is introduced into the sentence, it doesn't have to be the
             *   same gender as the previous subject to become the most likely
             *   antecedent for a reflexive.
             */
            local idx = reflexiveAnte.indexWhich({ o: o.ofKind(Thing) });

            if (idx == nil)
            {
                /*
                 *   There's no object with this pronoun yet - add it.  We want
                 *   to keep earlier antecedents with different pronouns
                 *   because we can still refer back to earlier objects
                 *   reflexively as long as it's clear which one we're talking
                 *   about.  The distinct pronouns provide that clarity.
                 */
                reflexiveAnte.append(obj);
            }
            else
            {
                /*
                 *   We already have an object with this pronoun, so replace it
                 *   with the new one.  Reflexives generally bind to the prior
                 *   noun phrase that matches the pronoun *and* is closest in
                 *   terms of word order.  Once another noun phrase intervenes
                 *   in word order, we can only refer back to an earlier one by
                 *   reflexive pronoun if the earlier noun is distinguishable
                 *   from the later one by its use of a distinct pronoun from
                 *   the later one.  For example, we can say "Bob asked Sue
                 *   about himself", because "himself" could only refer to Bob,
                 *   but most people would take "Bob asked Sam about himself"
                 *   to mean that Bob is asking about Sam.
                 */
                reflexiveAnte[idx] = obj;
            }
        }

        /*
         *   Note a verb parameter.
         */
        noteVerb()
        {
            /* note that we got a verb */
            gotVerb = true;
        }

        /*
         *   Was the last parameter value plural?  If the value is numeric, 1
         *   is singular and anything else is plural.  If it's a list, a
         *   one-element list is singular and anything else is plural.  If it's
         *   a Mentionable, the 'plural' property determines it.
         */
        lastParamPlural()
        {
            switch (dataType(lastParam))
            {
            case TypeInt:
                return lastParam != 1;

            case TypeObject:
                if (lastParam.ofKind(Mentionable) || lastParam.ofKind(Pronoun))
                    return lastParam.plural;
                else if (lastParam.ofKind(List) || lastParam.ofKind(Vector))
                    return lastParam.length() != 1;
                else if (lastParam.ofKind(BigNumber))
                    return lastParam != 1;
                else
                    return nil;

            case TypeList:
                return lastParam.length() != 1;

            default:
                return nil;
            }
        }

        /*
         *   Is the actor involved in the Command the PC?  If there's a Command
         *   with an actor, we check to see if it's the PC.  If there's no
         *   Command or no actor, we assume that the PC is the relevant actor
         *   (since there's nothing else specified anywhere) and return true.
         */
        actorIsPC()
        {
            return cmd == nil || cmd.actor == nil
                || cmd.actor == gPlayerChar;
        }

        /* the last parameter value */
        lastParam = nil

        /* are we on the initial pre-expansion scan? */
        prescan = nil

        /* the subject of the sentence (as a Mentionable object) */
        subj = nil

        /* the last object of the verb we saw */
        vobj = nil

        /* have we seen a verb parameter in this sentence yet? */
        gotVerb = nil

        /* the message argument list */
        args = nil

        /* the Command object among the arguments, if any */
        cmd = nil

        /*
         *   The reflexive antecedents.  Each time we see an object in a
         *   non-subject role, and the object has different pronoun usage from
         *   any previous entry, we'll add it to this list.  If we see the same
         *   object subsequently in another non-subject role, we'll know that
         *   we should generate a reflexive pronoun for the object rather than
         *   the name or a regular pronoun:
         *
         *   You can't put the tongs in the box with the tongs -> with
         *   themselves
         */
        reflexiveAnte = perInstance(new Vector(5))
    ;

    /*
     *   Dummy command placeholder for messages generated without a command.
     */
    messageDummyCommand: object
        /* use the player character as the actor */
        actor = (gPlayerChar)

    ;

    /*
     *    Use the message builder to format a message without supplying a key
     *    to look up at alternative message. We can use this with library
     *    messages that employ object properties (e.g. cannotTakeMsg) or user
     *    code.
     *
     *    dmsg() displays the resultant message.
     */
    dmsg(txt, [args])
    {
        message('', txt, args...);
    }

    /* bmsg returns the text of a message formatted by the message formatter. */
    bmsg(txt, [args])
    {
        return buildMessage('', txt, args...);
    }

    /* ------------------------------------------------------------------------ */
    /*
     *   Message customizer object.  Language extensions and games can use this
     *   class to define their own custom messages that override the default
     *   English messages used throughout the library.
     *
     *   Each CustomMessages object can define a list of messages to be
     *   customized.  This lets you centrally locate all of your custom
     *   messages by putting them all in a single object, if you wish.
     *   Alternatively, you can create separate objects, if you prefer to keep
     *   them with some other body of code they apply to.  In either case, the
     *   library gathers them all up during preinit.
     */
    class CustomMessages: object
        /*
         *   The priority determines the precedence of a message defined in
         *   this object, if the same message is defined in more than one
         *   CustomMessages object.  The message with the highest priority is
         *   the one that's actually displayed.
         *
         *   The library defines one standard priority level: 100 is the
         *   priority for language module overrides.  Each language module
         *   provides a translated set of the standard library messages, via a
         *   CustomMessages object with priority 100.  (The default English
         *   messages defined in-line throughout the library via DMsg() macros
         *   effectively have a priority of negative infinity, since any custom
         *   message of any priority overrides a default.)
         *
         *   Games will generally want to override all library messages,
         *   including translations, so we set the default here to 200.
         */
        priority = 200

        /*
         *   Is this customizer active?  If you want to change the messages at
         *   different points in the course of the game, you can use this to
         *   turn sets of messages on and off.  For example, if your game
         *   includes narrator changes at certain points, you can create
         *   separate sets of messages per narrator.  By default, we make all
         *   customizations active, but you can override this to turn selected
         *   messages on and off as needed.  Note that the library consults
         *   this every time it looks up a message, so you can change the value
         *   dynamically, or use a method whose return value changes
         *   dynamically.
         */
        active = true

        /*
         *   The message list.  This can contain any number of messages; the
         *   order isn't important.  Each message is defined with a Msg()
         *   macro:
         *
         *.     Msg(id key, 'Message text'),  ...
         *
         *   The "id key" is the message ID that the library uses in the DMsg()
         *   message that you're customizing.  (DON'T use quotes around the ID
         *   key.)  The message text is a single-quoted string giving the
         *   message text.  This can contain curly-brace replacement
         *   parameters.
         */
        messages = []

        /*
         *   Construction.  Build the lookup table of our messages for fast
         *   access at run-time.
         */
        construct()
        {
            /* add me to the master list of message customizers */
            CustomMessages.all += self;

            /* create the lookup table */
            msgTab = new LookupTable(64, 128);

            /* populate it with our key->string mappings */
            for (local i = 1, local len = messages.length() ; i <= len ; i += 2)
                msgTab[messages[i]] = messages[i+1];
        }

        /* message lookup table - this maps ID keys to message text strings */
        msgTab = nil

        /*
         *   class property: the master list of CustomMessages objects defined
         *   throughout the game
         */
        all = []
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Message Parameter Handler.  This object defines and handles parameter
     *   expansion for '{...}' strings in displayed messages.
     *
     *   The language module must provide one instance of this class.  The name
     *   of the instance doesn't matter - we'll find it at preinit time.  The
     *   object must provide the 'params' property giving the language-specific
     *   list of substitution parameter names and handler functions.
     */
    class MessageParams: object
        /*
         *   Expand a parameter string.  'ctx' is a MessageCtx object with the
         *   current sentence context.  This contains the message expansion
         *   arguments (ctx.args), the Command object from the arguments
         *   (ctx.cmd), and information on the grammar elements of the
         *   sentence.  'params' is the list of space-delimited tokens within
         *   the curly-brace parameter string.  Returns the string to
         *   substitute for the parameter in the message output.
         */
        expand(ctx, params)
        {
            /* look up the parameter name */
            local pname = params[1].trim();
            local t = paramTab[pname.toLower()];

            /*
             *   If we found an entry, let the entry handle it.  If there's no
             *   matching entry, it's basically an error: return the original
             *   parameter source text, with the braces restored, so that it'll
             *   be obvious in the output.
             */
            local txt = nil;
            if (t != nil)
            {
                /*
                 *   If we have not previously identified a subject for this
                 *   context, use the dummy_ object, which provides a dummy third
                 *   person singular noun as a default subject for the verb.
                 */
                if(ctx.subj == nil)
                    ctx.subj = dummy_;

                /* we found the parameter - get the translation */
                txt = t[2](ctx, params);

                /*
                 *   if this isn't a pre-scan, and the parameter name starts
                 *   with a capital, mimic that pattern in the result
                 */
                if (txt != nil && !ctx.prescan && rexMatch(R'<upper>', pname) != nil)
                    txt = txt.firstChar().toUpper() + txt.delFirst();
            }

            /*
             *   if we failed to find an expansion, and this isn't just a
             *   pre-scan, return the source text as an error indication
             */
            if (txt == nil && !ctx.prescan)
                txt = '{' + params.join(' ') + '}';

            /* return the result */
            return txt;
        }

        /*
         *   Parameter mapping list.  This is a list of lists: [name, func],
         *   where 'name' is the parameter name (as a string), and 'func' is
         *   the expansion handler function.
         *
         *   The parameter name must be all lower case.  During expansion, we
         *   convert the first space-delimited token within the {curly brace}
         *   parameter string to lower case, then look for an entry in the list
         *   with the matching parameter name.  If we find an entry, we call
         *   its handler function.
         *
         *   The handler function is a pointer to a function that takes two
         *   arguments: func(params, ctx), where 'params' is the list of tokens
         *   within the {curly braces} of the substitution string, as a list of
         *   strings, where each string is a space-delimited token in the
         *   original {curly brace} string; and 'ctx' is the MessageCtx object
         *   for the expansion.  The function returns a string giving the
         *   expansion of the parameter.
         *
         *   The parameter list must be provided by the language module, since
         *   each language's list of parameters and expansions will vary.
         */
        params = [ ]

        /*
         *   Some parameters expand to properties of objects involved in the
         *   command.  cmdInfo() makes it easier to define the expansion
         *   functions for such parameters.  We search the parameters for a
         *   Command object, and if we find it, we retrieve a particular source
         *   object and evaluate a particular property on the source object to
         *   get the result string.
         *
         *   For example, {the dobj} could be handled via cmdInfo('ctx, dobj',
         *   &theName, vSubject): we find the current 'dobj' object in the
         *   Command, then evaluate the &theName property on that object.
         *
         *   'ctx' is the MessageCtx object with the current sentence context.
         *
         *   'src' is the source object in the Command.  This can be given as a
         *   property pointer (&actor, say), in which case we simply evaluate
         *   that property of the Command object (cmd.(src)) to get the source
         *   object.  Or, it can be a string giving a NounRole name (dobj,
         *   iobj, acc), in which case we'll retrieve the current object for
         *   the noun role from the Command.  Or, it can be a string with a
         *   number, in which case we'll use the number as an index into the
         *   argument list.
         *
         *   'objProp' is the property of the source object to evaluate to get
         *   the expansion string.
         *
         *   'role' is vSubject if this is a noun phrase with subject usage (in
         *   most languages, this is a noun phrase in the nominative case; in
         *   English this is called subjective case).  It's vObject for any
         *   other noun phrase role (direct object, prepositional object, etc).
         *   If the role isn't clear from the context (the case marking of the
         *   parameter, or the position), use vAmbig to mark it as ambiguous.
         */
        cmdInfo(ctx, src, objProp, role)
        {
            try
            {
                /* we don't have a source object yet */
                local srcObj = nil;

                /* if the source is a role name, get the corresponding property */
                if (dataType(src) == TypeSString)
                {
                    /* check for a number */
                    if (rexMatch(R'<digit>+', src) != nil)
                    {
                        /* it's an argument index */
                        src = ctx.args[toInteger(src)];
                    }
                    else
                    {
                        /* Find the source object corresponding to the string */
                        src = findStrParam(src, role);

                    /*
                     *   If we didn't find an object, return nil to the caller to
                     *   indicate an error
                     */

                        if(src == nil)
                            return nil;

                    }

                }

                /* retrieve the source object from the command */
                if (dataType(src) == TypeProp)
                    srcObj = ctx.cmd.(src);
                else if (dataType(src) == TypeObject
                         && (src.ofKind(Mentionable) || src.ofKind(Pronoun)
                             || src.ofKind(LiteralObject) ||
                             src.ofKind(ResolvedTopic)))
                    srcObj = src;

                /* check for reflexivity */
                if (srcObj != nil && role == vObject && !ctx.prescan)
                {
                    local r = cmdInfoReflexive(ctx, srcObj, objProp);
                    if (r != nil)
                        return r;
                }

                /* note the object's role in the sentence context */
                if (srcObj != nil)
                    ctx.noteObj(srcObj, role);

                /*
                 *   if we're in pre-scan mode, skip the actual expansion;
                 *   otherwise evaluate the target property on the source
                 *   object to get the expansion text
                 */
                return ctx.prescan ? nil : srcObj.(objProp);
            }
            catch (Exception e)
            {
                /*
                 *   if anything went wrong, return nil to indicate we failed
                 *   to find an expansion
                 */
                return nil;
            }
        }

        findStrParam(src, role)
        {
            local targetObj;

            if (gAction != nil)
            {
                /* get the target object by name through the action */
                targetObj = gAction.getMessageParam(src);
            }
            else
            {
                /* there's no action, so we don't have a value yet */
                targetObj = nil;
            }

            if (targetObj == nil)
            {
                /* look up the name */
                targetObj = libGlobal.nameTable_[src];

                /*
                 *   if we found it, and the result is a function pointer or an
                 *   anonymous function, invoke the function to get the result
                 */
                if (dataTypeXlat(targetObj) == TypeFuncPtr)
                {
                    /* evaluate the function */
                    targetObj = (targetObj)();
                }
            }

            /*
             *   If we still haven't found a targetObj, try getting it from the
             *   role's object property.
             */

            if (targetObj == nil && role != nil)
            {
                /* it's a role name - look up the role */
                local role = NounRole.all.valWhich({ r: r.name == src });

                /* get the role's object property */
                if(role != nil)
                   targetObj = role.objProp;
            }

            /*
             *   If we still haven't found a targetObj, there's probably an error in
             *   the way the object parameter was specified, but rather than
             *   allowing this to cause a run-time error down the track, substitute
             *   a dummy object and note the offending src parameter on an
             *   appropiate property so that the text presented should make it clear
             *   where the problem lies.
             */

            if(targetObj == nil)
            {
                targetObj = dummy_;
                dummy_.noteName('[' + src + ']');
            }

            return targetObj;
        }

        /*
         *   Parameter lookup table.  This maps a parameter name to its handler
         *   function.
         */
        paramTab = nil

        /* the language module's instance of the class */
        langObj = nil

        /* construction - build the lookup table */
        construct()
        {
            /* remember the instance */
            MessageParams.langObj = self;

            /* create the parameter table */
            paramTab = new LookupTable(64, 128);
            foreach (local p in params)
                paramTab[p[1]] = p;
        }
    ;

    /* Dummy object to use as a fallback when a parameter can't be identified */

    dummy_: Thing
        noteName(src) { }
    ;

    pluralDummy_: Thing
        noteName(src) { }
    ;

    /*----------------------------------------------------------------------------*/

Adv3Lite Library Reference Manual
Generated on 25/04/2024 from adv3Lite version 2.0