# query.t | documentation

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

    /*
     *   ***************************************************************************
     *   query.t
     *
     *   This module forms part of the adv3Lite library (c) 2012-13 Eric Eve
     *
     *   Based substantially on the query.t module in the Mercury Library (c) 2012
     *   Michael J. Roberts.
     */

    /* ------------------------------------------------------------------------ */
    /*
     *   Q is the general-purpose global Query object.  Its various methods are
     *   used to ask questions about the game state.
     *
     *   For any query, there are two sources of answers.  First, there's the
     *   standard answer based on the basic "physics" of the adventure world
     *   model.  Second, there are any number of custom answers from Special
     *   objects, which define customizations that apply to specific
     *   combinations of actors, locations, objects, times, or just about
     *   anything else that the game can model.
     *
     *   The standard physics-based answer is the default.  It provides the
     *   answer if there are no active Special objects that provide custom
     *   answers.
     *
     *   If there are active Specials, the only ones that matter for a
     *   particular query are the ones that define that query's method.  If
     *   there are any active Special objects that define a query method,
     *   calling Q.foo() actually calls the highest-priority Special's version
     *   of the foo() method.  That Special method can in turn call the next
     *   lower priority Special using next().  If there are no active Special
     *   objects defining a query method, the default handler in QDefaults will
     *   be used automatically.
     */

    Q: object
        /*
         *   Get the list of objects that are in scope for the given actor.
         *   Returns a ScopeList object containing the scope.  You can convert
         *   the ScopeList to an ordinary list of objects via toList().
         */
        scopeList(actor)
            { return Special.first(&scopeList).scopeList(actor); }

        knownScopeList()
            { return Special.first(&knownScopeList).knownScopeList;}

        topicScopeList()
            { return Special.first(&topicScopeList).topicScopeList;}

        /*
         *   Is A in the light?  This determines if there's light shining on
         *   the exterior surface of A.
         */
        inLight(a)
            { return Special.first(&inLight).inLight(a); }

        /*
         *   Is B in gthe light from the perspective of A, where A may be either inside or outside B?
         */
        inLightFor(a, b)
            { return Special.first(&inLightFor).inLightFor(a); }

        /*
         *   Can A see B?
         */
        canSee(a, b)
            { return Special.first(&canSee).canSee(a, b); }

        /*
         *   Determine if there's anything blocking the sight path from A to B.
         *   Returns a list of objects blocking sight; if there's no
         *   obstruction, returns an empty list.  If the two objects are in
         *   separate rooms, the outermost room containing 'a' represents the
         *   room separation.  If there's no obstruction, returns an empty
         *   list.
         */
        sightBlocker(a, b)
            { return Special.first(&sightBlocker).sightBlocker(a, b); }

        /*
         *   Can we reach from A to B?  We return true if there's nothing in
         *   the way, nil otherwise.
         */
        canReach(a, b)
            { return Special.first(&canReach).canReach(a, b); }

        /*
         *   Determine if A can reach B, and if not, what stands in the way.
         *   Returns a list of containers along the path between A and B that
         *   obstruct the reach.  If the two objects are in separate rooms, the
         *   top-level room containing A is in the list to represent the room
         *   separation.  If there's no obstruction, we return an empty list.
         */
        reachBlocker(a, b)
            { return Special.first(&reachBlocker).reachBlocker(a, b); }

        /*
         *   Determine if there is a problem with A reaching B, and if so what it
         *   is. If there is a problem return a ReachProblem object describing what
         *   the problem is, otherwise return nil.
         */
        reachProblem(a, b)
           { return Special.first(&reachProblem).reachProblem(a, b); }

        reachProblemVerify(a, b)
           { return Special.first(&reachProblemVerify).reachProblemVerify(a, b); }

        reachProblemCheck(a, b)
           { return Special.first(&reachProblemCheck).reachProblemCheck(a, b); }

        /*
         *   Can A hear B?
         */
        canHear(a, b)
            { return Special.first(&canHear).canHear(a, b); }

        /*
         *   Determine if A can hear B, and if not, what stands in the way.  We
         *   return a list of the obstructions to sound between A and B.  If
         *   the two objects are in separate rooms, the top level room
         *   containing A represents the room separation.  If there are no
         *   sound obstructions, returns an empty list.
         */
        soundBlocker(a, b)
            { return Special.first(&soundBlocker).soundBlocker(a, b); }

        /*
         *   Can A smell B?
         */
        canSmell(a, b)
            { return Special.first(&canSmell).canSmell(a, b); }

        /*
         *   Determine if A can smell B, and if not, what stands in the way.
         *   Returns a list of obstructions to scent between A and B.  If the
         *   two objects are in separate rooms, the outermost room containing A
         *   represents the room separation.  If there are no obstructions,
         *   returns an empty list.
         */
        scentBlocker(a, b)
            { return Special.first(&scentBlocker).scentBlocker(a, b); }

        /*  Determine if A can talk to B. */

        canTalkTo(a, b)
            { return Special.first(&canTalkTo).canTalkTo(a, b); }

        /*  Determine if A can Throw something to B. */
        canThrowTo(a, b)
            { return Special.first(&canThrowTo).canThrowTo(a, b); }

        /*
         *   Are the active conditions on Specials dynamic (i.e. such as to change
         *   during the course of the game)? By default we'll assume that some of
         *   them may be.
         */
        dynamicSpecials = true
    ;

    /*
     *   Query Defaults.  This provides the default handlers for all query
     *   methods.  These are the results that you get using the basic adventure
     *   game "physics" model to answer the questions, ignoring any special
     *   exceptions defined by the game.
     *
     *   This is the lowest-ranking Special object, and is always active.
     */
    QDefaults: Special
        /* this is the defaults object, so it has the lower priority */
        priority = 0

        /* this is the defaults object, so it's always active */
        active = true

        /*
         *   Get the list of objects that are in scope for the given actor.
         *   Returns a ScopeList object containing the scope.  You can convert
         *   the ScopeList to an ordinary list of objects via toList().
         */
        scopeList(actor)
        {
            /* start a new scope list */
            local s = new ScopeList();

            /* everything the actor is directly holding is in scope */
            s.addAll(actor.directlyHeld);

            local c = actor.outermostVisibleParent();

            /*
             *   If we're in a lighted area, add the actor's outermost visible
             *   container and its contents.  In the dark, add the actor's
             *   immediate container only (not its contents), on the assumption
             *   that the actor is in physical contact with it and thus can
             *   refer to it and manipulate it even without seeing it.
             */
            if (inLight(actor))
            {
                /* lit area - add the outermost container and its contents */

                s.addOnly(c);
                s.addWithin(c);
            }
            else
            {
                /* in the dark - add only the immediate container */
                s.addOnly(actor.location);

                /* plus anything that's self illuminating */
                s.addSelfIlluminatingWithin(c);
            }

            /* close the scope */
            s.close();

            /* return the ScopeList we've built */
            return s;
        }

        /* Get a list of all Things that are known to the player char */

        knownScopeList()
        {
            local vec = new Vector(30);
            for(local obj = firstObj(Thing); obj != nil; obj = nextObj(obj, Thing))
            {
                if(obj.known)
                    vec += obj;
            }

            return vec.toList;
        }

        /*
         *   Get a list of all known mentionable objects, which we assume will
         *   include both known Things and known Topics
         */
        topicScopeList()
        {
            return World.universalScope.subset({o: o.known});
        }
        /*
         *   Is A in the light?  This determines if there's light shining on
         *   the exterior surface of A.
         */
        inLight(a)
        {
            /* A is lit if it's a Room and it's illuminated */
            if(a.ofKind(Room))
                return a.isIlluminated;

            /* Otherwise a may be lit if it's visible in the dark */
            if(a.visibleInDark)
                return true;

            /* A is lit if its enclosing parent is lit within */
            local par = a.interiorParent();
            return par != nil && par.litWithin();
        }

        /* is B lit from the perspect of A, who may be inside B. */
        inLightFor(a, b)
        {
            /*
             *   The special case is where A wishes to view B and B is A's outermostVisibleParent, such
             *   as a closed, opaque booth. In that case B is in light if it's lit from within.
             */
            if(b == a.outermostVisibleParent())
                return b.litWithin();
            /* Otherwise b is lit for if b is in light. */
            else
                return inLight(b);
        }

        /*
         *   Can A see B?  We return true if and only if B is in light and there's a
         *   clear sight path from A to B. Also A can't see B is B is explicitly
         *   hidden.
         */
        canSee(a, b)
        {

            if(a.isIn(nil) || b.isIn(nil) || b.isHidden)
                return nil;

            /*
             *   we can see it if it's in light and we're outside it or it's our enclosing container and
             *   it's lit within, and there's a clear path to it
             */
            return inLightFor(a, b) && sightBlocker(a, b).indexWhich({x: x not in (a, b)}) ==  nil;

        }

        /*
         *   Determine if there's anything blocking the sight path from A to B.
         *   Returns a list of objects blocking sight; if there's no
         *   obstruction, returns an empty list.  If the two objects are in
         *   separate rooms, the outermost room containing 'a' represents the
         *   room separation.  If there's no obstruction, returns an empty
         *   list.
         */
        sightBlocker(a, b)
        {
            /* scan for sight blockages along the containment path */
            return a.containerPathBlock(b, &canSeeOut, &canSeeIn);
        }

        /*
         *   Can we reach from A to B?  We return true if there's a clear reach path
         *   from A to B, which we take to be the case if we can't find any problems
         *   in reaching from A to B.
         */
        canReach(a, b)
        {
            return Q.reachProblem(a, b) == [];
        }

        /*
         *   Determine if there is anything preventing or hindering A from reaching
         *   B; if so return a ReachProblem object describing the problem in a way
         *   that a check or verify routine can act on (possibly with an implicit
         *   action to remove the problem). If not, return an empty list.
         *
         *   NOTE: if you provide your own version of this method on a Special it
         *   must return either an empty list (to indicate that there are no
         *   problems with reaching from A to B) or a list of one or more
         *   ReachProblem objects describing what is preventing A from reaching B.
         *
         *   Your own Special should normally leave reachProblem() alone and
         *   override reachProblemVerify() and/or reachProblemCheck().
         */

        reachProblem(a, b)
        {
            /*
             *   A list of issues that might prevent reaching from A to B. If we
             *   encounter a fatal one we return the list straight away rather than
             *   carrying out more checks.
             */
            local issues = Q.reachProblemVerify(a, b);
            if(issues.length > 0)
                return issues;

            return Q.reachProblemCheck(a, b);

        }

        /* Return a list of reach issues that might occur at the verify stage. */
        reachProblemVerify(a, b)
        {
            /*
             *   A list of issues that might prevent reaching from A to B. If we
             *   encounter a fatal one we return the list straight away rather than
             *   carrying out more checks.
             */
            local issues = [];

            if(a.isIn(nil) || b.isIn(nil))
            {
                issues += new ReachProblemDistance(a, b);
                return issues;
            }

            local lst = Q.reachBlocker(a, b);

             /*
              *   If there's a blocking object but the blocking object is the one
              *   we're trying to reach, then presumably we can reach it after all
              *   (e.g. an actor inside a closed box. Otherwise if there's a
              *   blocking object then reach is impossible.
              */

            if(lst.length > 0 && lst[1] != b)
            {

                /*
                 *   If the blocking object is a room, then the problem is that the
                 *   other object is too far away.
                 */
                if(lst[1].ofKind(Room))
                    issues += new ReachProblemDistance(a, b);
                /* Otherwise some enclosing object is in the way */
                else
                    issues += new ReachProblemBlocker(b, lst[1]);

                return issues;
            }

            /*
             *   If a defines a verifyReach method, check whether running it adds a
             *   new verify result to the current action's verifyTab table. If so,
             *   there's a problem with reaching so return a ReachProblemVerifyReach
             *   object.
             */

            if(b.propDefined(&verifyReach))
            {
                local tabCount = gAction.verifyTab.getEntryCount();
                b.verifyReach(a);
                if(gAction.verifyTab.getEntryCount > tabCount)
                   issues += new ReachProblemVerifyReach(a, b);
            }

            /* Return our list of issues. */
            return issues;
        }

        reachProblemCheck(a, b)
        {
            /*
             *   A list of issues that might prevent reaching from A to B. If we
             *   encounter a fatal one we return the list straight away rather than
             *   carrying out more checks.
             */
            local issues = [];

            /*
             *   Set up a local variable to hold the check message that may result from running the
             *   appropriatte checkReach() method.
             */
            local checkMsg = nil;

            /* Store a list of the objects (if any) that are blocking a from reaching b. */
            local lst = Q.reachBlocker(a, b);

            /* Set up a local variable that will be used to hold various locations. */
            local loc;

            /* Note a's starting location. */
            local startLoc = a.location;

            try
            {
                /*
                 *   Determine whether there's any problem with b reached from a
                 *   defined in b's checkReach (from a) method.
                 */
                checkMsg = gOutStream.captureOutputIgnoreExit({: b.checkReach(a)});

                /*
                 *   If the checkReach method generates a non-empty string, add a
                 *   new ReachProblemCheckReach object that encapsulates it
                 */
                if(checkMsg not in (nil, ''))
                    issues += new ReachProblemCheckReach(b, checkMsg);

                /*
                 *   Next check whether there's any problem reaching inside B from A
                 *   defined in B's checkReachIn method or the checkReach in method
                 *   of anything that contains B.
                 */
                local cpar = b.commonContainingParent(a);

                if(cpar != nil)
                {
                    for(loc = b.location; loc != cpar; loc = loc.location)
                    {
                        checkMsg = gOutStream.captureOutputIgnoreExit(
                            {: loc.checkReachIn(a, b)});
                        if(checkMsg not in (nil, ''))
                            issues += new ReachProblemCheckReach(b, checkMsg);
                    }

                }
            }
            /*
             *   Game authors aren't meant to use the exit macro in check methods,
             *   but in case they do we handle it here.
             */
            catch (ExitSignal ex)
            {
                /*
                 *   If for some reason a check method uses exit without displaying
                 *   a method, we supply a dummy failure message at this point.
                 */

                if(checkMsg is in (nil, ''))
                    checkMsg = gAction.failCheckMsg;

                issues += new ReachProblemCheckReach(b, checkMsg);
            }

            /*
             *   If running checkReach() methods has resulted in any issues, stop here and return a list
             *   of the issues.
             */
            if(issues.length > 0)
                return issues;

            /*
             *   Next check whether the actor is in a nested room that does not
             *   contain the object being reached, and if said nested room does not
             *   allow reaching out.
             */

            loc = a.location;
            if(loc != a.getOutermostRoom && !b.isOrIsIn(loc))
            {
                /*
                 *   If we don't allow reaching out of a's location to touch b, then add a new
                 *   ReachProblemReachOut to our list of problems, so that when its check method is run
                 *   we can attempt to take the actor (a) out their location.
                 */
                if(!loc.allowReachOut(b))
                    issues += new ReachProblemReachOut(b);

                /*
                 *   If the common containing parent of a and b is the location of b, then having left
                 *   the location of a we'll have got where we need to go, so we can just return the
                 *   list of issuess we've accumulated so far.
                 */
                loc = a.commonContainingParent(b);
                if(b.location == loc)
                    return issues;

                /*
                 *   If we reach here, a should be moved to the common containaing parent, which should
                 *   then be the starting location for moving inwards to the location of b.
                 */
                startLoc = loc;

            }

            /*
             *   Next check whether the actor is  not in a nested room that contains
             *   the object the actor is attempting to reach, and if said nested room does not
             *   allow reaching in.
             */
            if(b.location.isIn(startLoc)) // change a.location to startLoc
            {
                /*
                 *   Set up a list to contain the list of nested rooms the actor needs to enter to reach
                 *   the target object, starting at the outermost and progressing inwards.
                 */
                local locList = [];

                /* Note the location of the target object we're trying to reach. */
                loc = b.location;

                /*
                 *   Loop outwards from the target object's location to the actor's location to
                 *   construct the list of locations the actor needs to pass through to reach the
                 *   target.
                 */
                while(loc != startLoc && loc != a.getOutermostRoom)
                {
                   /*
                    *   prepend this location to our list so that our list will end up working from
                    *   outermost to innermost.
                    */
                    locList = locList.prepend(loc);

                    /*
                     *   Then set loc to the next location working outwards towards the actor's
                     *   location.
                     */
                    loc = loc.location;
                }

                /*
                 *   Loop through the locations in the list we've just constructed working inwards trom
                 *   the actor's location.
                 */
                foreach(loc in locList)
                {
                    /*
                     *   If loc is not the actor's location and it doesn't allow reaching in and it's a
                     *   nested room (enterable or boardable) then create a new ReachProblemReachIn
                     *   object corresponding to loc to add to our list of issues.
                     */
                    if(loc != startLoc && (loc.isEnterable || loc.isBoardable))
                    {
                        if(!loc.allowReachIn(b))
                            issues += new ReachProblemReachIn(loc, b);

                    }
                    else if(b.isIn(loc) && lst.indexOf(loc))
                        issues += new ReachProblemBlocker(b, loc);
                }
            }

            /* Return our list of issues */
            return issues;

        }

        /*
         *   Determine if A can reach B, and if not, what stands in the way. Returns
         *   a list of containers along the path between A and B that obstruct the
         *   reach.  If the two objects are in separate rooms, the top-level room
         *   containing A is in the list to represent the room separation.  If
         *   there's no obstruction, we return an empty list.
         */
        reachBlocker(a, b)
        {
            return a.containerPathBlock(b, &canReachOut, &canReachIn);
        }

        /*
         *   Can A hear B?  We return true if there's a clear sound path from A to
         *   B.
         */
        canHear(a, b)
        {
            if(a.isIn(nil) || b.isIn(nil))
                return nil;

            return soundBlocker(a, b).indexWhich({x: x not in (a, b)}) ==  nil;
        }

        /*
         *   Determine if A can hear B, and if not, what stands in the way.  We
         *   return a list of the obstructions to sound between A and B.  If
         *   the two objects are in separate rooms, the top level room
         *   containing A represents the room separation.  If there are no
         *   sound obstructions, returns an empty list.
         */
        soundBlocker(a, b)
        {
            return a.containerPathBlock(b, &canHearOut, &canHearIn);
        }

        /*
         *   Can A smell B?  We return true if there's a clear scent path from
         *   A to B.
         */
        canSmell(a, b)
        {
            if(a.isIn(nil) || b.isIn(nil))
                return nil;

            return scentBlocker(a, b).indexWhich({x: x not in (a, b)}) ==  nil;
        }

        /*
         *   Determine if A can smell B, and if not, what stands in the way.
         *   Returns a list of obstructions to scent between A and B.  If the
         *   two objects are in separate rooms, the outermost room containing A
         *   represents the room separation.  If there are no obstructions,
         *   returns an empty list.
         */
        scentBlocker(a, b)
        {
            return a.containerPathBlock(b, &canSmellOut, &canSmellIn);
        }

        /*
         *   Determine if A can talk to B. In the base situation A can talk to B if
         *   A can hear B.
         */
        canTalkTo(a, b)
        {
            return Q.canHear(a, b);
        }

        /*
         *   Determine if A can throw something to B. In the base situation A can
         *   throw to B if A can reach B.
         *
         */
        canThrowTo(a, b)
        {
            return canReach(a, b);
        }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A Special defines a set of custom overrides to standard Query
     *   questions that apply under specific conditions.
     *
     *   At any given time, a Special is either active or inactive.  This is
     *   determined by the active() method.
     */
    class Special: object
        /*
         *   Am I active?  Each instance should override this to define the
         *   conditions that activate the Special.
         */
        active = nil

        /*
         *   My priority.  This is an integer value that determines which
         *   Special takes precedence when two or more Specials are active at
         *   the same time, and they both/all define a given query method.  In
         *   such a situation, Q calls the active Specials in ascending
         *   priority order (lowest first, highest last), and takes the last
         *   one's answer as the true answer to the question.  This means that
         *   the Special with the highest priority takes precedence, and can
         *   override any lower-ranking Special that's active at the same time.
         *
         *   The library uses the following special priority values:
         *
         *   0 = the basic library defaults.  The defaults must have the lowest
         *   priority, meaning that all Special objects defined by a game or
         *   extension must use priorities higher than 0.
         *
         *   Other than the special priorities listed above, the priority is
         *   simply a relative ordering, so games and extensions can use
         *   whatever range of values they like.
         *
         *   Note that priorities can't change while running.  This is a
         *   permanent feature of the object.  We take advantage of this to
         *   avoid re-sorting the active list every time we build it.  We sort
         *   the master list at initialization and assume it stays sorted, so
         *   that any subset is inherently sorted.  If it's important to the
         *   game to dynamically change priorities, you just need to re-sort
         *   the allActive_ list at appropriate times.  If priorities can only
         *   change when the game-world state changes, you can simply sort the
         *   list in allActive() each time it's rebuilt.  If priorities can
         *   change at other times (which doesn't seem like it'd be useful, but
         *   just in case), you'd need to re-sort the list on every call to
         *   allActive(), even when the list isn't rebuilt.
         */
        priority = 1

        /*
         *   Call the same method in the next lower priority Special.  This can
         *   be used in any Special query method to invoke the "default"
         *   version that would have been used if the current Special had not
         *   been active.
         *
         *   This is analogous to using 'inherited' to inherit the superclass
         *   version of a method from an overriding version in a subclass.  As
         *   with 'inherited', you can only call this directly from the method
         *   that you want to pass to the default handling, because this
         *   routine determines what to call based on the caller.
         */
        next()
        {
            /* get the caller's stack trace information */
            local stk = t3GetStackTrace(2);
            local prop = stk.prop_;

            /* find the 'self' object in the currently active Specials list */
            local slst = Special.allActive();
            local idx = slst.indexOf(stk.self_);

            /* get the next Special that defines the method */
            while (!slst[++idx].propDefined(prop)) ;

            /* call the query method in the next Special, returning the result */
            return slst[idx].(prop)(stk.argList_...);
        }

        /*
         *   Get the first active Special (the one with the highest priority)
         *   that defines the given method.  This is used by the Q query
         *   methods to invoke the correct current Special version of the
         *   method.
         */
        first(prop)
        {
            /*
             *   If the active conditions on one or more Specials may change during
             *   the course of the game, invalidate the list of active Specials to
             *   force it to be rebuilt.
             */
            if(Q.dynamicSpecials)
                Special.allActive_ = nil;

            /* get the active Specials */
            local slst = Special.allActive();

            /* find the first definer of the method */
            local idx = 0;
            while (!slst[++idx].propDefined(prop)) ;

            /* return the one we found */
            return slst[idx];
        }

        /* Class method: get the list of active Specials. */
        allActive()
        {
            local a;

            /* if the cache is empty, rebuild it */
            if ((a = allActive_) == nil)
                a = allActive_ = all.subset({ s: s.active() });

            /* return the list */
            return a;
        }

        /*
         *   Class property: cache of all currently active Specials.  This is
         *   set whenever someone asks for the list and it's not available, and
         *   is cleared whenever an Effect modifies the game state.  (Callers
         *   shouldn't access this directly - this is an internal cache.  Use
         *   the allActive() method instead.)
         */
        allActive_ = nil

        /* during initialization, build the list of all Specials */
        classInit()
        {
            /* build the list of all Specials */
            local v = new Vector(128);
            forEachInstance(Special, { s: v.append(s) });

            /*
             *   Sort it in ascending priority order.  Since we assume that
             *   priorities are fixed, this eliminates the need to sort when
             *   creating active subsets - the subsets will automatically come
             *   up in priority order because they're taken from a list that
             *   starts in priority order.
             */
            v.sort(SortDesc, { a, b: a.priority - b.priority });

            /* save it as a list */
            all = v.toList();
        }

        /*
         *   Class property: the list of all Special objects throughout the
         *   game.  This is set up during preinit.
         */
        all = []
    ;

    /*------------------------------------------------------------------------- */
    /*
     *   A commLink is a Special that establishes a communications link between the
     *   player character and one or more actors in remote locations.
     *
     *   To activate the commLink with another actor, call
     *   commLink.connectTo(other). To make it a video link as well as an audio
     *   link, call commLink.connectTo(other, true).
     *
     *   To disconnect the call with a specific actor,  call
     *   commLink.disconnectFrom(other); to terminate the commLink with all actors,
     *   call commLink.disconnect()
     *
     */
    commLink: Special

        /*
         *   Our scope list must include all the actors we're currently connected
         *   to.
         */
        scopeList(actor)
        {
            local s = next();

            s.vec_ += connectionList.mapAll({x: x[1]});

            s.vec_ = s.vec_.getUnique();

            return s;
        }

        /* We can hear an actor if s/he's in our connection list */
        canHear(a, b)
        {
            /*
             *   We assume that if a can hear b, b can hear a, but the link is only
             *   between the player character an another actor. If b is the player
             *   character swap a and b so that the tests that follow will still
             *   apply.
             */
            if(b == gPlayerChar)
            {
                b = a;
                a = gPlayerChar;
            }

            /*
             *   If one of the actors is the player character and the other is in
             *   our connection list, then they can hear each other.
             */
            if(a == gPlayerChar && isConnectedTo(b))
                return true;

            /* Otherwise use the next special. */
            return next();
        }

        canSee(a, b)
        {
            /*
             *   We assume that if a can see b, b can see a, but the link is only
             *   between the player character and another actor. If b is the player
             *   character swap a and b so that the tests that follow will still
             *   apply.
             */
            if(b == gPlayerChar)
            {
                b = a;
                a = gPlayerChar;
            }

            /*
             *   If one of the actors is the player character and the other is in
             *   our connection list with a video value of true, then they can see
             *   each other.
             */
            if(a == gPlayerChar && isConnectedTo(b) == VideoLink)
                return true;

            /* Otherwise use the next special. */
            return next();
        }

        canTalkTo(a, b)
        {
            /*
             *   We assume that if a can talk to b, b can talk to a, but the link is
             *   only between the player character and another actor. If b is the
             *   player character swap a and b so that the tests that follow will
             *   still apply.
             */
            if(b == gPlayerChar)
            {
                b = a;
                a = gPlayerChar;
            }

            /*
             *   If one of the actors is the player character and the other is in
             *   our connection list, then they can talk to each other.
             */
            if(a == gPlayerChar && isConnectedTo(b))
                return true;

            /* Otherwise use the next special. */
            return next();
        }

        /*
         *   The list of actors we're currently connected to. This is a list of two
         *   element lists in the form [actor, video], where actor is the actor
         *   we're connected to and video is true or nil according to whether the
         *   link to that actor is a video link as well as an audio link.
         */
        connectionList = []

        /* This Special is active is there's anything in its connectionList. */
        active = connectionList.length > 0

        /*
         *   Connect this comms link to other; if video is specified and is true,
         *   the comms links is also a video link.
         */
        connectTo(other, video = nil)
        {
            /*
             *   In case the video parameter is supplied as AudioLink or VideoLink
             *   (some game authors may try this even though it's not documented),
             *   we should first translate the video parameter into true or nil as
             *   appropriate.
             */
            if(video == AudioLink)
                video = nil;

            if(video == VideoLink)
                video = true;

            /* Add other to our connection list. */
            connectionList = connectionList.append([other, video]);

            /* Force the Special class to rebuild its list of active Specials. */
            Special.allActive_ = nil;
        }

        /* Disconnect this commLink from everyone */
        disconnect()
        {
            /* Empty our out connectionList */
            connectionList = [];

            /* Force the Special class to rebuild its list of active Specials. */
            Special.allActive_ = nil;
        }

        /*
         *   Disconnect this commLink from lst, where lst may be a single actor or a
         *   list of actors.
         */
        disconnectFrom(lst)
        {
            /* Convert the lst parameter to a list if it isn't one already */
            lst = valToList(lst);

            /*
             *   Reduce our connectionList to a subset of members that aren't in
             *   lst.
             */
            connectionList = connectionList.subset({x: lst.indexOf(x[1]) == nil});

            /* Force the Special class to rebuild its list of active Specials. */
            Special.allActive_ = nil;
        }

        /*
         *   Is there a communications link with obj? Return nil if there is none,
         *   AudioLink if there's an audio connection only and VideoLink if there's
         *   a video connection as well.
         */
        isConnectedTo(obj)
        {
            local conn = connectionList.valWhich({x: x[1] == obj});
            if(conn == nil)
                return nil;

            return conn[2] ? VideoLink : AudioLink;
        }

        /*
         *   Give this Special a higher priority that the QSenseRegion Special so
         *   that it takes precedence when it's active.
         */
        priority = 5
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A ScopeList is a helper object used to build the list of objects in
     *   scope.  This object provides methods for the common ways of adding
     *   objects to scope.
     *
     *   The ScopeList isn't a true Collection object, but it mimics one by
     *   providing most of the standard methods.  You can use length() and the
     *   [] operator to scan the list, perform a foreach or for..in loop with a
     *   ScopeList to iterate over the items in scope, you can use find() to
     *   check if a given object is in scope, and you can use subset() to get a
     *   list of in-scope objects satisfying some condition.
     */
    class ScopeList: object
        /*
         *   Add an object and its contents to the scope.
         */
        add(obj)
        {
            /*
             *   if we've already visited this object in full-contents mode,
             *   there's no need to repeat all that
             */
            local tstat = status_[obj];
            if (tstat == 2)
                return;

            /* if the object isn't already in the list at all, add it */
            if (tstat == nil)
                vec_.append(obj);

            /* promote it to status 2: added with contents */
            status_[obj] = 2;

            /*
             *   if we can see in, add all of the contents, interior and
             *   exterior; otherwise add just the exterior contents
             */
            if (obj.canSeeIn)
                addAll(obj.contents);
            else
                addAll(obj.extContents);
        }

        /*
         *   Add all of the objects in the given list
         */
        addAll(lst)
        {
            for (local i = 1, local len = lst.length() ; i <= len ; ++i)
                add(lst[i]);
        }

        /*
         *   Add the interior contents of an object to the scope.  This adds
         *   only the contents, not the object itself.
         */
        addWithin(obj)
        {
            /* add each object in the interior contents */
            addAll(obj.intContents);
        }

        /* add each self-illuminating object in the interior contents */
        addSelfIlluminatingWithin(obj)
        {
            addAll(obj.intContents.subset({x: x.visibleInDark}));
        }

        /*
         *   Add a single object to the scope.  This doesn't add anything
         *   related to the object (such as its contents) - just the object
         *   itself.
         */
        addOnly(obj)
        {
            /*
             *   If this object is already in the status table with any status,
             *   there's no need to add it again.  We also don't want to change
             *   its existing status, because if we've already added it with
             *   its contents, adding it redundantly by itself doesn't change
             *   the fact that we've added its contents.
             */
            if (status_[obj] != nil)
                return;

            /* add it to the vector */
            vec_.append(obj);

            /* set the status to 1: we've added only this object */
            status_[obj] = 1;
        }

        /* "close" the scope list - this converts the vector to a list */
        close()
        {
            vec_ = vec_.toList();
            status_ = nil;
        }

        /* get the number of items in scope */
        length() { return vec_.length(); }

        /* get an item from the list */
        operator[](idx) { return vec_[idx]; }

        /* is the given object in scope? */
        find(obj)
        {
            if(status_ == nil)
                status_ = new LookupTable(64, 128);

            return status_[obj] != nil;
        }

        /* get the subset of the objects in scope matching the given condition */
        subset(func) { return vec_.subset(func); }

        /* return the scope as a simple list of objects */
        toList() { return vec_; }

        /* create an iterator, for foreach() */
        createIterator() { return vec_.createIterator(); }

        /* create a live iterator */
        createLiveIterator() { return vec_.createLiveIterator(); }

        /* a vector with the objects in scope */
        vec_ = perInstance(new Vector(50))

        /*
         *   A LookupTable with the objects already added to the list.  We use
         *   this to avoid redundantly scanning containment trees for objects
         *   that we've already added.  For each object, we set status_[obj] to
         *   a status indicator:
         *
         *.    nil (unset) - the object has never been visited
         *.    1 - we've added the object only, not its contents
         *.    2 - we've added the object and its contents
         */
        status_ = perInstance(new LookupTable(64, 128))
    ;

    /*
     *   An object describing a reach problem; such objects are used by the Query
     *   object to communicate problems with one object touching another to the
     *   touchObj PreCondition (see also precond.t). ReachProblem objects are
     *   normally created dynamicallty as required, although it is usually one of
     *   the subclasses of ReachProblem that it used.
     */
    class ReachProblem: object
        /*
         *   Problems which reaching an object that occur at the verify stage and
         *   which might affect the choice of object. If the verify() method of a
         *   ReachProblem object wishes to rule out an action it should do so using
         *   illogical(), inaccessible() or other such verification macros.
         */
        verify() { }

        /*
         *   The check() method of a ReachProblem should check whether the target
         *   object can be reached by the source object. If allowImplicit is true
         *   the check method may attempt an implicit action to bring the target
         *   object within reach.
         *
         *   Return true if the target object is within reach, and nil otherwise.
         *
         *   Note that the check() method of a ReachProblem will normally be called
         *   from the checkPreCondition() method of touchObj.
         */
        check(allowImplicit) { return true; }

        construct(target)
        {
            target_ = target;
        }

        /* The object we're trying to reach */
        target_ = nil
    ;

    /*
     *   A ReachProblem object for when the target object is too far away (because
     *   it's in another room).
     */
    class ReachProblemDistance: ReachProblem
        verify()
        {
            inaccessible(tooFarAwayMsg);
        }

        tooFarAwayMsg()
        {
            return source_.getOutermostRoom.cannotReachTargetMsg(target_);
        }

        construct(source, target)
        {
            source_ = source;
            inherited(target);
        }

        /* The object that's trying to reach the target */
        source_ = nil;
    ;

    /*
     *   A ReachProblem object for when access to the target is blocked by a closed
     *   container along the path from the source to the target.
     */
    class ReachProblemBlocker: ReachProblem
        verify()
        {
            /*
             *   If the obstructor is closed but openable, there's a possibility that we could remove
             *   the obstruction by opening it, so we'll allow the action to go ahead but give it a
             *   sligjtly lower logical rank to nudge the parser towards choosing a more accessible
             *   object if one is available.
             */
            if(!obstructor_.isOpen && obstructor_.isOpenable)
                logicalRank(90);

            /*
             *   Otherwise just rule out that the action for attempting to reach an inaccessible object.
             */
            else
                inaccessible(reachBlockedMsg);
        }

        /*
         *   If we've passed the verify stage the obstructor must be closed and openable, so all we need
         *   to do here is to attampt to open the obstructor and see if that's resolved the problem with
         *   reaching.
         */
        check(allowImplicit)
        {
            /*
             *   If the obstructor is closed and we're allowing an implicit action then try opening the
             *   obstructor and return true or nil according to whether we can now reach in through the
             *   obstructor.
             */

            if(!obstructor_.isOpen && allowImplicit && tryImplicitAction(Open, obstructor_))
                return obstructor_.canReachIn;

            /*
             *   Otherwise disspay a message to say we can't reach in and then return nil to fail the
             *   action.
             */
            say(reachBlockedMsg);
            return nil;
        }

        /*
         *   Delegate defining the message explaining that blocking is reached to
         *   the blocking object.
         */
        reachBlockedMsg()
        {
            return obstructor_.reachBlockedMsg(target_);
        }

        /*
         *   The closed container that is preventing access to the target object the
         *   actor is trying to reach.
         */
        obstructor_ = nil

        construct(target, obstructor)
        {
            inherited(target);
            obstructor_ = obstructor;
        }
    ;

    /*
     *   A ReachProblem resulting from the verifyReach() method of the target
     *   object.
     */
    class ReachProblemVerifyReach: ReachProblem
        verify()
        {
            source_.verifyReach(target_);
        }

        construct(source, target)
        {
            inherited(target);
            source_ = source;
        }

        source_ = nil
    ;

    /*
     *   A ReachProblem resulting from reach being prohibited by the checkReach()
     *   method or checkReachIn() method of the target object or an object along the
     *   reach path.
     */
    class ReachProblemCheckReach: ReachProblem
        errMsg_ = nil

        construct(target, errMsg)
        {
            inherited(target);
            errMsg_ = errMsg;
        }

        check(allowImplicit)
        {
            say(errMsg_);
            return nil;
        }

    ;

    /*
     *   A ReachProblem object for when the actor can't reach the target from the
     *   actor's (non top-level room) container.
     */
    class ReachProblemReachOut: ReachProblem
        /*
         *   If allowImplicit is true we can try moving the actor out of its
         *   immediate container to see if this solves the problem. If it does,
         *   return true; otherwise return nil.
         */
        check(allowImplicit)
        {
            /* Note the actor's immediate location. */
            local loc = gActor.location;

            /* Note the target we're trying to reach. */
            local obj = target_;

            /*
             *   The action needed by the actor to leave the actor's immediate
             *   location (GetOff or GetOutOf, depending whether the actor is on a
             *   Platform or in a Booth).
             */
            local getOutAction;

            /*
             *   Keep trying to move the actor out of its immediate location until
             *   the actor is in its outermost room, or until we can't move the
             *   actor, or until the actor can reach the target object.
             */
            while(loc != gActor.getOutermostRoom && !obj.isOrIsIn(loc) &&
                  !loc.allowReachOut(obj))
            {
                /*
                 *   The action we need to move the actor out of its immediate
                 *   location will be GetOff or GetOutOf depending on the contType
                 *   of the actor's location.
                 */
                getOutAction = loc.contType == On ? GetOff : GetOutOf;

                /*
                 *   If we're allowed to attempt an implicit action, and the actor's
                 *   location is happy to allow the actor to leave it via an
                 *   implicit action for the purpose of reaching, then try an
                 *   implicit action to take the actor out of its location.
                 */
                if(allowImplicit && loc.autoGetOutToReach
                   && tryImplicitAction(getOutAction, loc))
                {
                    /*
                     *   If the actor is still in its original location, then we
                     *   were unable to move it out, so return nil to signal that
                     *   the reachability condition can't be met.
                     */
                    if(gActor.isIn(loc))
                        return nil;
                }
                /*
                 *   Otherwise, if we can't perform an implicit action to remove the
                 *   actor from its immediate location, display a message explaining
                 *   the problem and return nil to signal that the reachability
                 *   condition can't be met.
                 */
                else
                {
                    say(loc.cannotReachOutMsg(obj));
                    return nil;
                }

                /*
                 *   If we've reached this point we haven't failed either check in
                 *   this loop, so note that the actor's location is now the
                 *   location of its former location and then continue round the
                 *   loop.
                 */
                loc = loc.location;
            }

            /*
             *   If we've reached this point, we must have met the reachability
             *   condition, so return true.
             */
            return true;
        }
    ;

    /*
     *   A ReachProblem object that represents the need for the actor to be inside the blocking nested
     *   room (Platform or Booth) to reach the target object.
     */
    class ReachProblemReachIn: ReachProblem
        construct(block, targ)
        {
            /* Note the blocking nested room the actor needs to be able to get into on on. */
            blocker = block;

            /* Note the target object the actor is trying to reach. */
            target = targ;
        }

        /* The blocking nested room the actor needs to be able to get into or on. */
        target = nil

        /* The target object the actor is trying to reach. */
        blocker = nil

        check(allowImplicit)
        {
            /*
             *   first check that the target and/or blocker is directly or indiractly in the actor's
             *   location so that there is a possible path inwards.
             */
            if(!target.isIn(gActor.location))
            {
                say(reachBlockedMsg); //or say(loc.cannotReachInMsg(target, blocker));
                return nil;
            }

            /*
             *   If the blocking object is neither enterable nor boardable, the actor can't get in/on
             *   it, but if it's closed and openable  we can try opening it. We then return whether or
             *   not the actor can reach into the blocking object to see if that's resolved the issue.
             *   Otherwise we display a message saying access isn't possible and return nil to our
             *   caller to signal that we can't touch the target.
             */
            if(!blocker.isEnterable && !blocker.isBoardable)
            {
                if(!blocker.isOpen && blocker.isOpenable && allowImplicit
                   && tryImplicitAction(Open, blocker))
                    return blocker.canReachIn;

                say(reachBlockedMsg);
                return nil;

            }

            /* Note the actor's immediate location. */
            local loc = gActor.location;

            /*
             *   The action needed by the actor to enter the location we're trying to reach (Board or
             *   Enter, depending whether target is a Platform or a Booth).
             */
            local getInAction;

            /*
             *   Keep trying to move the actor into the target location until we reach it or
             *   fail to move the actor.
             */
            while(gActor.location != blocker)
            {
                /*
                 *   The action we need to move the actor out of its immediate
                 *   location will be GetOff or GetOutOf depending on the contType
                 *   of the actor's location.
                 */
                getInAction = blocker.contType == On ? Board : Enter;

                /*
                 *   If we're allowed to attempt an implicit action, and the actor's
                 *   target is happy to allow the actor to enter it via an
                 *   implicit action for the purpose of reaching, then try an
                 *   implicit action to take the actor out of its location.
                 */
                if(allowImplicit && blocker.autoGetInToReach
                   && tryImplicitAction(getInAction, blocker))
                {
                    /*
                     *   If the actor is still in its original location, then we
                     *   were unable to move it in, so return nil to signal that
                     *   the reachability condition can't be met.
                     */
                    if(gActor.location == loc)
                        return nil;
                }
                /*
                 *   Otherwise, if we can't perform an implicit action to remove the
                 *   actor from its immediate location, display a message explaining
                 *   the problem and return nil to signal that the reachability
                 *   condition can't be met.
                 */
                else
                {
                    say(loc.cannotReachInMsg(target, blocker));
                    return nil;
                }

                /*
                 *   If we've reached this point we haven't failed either check in this loop, so note
                 *   that the actor's new location and then continue round the loop.
                 */
                loc = gActor.location;
            }

            /*
             *   If we've reached this point, we must have met the reachability
             *   condition, so return true.
             */
            return true;
        }
    ;

Adv3Lite Library Reference Manual
Generated on 26/02/2025 from adv3Lite version 2.2