Project: ActiveNPCs: An interactive-NPC controller

Back to project

File name: Controller.lsl
Code: View Raw Code
integer channel = 68;
integer PEG_CHAN=699;
integer TIMER_INTERVAL=5; // how often to run the timer
integer autoLoadOnReset=0;
string LASTNAME="(NPC)";


// Nothing to edit here, see https://github.com/opensimworld/active-npcs for configuration

list availableNames = [];
list lastNames = [];
// These will be loaded from notecards
list wNodes = [];
list wLinks = [];
list wNodeNames=[];

//  list of nodes for the "Flyaround" command
list flyTargets = [];

list menuItems = ["SaveNPC", "LoadNPC", "RemoveNPC", "RemoveAll", "LoadAll", "ReConfig","InitCmds",  "DumpData", "TimerOnOff", "Close"];

string userInputState ="";
integer gListener;
integer zListener;
integer howmany;
list avis;
integer curVisitors=1;
integer deflectToNode = -1; // if set, the NPCs will only run notecards at the specified waypoint

list aviUids;
list aviNames;
list aviNodes;
list aviPrevNodes;
list aviStatus;
list aviFollow;
list aviCurrentAnim;
list aviPath;
list aviAlarm;
list aviScriptIndex;
list aviScriptText;
list aviHttpId;
list aviTarget; // user we are interacting with
list scriptVars;
list aviScriptState;
list aviPrompts;
list cache;


integer aviIndex = -1;
list seenArchive;

list positionsList;
list greetedAvis;
integer timerRuns;
integer timerRunning;
integer curPoint;
integer prevPoint;
list wayPoints;
list wayNames;
list wayLinks;
list wayKeys;
string name;
key npc;

list candidateNode=[];



string vec2str(vector v)
{
    return "<"+v.x+","+v.y+","+v.z+">";
}

string GetLastName(string first)
{
    integer idx = llListFindList(availableNames, [first]);
    if (idx >=0) return llList2String(lastNames, idx);
    else return LASTNAME;
}

key getAgentByName(string firstName)
{
        firstName = llToLower(firstName);
        list ag = osGetAvatarList();
        integer howmany = llGetListLength(ag);
        integer i;
        for (i =0; i < howmany; i+=3)
        {
            string name = llList2String(ag, i+2);
            integer sep = llSubStringIndex(name, " ");
            if (llToLower(llGetSubString(name, 0,sep-1)) == firstName)
            {
                return llList2Key(ag, i);
            }
        }
    return NULL_KEY;            
}

string GetScriptVar(string cmd3)
{
    integer i;
    for (i=0; i < llGetListLength(scriptVars); i+=2)
    {
        if (llList2String(scriptVars,i) == cmd3 )
        {
            return llList2String(scriptVars, i+1);
        }
    }
    return ""; // default value;
}


integer ScriptJump(integer idx, string label, integer complain)
{
    // Jump to a label in the notecard
    integer foundLine = FindScriptLineAfter(llList2String(aviScriptText,idx), "@"+label,-1);
    if (foundLine == -1)
    {
        if (complain)  llOwnerSay("Error: @"+label+" label not found");
        return 0;
    }
    else
    {
        aviScriptIndex  = []+llListReplaceList(aviScriptIndex, [foundLine+1], idx, idx);
        return 1;
    }
}

list permList;
string permCache;

LoadPerms()
{
    permList=[];
    permCache = "";
    if (llGetInventoryType("__permissions")==INVENTORY_NOTECARD)
    {
        llOwnerSay("Loading Permissions...");
        list lines = llParseString2List(osGetNotecard("__permissions"), ["\n"], []);
        integer l;
        for (l=0;l<llGetListLength(lines);l++)
        {
            list tk = llParseString2List(llList2String(lines,l), [" "], []);
            if (llList2String(tk,2) == "=")
            {
                string kw = llList2String(tk,3);
                string rule=llToLower(llList2String(tk,0)+" "+llList2String(tk,1));
                string n= llToLower(llStringTrim(llList2String(tk,4)+ " " + llList2String(tk,5), STRING_TRIM));
                if (n == "" ) n= "*";
                string val=kw+"|"+n+"|";
                permList+=rule;
                permList+=kw;
                permList+=n;
                if (llSubStringIndex(permCache,rule)<0) permCache+= rule;
            }
        }
        llOwnerSay(llList2CSV(permList));
        llOwnerSay(llList2CSV(permCache));
    }
}


integer IsAllowed(string npc, string cmd, key uid)
{
    if (uid == llGetOwner() || llGetListLength(permList)==0) return 1;
    
    string ss = "* *";
    string ns = npc+" *";
    string sc = "* "+cmd;    
    string nc = npc+" "+cmd;
    
    if (llSubStringIndex(permCache, ss)<0 &&  llSubStringIndex(permCache, ns)<0 &&  llSubStringIndex(permCache, sc)<0 &&    llSubStringIndex(permCache, nc)<0 ) return 1;

    integer allow=1;
    string name = llToLower(llKey2Name(uid));
    integer k;
    for (k=0; k< llGetListLength(permList); k+=3)
    {
        string rule = llList2String(permList,k);
        if (rule==ss || rule==ns || rule==sc || rule==nc)
        {
            //llOwnerSay("R="+rule);
            string r = llList2String(permList, k+1);
            if (r =="ALLOW" || r == "DENY")
            {
                string who=llList2String(permList, k+2);
                if (who == "*" || who==name)
                {
                    if (r == "ALLOW") allow=1;
                    else allow=0;
                }
            }
            else if (r=="ALLOWID")
            {
                if ( uid == llList2Key(permList, k+2)) allow=1;
            }
            else if (r=="DENYID")
            {
                if ( uid == llList2Key(permList, k+2)) allow=0;
            }
            else if (r =="ALLOWSAMEGROUP") 
            {
                if ( llSameGroup(uid)) { 
                    allow=1;
                }
            }
        }
    }
    return allow;
}



ReloadConfig()
{
    availableNames = [];
    lastNames = [];
    list tk = llParseString2List(osGetNotecard("__npc_names"), ["\n"] , []);
    integer i=0;
    for (i=0; i < llGetListLength(tk); i++)
    {
        list t2 = llParseString2List(llList2String(tk,i), [" "] , []);
        string f = llList2String(t2, 0);
        string l = llList2String(t2, 1);
        if (l =="") l = LASTNAME;
        if (f != "")
        {
            availableNames += f;
            lastNames += l;
        }
    }
    llOwnerSay("npc_names: "+llList2CSV(availableNames));
        
    flyTargets = llParseString2List(osGetNotecard("__fly_targets"), [" ", "\n", ""] , []);

    string ncName = "__config";
    autoLoadOnReset=0;

    if (llGetInventoryType(ncName) == INVENTORY_NOTECARD)
    {
        list tok = llParseString2List(osGetNotecard("__config"), ["=", "\n"] , []);
        integer j;
        for (j=0; j < llGetListLength(tok); j+=2)
        {
            string opt = llList2String(tok,j);
            if (opt== "AutoLoadOnReset") autoLoadOnReset = (integer)llList2String(tok, j+1);
            else if (opt== "LastName") LASTNAME = llList2String(tok, j+1);
        }
    }
    if (LASTNAME == "") LASTNAME = "(NPC)";
    
    LoadPerms();

}


integer countVisitors()
{
    list avis = llGetAgentList(AGENT_LIST_REGION, []);
    integer howmany = llGetListLength(avis);
    integer i;
    integer nNew =0;
    for ( i = 0; i < howmany; i++ ) {
        if ( ! osIsNpc(llList2Key(avis, i)) )
        {
            nNew++; // only non-NPC's
            key uu = llList2Key(avis, i);
            string nm = llKey2Name(uu);
            if (nm != "")
            {
                integer fnd = llListFindList(seenArchive, [nm]);
                if (fnd >=0)
                    seenArchive = [] + llListReplaceList(seenArchive, [nm, llGetUnixTime()], fnd, fnd+1);
                else 
                    seenArchive = [] + seenArchive  + [nm, llGetUnixTime()];
            }
        }
    }
    return nNew;
}

doLoadNPC(string first, string last)
{                 
        integer idx =(GetNPCIndex(first));
        if (idx >=0)
        {
            llOwnerSay(first+ " is already in region, not loading");
            osNpcStand(llList2Key(aviUids, idx));
            return;
        }

        key unpc = osNpcCreate(first, last, llGetPos()+<0,0,3>, "APP_"+llToLower(first), OS_NPC_SENSE_AS_AGENT );
        if (unpc != NULL_KEY)
            doAddNpc(first, unpc);
}


doAddNpc(string name, string unpc)
{

        llOwnerSay( "Adding '"+name+"'");
        aviUids += unpc;
        aviNames += llToLower(name);
        aviNodes += 1;
        aviPrevNodes += 0;
        aviStatus += "";
        aviFollow += "";
        aviCurrentAnim += "";
        aviHttpId += "";
        aviAlarm += -1;
        aviScriptIndex += -1;
        aviScriptText += "";
        aviTarget += NULL_KEY;
        aviPath  += "";
        aviScriptState += "";
        aviPrompts += "";
        osNpcMoveToTarget(unpc, osNpcGetPos(unpc) + <1,0,0>, OS_NPC_NO_FLY );
}

doRemoveNpc(string who)
{
        integer idx = GetNPCIndex(who);
        if (idx <0) return;
        
        key u = llList2Key(aviUids, idx);
        aviNames =  [] + llDeleteSubList(aviNames, idx, idx);
        aviUids =  [] + llDeleteSubList(aviUids, idx, idx);
        aviNodes =  [] + llDeleteSubList(aviNodes, idx, idx);
        aviPrevNodes = [] + llDeleteSubList(aviNodes, idx, idx);
        aviFollow = [] + llDeleteSubList(aviFollow, idx, idx); [];
        aviStatus = [] + llDeleteSubList(aviStatus, idx, idx);
        aviCurrentAnim = [] + llDeleteSubList(aviCurrentAnim, idx, idx);
        aviPath = [] + llDeleteSubList(aviPath, idx, idx);
        aviHttpId = [] + llDeleteSubList(aviHttpId, idx, idx);
        aviAlarm = [] + llDeleteSubList(aviAlarm, idx, idx);
        aviScriptIndex = [] + llDeleteSubList(aviScriptIndex, idx, idx);
        aviScriptText = [] + llDeleteSubList(aviScriptText, idx, idx);
        aviTarget = [] + llDeleteSubList(aviTarget, idx, idx);
        aviScriptState = [] + llDeleteSubList(aviScriptState, idx, idx);
        aviPrompts = [] + llDeleteSubList(aviPrompts, idx, idx);
       
        llOwnerSay("Removing "+who + "");
        osNpcStand(u);
        osNpcRemove(u);
}

doLoadAll()
{
            integer i;
            for (i=0; i < llGetListLength(availableNames);i++)
            {
                doLoadNPC(llList2String(availableNames, i),  llList2String(lastNames, i));
            }
    
}


doInitCmds()
{
    string notecard= "__initcommands";
    integer i;
    for(i=0; i<=osGetNumberOfNotecardLines(notecard); i++) {        
        string line = llStringTrim(osGetNotecardLine(notecard, i), STRING_TRIM);
        if (llStringLength(line)>0 && line != "")
        {
            list l = llParseString2List(line, [" "], []);
            line = "! "+(string)NULL_KEY+" "+llList2String(l,0)+" "+ line;
            llOwnerSay("InitCmd="+line);
            ProcessNPCCommand(line);
        }
    }

}

setVar(string cmd2, string cmd3)
{
            integer i;
            for (i=0; i < llGetListLength(scriptVars); i+=2)
            {
                if (llList2String(scriptVars,i) == cmd2)
                { 
                    scriptVars = [] + llListReplaceList(scriptVars, [cmd3], i+1, i+1);
                    return;
                }
            }
            scriptVars += cmd2;
            scriptVars += cmd3;
}


integer RescanAvis()
{
        avis = osGetAvatarList();
        howmany = llGetListLength(avis);
        integer i;
        for (i =0; i < howmany; i+=3)
        {
            if (osIsNpc(llList2Key(avis, i)))
            {
                integer sep = llSubStringIndex(llList2Key(avis, i+2), " ");
                string nm = llGetSubString(llList2Key(avis, i+2), 0, sep-1 );
                doAddNpc(nm,  llList2Key(avis, i));
            }
        }
        llOwnerSay(llList2CSV(aviNames));
        llOwnerSay(llList2CSV(aviStatus));
        return llGetListLength(aviUids);
}


LoadMapData()
{
    integer tl = osGetNumberOfNotecardLines("__waypoints");
    integer i;
    wNodes = [];
    for (i=0; i < tl; i++)
    {
        string line = osGetNotecardLine("__waypoints",i);
        list tok = llParseStringKeepNulls(line, [","], []);
        float x = llList2Float(tok,0);
        if (x>0)
        {
            vector v = <llList2Float(tok,0), llList2Float(tok,1),llList2Float(tok,2)>;
            wNodes += v;
            wNodeNames += llList2String(tok,3);
        }
    }
    llOwnerSay("loaded "+(string)(llGetListLength(wNodes))+" waypoints");

    
    integer tnodes = llGetListLength(wNodes);
    wLinks = [];
    tl = osGetNumberOfNotecardLines("__links");
    for (i=0; i < tl; i++)
    {
        string line = osGetNotecardLine("__links",i);
        list tok = llParseString2List(line, [","],"");
        integer a= llList2Integer(tok,0);
        integer b = llList2Integer(tok,1);
        if (a !=b)
            wLinks += [a,b];
    }
    llOwnerSay("loaded "+(string)(llGetListLength(wLinks)/2)+" links");
    cache = [];
}


integer GetNPCIndex(string name) /// name is in lowercase
{
    return llListFindList(aviNames, [llToLower(name)]);
}


integer GetWalkTime(float distance)
{
    return llCeil(distance / 1.7);
}

integer GetNearestNode(vector pos)
{
    integer i;
    float min = 9999991.;
    integer l =-1;
    for (i=0;i < llGetListLength(wNodes); i++)
    {
        float dist = llVecDist(pos, llList2Vector(wNodes,i));
        if (dist < min)
        {
            min = dist;
            l=i;
        }
    }
    return l;
}

list foundPaths;
// Get  path through LSL -- Slow
integer GenPaths(integer a, integer tgt, string path,  integer depth)
{
    if (depth > 17) 
    {
        //llOwnerSay("Bailing at " + path);
        return 0;
    }
    integer i;
    for (i=0; i < llGetListLength(wLinks); i+=2)
    {
        integer ca = llList2Integer(wLinks, i);
        integer cb = llList2Integer(wLinks, i+1);
        integer fn = -1;
        if (cb == a || ca == a)
        {
            if (cb == a)
                fn = ca;
            else if (ca == a)
                fn = cb;
            if (llSubStringIndex(path, ":"+fn+":")<0)
            {
                if (fn == tgt)
                {
                    path += ""+fn+":";
                    foundPaths += (path);
                    return 1;
                }
                else
                {
                  
                    GenPaths(fn, tgt, path+fn+":",   depth+1);
                }
            }
        }
        
        if (llGetListLength(foundPaths)>30)
            return 2;
    }
    return 0;
}





string GetGotoPath(integer nodeA, integer nodeB)
{
    integer i;
    integer ww;
 
    
    string tmpPath = ":"+(string)nodeA+":";
    
    foundPaths = [];
    GenPaths(nodeA, nodeB, tmpPath, 0);
    //llOwnerSay(llList2CSV(foundPaths));
    if (llGetListLength(foundPaths) ==0)
        return "";
    integer min = 99999;
    string least = "";
    for (i=0; i < llGetListLength(foundPaths); i++)
    {
        ww = llStringLength(llList2String(foundPaths, i));
        if (ww < min)
        {
            min = ww;
            least = llList2String(foundPaths, i);
        }
    }
    //llOwnerSay(least);
    return least;
}



integer GetNPCIndexByUid(key name)
{
    return llListFindList(aviUids, [name]);
}


string GetScriptLine(string scriptData, integer line)
{
    list scriptLines = llParseStringKeepNulls(scriptData, ["\n",";"], []);
    return llList2String(scriptLines, line-1);
}

integer FindScriptLineAfter(string scriptData, string lineToFind, integer afterLine)
{
    integer endIdx;
    list scriptLines = llParseStringKeepNulls(scriptData, ["\n",";"], []);
    string toFind = llToLower(lineToFind);
    integer foundLine = -1;
    string line;
    for  (endIdx = afterLine+1;endIdx < llGetListLength(scriptLines); endIdx++)
    {
        line = llList2String(scriptLines, endIdx);
        if (llStringTrim(line, STRING_TRIM) == toFind)
        {
            foundLine =endIdx;
            jump _foundIdxOut;
        }
    }
    @_foundIdxOut;
    return foundLine;
}


integer FindMatchingEndif(string scriptData, integer afterLine)
{
    integer endIdx;
    list scriptLines = llParseStringKeepNulls(scriptData, ["\n",";"], []);
    string toFind = "end-if";
    integer foundLine = -1;
    string line;
    integer ifLevel=1;
    for  (endIdx = afterLine+1;endIdx < llGetListLength(scriptLines); endIdx++)
    {
        line = llStringTrim(llList2String(scriptLines, endIdx), STRING_TRIM);
        if (llGetSubString(line, 0, 1) == "if")
        {
            ifLevel++;
        }
        else if (line == "end-if")
        {
            ifLevel--;
            if (ifLevel==0)
            {
                foundLine =endIdx;
                jump _foundEIIdxOut;
            }
        }
    }
    @_foundEIIdxOut;
    return foundLine;
}

integer GetNodeIndexByName(string nodeName)
{
    integer i;     
    nodeName = llToLower(nodeName);
    for (i=0; i < llGetListLength(wNodeNames); i++)
    {
        if (llToLower(llList2String(wNodeNames, i)) == nodeName)
        {
            return i;
        }
    }
    return -1;
}

SetScriptAlarm(integer aviId, integer time)
{
    aviAlarm = [] + llListReplaceList(aviAlarm, [llGetUnixTime() + time], aviId, aviId);
}


doStopNpc(integer idx, key uNPC)
{
    aviStatus = [] + llListReplaceList(aviStatus, [""], idx, idx);
    SetScriptAlarm(idx, 0);
    list anToStop=llGetAnimationList(uNPC);
    integer stop=llGetListLength(anToStop);
    while (--stop>=0) { osNpcStopAnimation(uNPC,llList2Key(anToStop,stop)); }
    osNpcStopMoveToTarget(uNPC);
    osNpcStand(uNPC);
}

// Handler for all commands coming from chat
integer ProcessNPCCommand(string inputString)
{

    list tokens = llParseString2List(inputString, [" "], []);
    // first token should be just "!"

    //llOwnerSay("<<"  + inputString);
    key sendUid = llList2Key(tokens,1);
    string npcName = llToLower(llList2String(tokens,2));
    string name2 = llToLower(llList2String(tokens,3));
    //if (npcName != name2)     npcName = name2;

    integer idx = GetNPCIndex(npcName);
    if (idx <0)
    {
        return 1;
    }

    key uNPC= llList2Key(aviUids, idx);
    if (uNPC == NULL_KEY)
    {
        return 1;
    }
    
    
    
    if (llSubStringIndex(inputString, "$")>=0) //substiute variables
    {
        integer i;
        for (i=4; i < llGetListLength(tokens); i++)
        {
            string st = llList2String(tokens,i);
            if (llSubStringIndex(st, "$")==0)
            {
                tokens = [] + llListReplaceList(tokens, [ GetScriptVar(llGetSubString(st,1,-1) ) ],   i , i);
            }
        }
    }
   
    string cmd1= llList2String(tokens,4);
    string cmd2= llList2String(tokens,5);
    list userData;
    
    if (sendUid!= NULL_KEY && (llGetAgentSize(sendUid) != ZERO_VECTOR))
    {
        if (!IsAllowed(npcName, cmd1, sendUid)) 
        {
            llOwnerSay("Denied '"+cmd1+"' to "+(string)sendUid+" "+llKey2Name(sendUid));
            return 1;
        }
    }

    
    if (llList2String(aviStatus, idx) == "prompt")
    {
        aviStatus =  []+llListReplaceList(aviStatus, [""], idx, idx); // Turn off prompt in sync with the listener
        if (npcName != name2 ) // it (probably) a response to the prompt, rather than a command given to the npc
        {
            integer i;
            for (i=3; i < llGetListLength(tokens); i++)
            {
                
                if ( llSubStringIndex(llList2String(aviPrompts, idx), "["+llToLower(llList2String(tokens, i))+"]" ) > 0) // label existed in prompt
                {
                    aviTarget =  []+llListReplaceList(aviTarget, [sendUid], idx, idx);
                    ScriptJump(idx, llToLower(llList2String(tokens, i)) , 1);
                    return 1;
                }
            }
            return 1; 
        }
    }
    
    
    if (cmd1 == "stop")
    {
        doStopNpc(idx, uNPC);
    }
    else if (cmd1 == "come")
    {
        doStopNpc(idx, uNPC);
        userData=llGetObjectDetails((key)sendUid, [OBJECT_NAME,OBJECT_POS, OBJECT_ROT]);
        osNpcStopMoveToTarget(uNPC);
        osTeleportAgent(uNPC, llList2Vector(userData, 1) + <1, 0, 0>, <1,1,1>);
        if (sendUid != NULL_KEY) // NOTE: a real avatar sent this command - stop processing our script
             aviScriptIndex  =  []+llListReplaceList(aviScriptIndex, -1, idx, idx);
    }
    else if (cmd1 == "stand")
    {
        aviStatus =  []+llListReplaceList(aviStatus, [""], idx, idx);
        osNpcStand(uNPC);
        osNpcStopMoveToTarget(uNPC);
    }
    else if (cmd1 == "moveto" || cmd1 == "movetov" || cmd1 == "runtovr"|| cmd1 == "movetovr" || cmd1 == "flytov" || cmd1 == "runtov" || cmd1=="walk" )
    {
        // Walk to the specified waypoint or vector
        vector v;
        string anim =""; // Specify an animation to play while walking
        if  (cmd1 == "runtovr"||cmd1 == "movetovr")
        {
            // run to somewhere within the volume enclosed by v1 and v2
            vector v1 = (vector) cmd2;
            vector v2 = (vector) llList2String(tokens, 6);
            v.x= v1.x + llFrand(v2.x-v1.x);
            v.y= v1.y + llFrand(v2.y-v1.y);
            v.z= v1.z + llFrand(v2.z-v1.z);
            anim = llList2String(tokens, 7);
        }
        else if (cmd1 == "movetov" || cmd1 == "flytov" ||cmd1 == "runtov" || cmd1 =="walk")
        {
            v = (vector)cmd2;
            if (v == ZERO_VECTOR)
            {
                llOwnerSay(npcName + ": "+cmd2+" is not a good position. I am not going there!");
                return 1;
            }
            anim = llList2String(tokens, 6);
        }
        else
            v = llList2Vector(wNodes, (integer)cmd2);
        
        float dist = llVecDist(osNpcGetPos(uNPC), v);        
        
        if (cmd1 == "runtovr"|| cmd1 == "runtov")
        {
            osSetSpeed(uNPC, 1.0);
            osNpcMoveToTarget(uNPC, v + <0,0,1>, OS_NPC_NO_FLY | OS_NPC_RUNNING);
            SetScriptAlarm(idx, (integer)(GetWalkTime(dist)/2.));
        }
        else
        {
            if (anim == "")
                osNpcStand(uNPC);
            osNpcStopMoveToTarget(uNPC);
            osSetSpeed(uNPC, 0.5);
            if (cmd1 == "flytov")
                osNpcMoveToTarget(uNPC, v + <0,0,1>, OS_NPC_FLY );
            else
                osNpcMoveToTarget(uNPC, v + <0,0,1>, OS_NPC_NO_FLY);
            if (anim)
            {
                llSleep(0.5);
                osNpcPlayAnimation(uNPC, anim);
            }
            
            SetScriptAlarm(idx, GetWalkTime(dist));
        }
    }
    else if (cmd1 == "setvar")
    {
            string cmd3 = llList2String(tokens,6);
            setVar(cmd2, cmd3);
            return 0;
    }
    else if (cmd1 == "if" || cmd1 == "if-not" || cmd1=="if-prob")
    {
        integer res = 0;
        if (cmd1 == "if-prob")
        {
            if (llFrand(1.0)<(float)cmd2)
                res = 1;
        }
        else if (cmd2 == "name-is")
        {
            integer k;
            res=0;
            for (k=6; k < llGetListLength(tokens); k++)
            {
                if (llToLower(npcName) == llToLower(llList2String(tokens,k)))
                {
                    setVar("_found", npcName);
                    res=1;
                }
            }
        }
        else if (cmd2 == "var-is")
        {
            integer k;
            res=0;
            for (k=6; k < llGetListLength(tokens); k+=2)
            {
                string nm = llList2String(tokens,k);
                if (nm == "")  jump varIsBreak;
                string val = llList2String(tokens,k+1);
                if (GetScriptVar(nm) == val)
                        res =1;
                else 
                {
                    res=0;
                    jump varIsBreak;
                }
            }
            @varIsBreak;
        }
        else if (cmd2 == "state-is")
        {
            // If state-is <avi-name> <state-value>
            integer nwho = GetNPCIndex(llList2String(tokens,6));
            if (nwho >=0)
            {
                string what = llList2String(aviScriptState,nwho);
                integer k;
                for (k=7; k < llGetListLength(tokens); k++)
                {
                    if (what == llList2String(tokens,k))
                                      res=1;
                }
            }
        }
        
        if (cmd1 == "if-not")
            res = !res;
            
        integer scrline = llList2Integer(aviScriptIndex, idx);
        if (scrline <0) 
        {
            return 1; // wtf
        }

        if (!res)
        {
            integer foundLine = FindMatchingEndif(llList2String(aviScriptText,idx), scrline-1); /// this used to skip a line
            if (foundLine == -1)
            {
                llOwnerSay("Error: end-if not found afterr "+cmd1 + " "+cmd2 + "...");
            }
            else
            {
                aviScriptIndex  =  []+llListReplaceList(aviScriptIndex, [foundLine+1], idx, idx);// Go past the end-if -- runs notecards faster 
            }
        }
        return 0;
    }
    else if (cmd1 == "end-if")
    {
        // Do nothing
        return 0;
    }
    else if (cmd1 == "prompt")
    { 
        string prompt = llDumpList2String(llList2List(tokens, 5, -1), " ");
        aviStatus =  []+llListReplaceList(aviStatus, ["prompt"], idx, idx);
        osNpcSay(uNPC, prompt);
        aviPrompts = []+llListReplaceList(aviPrompts, [llToLower(inputString)], idx, idx);
        aviTarget =  []+llListReplaceList(aviTarget, [NULL_KEY], idx, idx);
        osMessageAttachments(uNPC, "prompt", [ATTACH_RIGHT_PEC], 0);
    }
    else if (cmd1 == "jump")
    {
        // Jump to a label in the notecard
        ScriptJump(idx, llToLower(cmd2), 1);
        return 0; // process next cmd immediately
    }
    else if ((cmd1 == "go" && cmd2 == "to")   || cmd1 == "goto")
    {
        // Pathfinding command
        integer nearest = GetNearestNode(osNpcGetPos(uNPC));
        integer foundId =-1;        
        if (cmd1 == "goto")
        {
            foundId = (integer) cmd2;
        }
        else
        {
            string where = llToLower(llStringTrim(llList2String(tokens,6) +" "+ llList2String(tokens,7) +" "+ llList2String(tokens,8), STRING_TRIM));
            integer i;
            if (where != "")
                foundId = GetNodeIndexByName(where);

            if (foundId <0)
            {
                list tmp;
                for (i=0; i < llGetListLength(wNodeNames); i++) 
                    if (llList2String(wNodeNames,i) != "")  
                        tmp += llList2String(wNodeNames, i);
                osNpcSay(uNPC, "Sorry i dont know how to get to the "+where+ ". Here's some of the places i know: " +llList2CSV(tmp));
                return 1;
            }
        }
        
        osNpcSay(uNPC, "Let me think... ");
        string cachekey = "f,"+(string)nearest+","+(string)foundId;
        string gotoPath ="";
        integer fidx = llListFindList(cache, [cachekey]);
        if (fidx>=0)
        {
            gotoPath = llList2String(cache, fidx+1);
        }
        else
        {
            gotoPath = GetGotoPath(nearest, foundId);
            if (gotoPath != "")
            {
                cache += cachekey;
                cache += gotoPath;
            }
        }
        if (gotoPath == "")
        {
            osNpcSay(uNPC, "I 'm dumb. i don't know how to get there ... ");
            return 1;
        }
        osNpcSay(uNPC, "If you want to go there, follow me.");
        SetScriptAlarm(idx, 0);
        aviPath = []+ llListReplaceList(aviPath, [gotoPath], idx, idx);
        aviStatus =  []+llListReplaceList(aviStatus, ["pathf"], idx, idx);
    }
    else if (cmd1 == "setpath")
    {
        //Path must be in format 2:4:63:22:1 where the numbers are the waypoints numbers
        aviPath = []+ llListReplaceList(aviPath, [cmd2], idx, idx);
        SetScriptAlarm(idx, 0);
        aviStatus =  []+llListReplaceList(aviStatus, ["pathf"], idx, idx);
    }
    else if (cmd1 == "waitvar")
    {
            // Wait until the value of variable named cmd2 reaches the value cmd3
            string cmd3 = llList2String(tokens,6);
            string vval = GetScriptVar(cmd2);

            if (vval == cmd3)
            {
                    return 1; /// OK continue with the next line
            }
            else if (cmd3 == "NONEMPTY" && vval != "")
            {
                return 1;
            }

            integer scriptIndex = llList2Integer(aviScriptIndex, idx);
            if  (scriptIndex>0)
            {
                aviScriptIndex = []+llListReplaceList(aviScriptIndex, [scriptIndex-1], idx, idx);
            }
    }
    else if  (cmd1 == "increase" || cmd1 == "decrease" || cmd1 == "zero")
    {
        integer v = (integer)GetScriptVar(cmd2);
        if (cmd1=="increase") v++;
        else if (cmd1 == "decrease") v--;
        else v=0;
        setVar(cmd2, (string)v);
        return 1;
    }
    else if (cmd1 == "wait")
    {
        integer tm = (integer)cmd2;
        integer tm2 = (integer)llList2String(tokens,6);
        if (tm2>0)
            tm = (integer)(tm + llFrand(tm2));
        SetScriptAlarm(idx,  tm);
    }
    else if (cmd1 == "say" || cmd1 == "shout")
    {
        // Say something on chat
        string txt = "";
        integer i;
        for (i=5; i < llGetListLength(tokens); i++)
            txt += llList2String(tokens,i) + " ";
        if (cmd1 == "shout")
            osNpcShout(uNPC, 0, txt);
        else
            osNpcSay(uNPC, txt);
        return 0;
    }
    else if (cmd1 == "saych")
    {
        // Say something on channel
        string txt = "";
        integer i;
        for (i=6; i < llGetListLength(tokens); i++)
            txt += llList2String(tokens,i) + " ";
        osNpcSay(uNPC,  llList2Integer(tokens,5), txt);
    }
    else if (cmd1 == "loadnpc")
    {
        doLoadNPC(cmd2, llList2String(tokens, 6));
    }
    else if (cmd1 == "removenpc")
    {
        doRemoveNpc(cmd2);
    }
    else if (cmd1 == "exec")
    {
        list tok2 = ["!", (string)NULL_KEY, cmd2] + llList2List(tokens, 5, -1);
        //llOwnerSay(llList2CSV(tok2));
        ProcessNPCCommand(llDumpList2String(tok2, " "));
    }

    else if (cmd1 == "msgatt")
    {

        list points = [];
        integer i;
        for (i=6; i < llGetListLength(tokens); i++)
        {
            if (llList2Integer(tokens, i)>0)
                points += llList2Integer(tokens,i);
        }
        osMessageAttachments(uNPC, cmd2, points, 0);
    }
    else if (cmd1 == "teleport")
    {
        vector w = (vector) cmd2;
        if (w == ZERO_VECTOR)
        {
            integer where = GetNodeIndexByName(cmd2);
            if (where >=0)
            {
                w = llList2Vector(wNodes, where);
                osTeleportAgent(uNPC, w, <0,0,0>);
            }
        }
        else osTeleportAgent(uNPC, w, <0,0,0>);
    }
    else if (cmd1 == "use")
    {
        // Sit-on-a-poseball command
        string cmd = llStringTrim(cmd2+" "+llList2String(tokens, 6)+" "+llList2String(tokens,7), STRING_TRIM);
        osMessageAttachments(uNPC, "do "+cmd, [ATTACH_RIGHT_PEC], 0);
    }
    else if  (cmd1 == "lookat")
    {
        vector v;
        if (cmd2=="me")
        {
            userData=llGetObjectDetails((key)sendUid, [OBJECT_NAME,OBJECT_POS, OBJECT_ROT]);
            v = llList2Vector(userData,1);
        }
        else
        {
            v = (vector)cmd2;
            if  (v == ZERO_VECTOR)
            {
                integer midx = GetNodeIndexByName(llToLower(cmd2));
                if (midx >=0)
                {
                    v = llList2Vector(wNodes, midx);
                }
            }
        }
        osNpcSetRot(uNPC, llRotBetween(<1,0,0>, v-osNpcGetPos(uNPC)));//llEuler2Rot(<0,0,ang>)); 
    }
    else if (cmd1 == "anim")
    {
        osNpcStopAnimation(uNPC, llList2String(aviCurrentAnim, idx));
        aviCurrentAnim = llListReplaceList(aviCurrentAnim, [cmd2], idx, idx);
        osNpcPlayAnimation(uNPC, cmd2);
    }
    else if (cmd1 == "give")
    {
        if (llGetInventoryType(cmd2) == INVENTORY_OBJECT)
            llGiveInventory(sendUid, cmd2);
    }
    else if (cmd1 == "light")
        osMessageAttachments(uNPC, "light", [ATTACH_RIGHT_PEC], 0);
    else if (cmd1 == "sound")
        osMessageAttachments(uNPC, "sound " + cmd2+" "+llList2String(tokens, 6) , [ATTACH_RIGHT_PEC], 0);
    else if (cmd1 == "batch")
    {
        // Run multiple commands from the chat, separated by ";"   --- replaces any running script
        string str = llDumpList2String(llList2List(tokens, 5, llGetListLength(tokens))," ");
        aviScriptText =  []+llListReplaceList(aviScriptText, str, idx, idx);
        aviScriptIndex =  []+llListReplaceList(aviScriptIndex, [1], idx, idx);
        aviStatus =  []+llListReplaceList(aviStatus, "", idx, idx);
        SetScriptAlarm(idx, 0);
    }
    else if (cmd1 == "follow")
    {
        aviStatus =  []+llListReplaceList(aviStatus, ["follow"], idx, idx);
        if (cmd2=="me" || cmd2=="")
        {
            userData=llGetObjectDetails((key)sendUid, [OBJECT_NAME,OBJECT_POS, OBJECT_ROT]);
            aviFollow =  []+llListReplaceList(aviFollow, [(key)sendUid], idx, idx);
            osNpcSay(uNPC, "Following you "+ llList2String(userData, 0));
        }
        else
        {
            key who = getAgentByName(cmd2);
            if (who != NULL_KEY)
            {
                    aviFollow =  []+llListReplaceList(aviFollow, [who], idx, idx);
                    osNpcSay(uNPC, "Following " + cmd2);
            }
        }

    }
    else if (cmd1 == "set-state") // set a variable that indicates the current state of an NPC -- useful for scripts
    {
        aviScriptState=  []+llListReplaceList(aviScriptState, [cmd2], idx, idx);
        return 0;
    }
    else if (cmd1 == "debug")
    {
        integer dd = llList2Integer(aviScriptIndex, idx);
        string scr;
        if  (dd >=0)
        {
            scr = GetScriptLine(llList2String(aviScriptText,idx) , dd-1);
        }
        llOwnerSay("Status="+llList2String(aviStatus, idx)+" node = "+llList2Integer(aviNodes, idx)+
            " follow="+llList2String(aviFollow, idx)+" Alarm = "+(string)(llList2Integer(aviAlarm,idx)-llGetUnixTime())+
            " scriptIndex="+llList2Integer(aviScriptIndex, idx)+" scriptText " +scr );
    }
    else if (cmd1 == "fly" && cmd2=="with")  // "fly with me" "fly with Foo"
    {
        string who = llList2String(tokens, 6);
        if (who == "me")
        {
            aviFollow =  []+llListReplaceList(aviFollow, [(key)sendUid], idx, idx);
        }
        else
        {
            key w = getAgentByName(who);
            if (w != NULL_KEY)
            {
                aviFollow =  []+llListReplaceList(aviFollow, [w], idx, idx);

            }
        }
        aviStatus = llListReplaceList(aviStatus, ["flyfollow"], idx, idx);
        osNpcSay(uNPC, "Flying ");
    }
    else if (cmd1 == "leave")
    {
        // Start wandering between waypoints
        osNpcStand(uNPC);
        aviNodes =  []+llListReplaceList(aviNodes, [GetNearestNode(osNpcGetPos(uNPC))], idx, idx);
        aviStatus =  []+llListReplaceList(aviStatus, ["wander"], idx, idx);
        aviPrevNodes =  []+llListReplaceList(aviPrevNodes, [-1], idx, idx);
    }
    else if (cmd1 == "flyaround")
    {
        // Start flying about between the waypoints in the "flyTargets" list -- useful for birds
        aviStatus =  []+llListReplaceList(aviStatus, ["godfly"], idx, idx);
        osNpcSay(uNPC, "Flying like an eagle!!");

    }
    else if (cmd1 == "run-notecard")
    {
        // Run the script contained in the notecard <argument>
        string stext= osGetNotecard(cmd2 );
        aviStatus=  []+llListReplaceList(aviStatus, "", idx, idx);
        if (stext == "ERROR")
        {
            llOwnerSay("Notecard error "+cmd2);
            return 1;
        }
        aviScriptText =  []+llListReplaceList(aviScriptText, stext, idx, idx);
        aviScriptIndex =  []+llListReplaceList(aviScriptIndex, [1], idx, idx);
        SetScriptAlarm(idx, 0);
    }
    else if (cmd1 == "stop-script")
    {
        // Stop executing the script and exit
        aviScriptIndex =  []+llListReplaceList(aviScriptIndex, [-1], idx, idx);
        SetScriptAlarm(idx, 0);
    }
    else if (cmd1 == "dress")
    {
        string suff = "";
        if (cmd2 != "") suff += "_"+cmd2;
        string nm = llList2String(aviNames, idx);
        llOwnerSay("Loading appearance "+"APP_"+nm+suff);
        osNpcLoadAppearance(uNPC, "APP_"+nm+suff);
    }
    else if (cmd1 == "touch")
    {
        osNpcTouch(uNPC, (key)cmd2, LINK_ROOT);
    }
    else if (cmd1 == "seen")
    {
        integer i;
        if (cmd2 == "all")
        {
            for (i=0; i < llGetListLength(seenArchive); i+=2)
                osNpcSay(uNPC, "I saw "+ llList2String(seenArchive,i) + "  "  + TimeAgo(llList2Integer(seenArchive,i+1) ));
            return 1;
        }
        else
        {
            for (i=0; i < llGetListLength(seenArchive); i+=2)
            {
                if (llSubStringIndex(llToLower(llList2String(seenArchive,i)), llToLower(cmd2))>=0)
                {
                    osNpcSay(uNPC, "I saw "+ llList2String(seenArchive,i) + " around "  + TimeAgo(llList2Integer(seenArchive,i+1) ));
                    return 1;
                }
            }
        }
        osNpcSay(uNPC, "I haven't seen "+ cmd2 + " around");
    }
    else if (cmd1 == "nearest")
    {
        integer n = GetNearestNode(osNpcGetPos(uNPC));
        osNpcSay(uNPC, "Nearest waypoint is #"+n); 
    }
    else if (llGetSubString(cmd1,0,0) == "@")
        return 0;
    else if (cmd1 != "")
    {
       
        {
            if (llGetInventoryType(cmd1+".scr") == INVENTORY_NOTECARD)
            {
                ExecScriptLine(npcName , "run-notecard "+cmd1+".scr");
            }
            else
                llMessageLinked(LINK_THIS, -1, inputString, uNPC);
        }
    }            
    return 1; // 1 means that wait until next timer tick for next notecard command 
}

integer FindNewTarget(integer curNode, integer prevNode)
{
    integer total=llGetListLength(wLinks);
    candidateNode = [];
    integer i;
    integer a;
    integer b;
    for (i=0; i< total; i+=2)
    {
        a = llList2Integer(wLinks,i);
        b = llList2Integer(wLinks,i+1);
        if (a == curNode && prevNode != b) /// dont go back where we came from
            candidateNode += b;
        else if (b == curNode && prevNode !=a)
            candidateNode += a;
    }
    
    integer  l = llGetListLength(candidateNode);
    if (l>0)
    {
        return llList2Integer(candidateNode, (integer)llFrand((float)l));
    }
    else 
        return prevNode; // go back to where we came from if there is no other option
}


integer MoveToNewTarget(integer idx)
{
    integer curNode = llList2Integer(aviNodes,idx);
    integer prevNode = llList2Integer(aviPrevNodes,idx);

    key uuid = llList2Key(aviUids, idx);
    if (uuid == NULL_KEY)  return 1;
    vector pos = osNpcGetPos(uuid);
    osNpcStand(uuid);
    
    vector wp = llList2Vector(wNodes, curNode);
    float dist = llVecDist(pos, wp);
    if (dist>10) osTeleportAgent(uuid,  wp, <1,1, 7.1>);

    integer nt = FindNewTarget(curNode, prevNode);
    if (nt <0) return 0;
    vector tgt = llList2Vector(wNodes, nt);
    // Try to stay in the right 'lane'
    vector rr = 0.5*llVecNorm(tgt - pos)*llEuler2Rot(<0,0,-PI/2>);
    tgt += rr;
    osSetSpeed(uuid, 0.5);
    osNpcMoveToTarget(uuid, tgt, OS_NPC_NO_FLY);
    SetScriptAlarm(idx,  GetWalkTime( llVecDist(wp, tgt) )+4);
    aviNodes = []+llListReplaceList(aviNodes, [nt], idx, idx);
    aviPrevNodes = []+llListReplaceList(aviPrevNodes, [curNode], idx, idx);
    return 0;
}


integer ExecScriptLine(string aviName, string scriptline)
{
    // The token list expects the name of the avi twice. we use 0000 as the sending-uid identifier
    string command = "! "+ (string)NULL_KEY +" " + aviName +" "+ aviName +" "+ scriptline;
   // list tokens = llParseString2List(command, [" "], [] );
    return ProcessNPCCommand(command);
}



string TimeAgo(integer time)
{
    // time difference in seconds
    integer now = llGetUnixTime();
    integer timeDifference = now - time;
    // small bug fix for when timeDifference is 0
    if (timeDifference == 0)
        return "just now";
 
    list periods = ["second",        "minute",        "hour",        "day",        "week",        "month",        "year",        "decade"];
 
    //the number equivalent to periods
    list lenghts = [1,        60,        3600,        86400,        604800,        2630880,        31570560,        315705600];
 
    integer v = llGetListLength(lenghts) - 1;
    integer no;
 
    while((0 <= v) && (no = timeDifference/llList2Integer(lenghts, v) <= 1))    --v; 
    string output = llList2String(periods, v);
 
    //this will get the correct time in periods, then divide the timeDifference
    integer ntime = timeDifference / llList2Integer(lenghts, llListFindList(periods, [output]));
 
    //if integer 'no' is not equal to 1 then it should have an s at the end
    if(no != 1)
        output += "s";
 
    //This produces the finished output
    output = (string)ntime + " "+ output + " ago";
    return output;
}

giveCommands(integer n)
{
    integer i;
    string lstr = "";
    string kstr = "";
    list lnks;
    for (i=0; i < llGetListLength(wayLinks); i+=2)
    {
        integer a = llList2Integer(wayLinks,i);
        integer b = llList2Integer(wayLinks,i+1);
        if (a == n)
        {
            lstr += (string)b+",";
            lnks += (string)llList2Key(wayKeys,b);
        }
        else if (b == n)
        {
            lstr += (string)a+",";
            lnks += (string)llList2Key(wayKeys,a);
        }
    }
    
    string wstr = (string)n+"|SETDATA|"+vec2str(llList2Vector(wayPoints, n));
    wstr += "|"+llList2String(wayNames, n)+"|"+lstr+"|0|"+llList2CSV(lnks);
    //llOwnerSay(wstr);
    llRegionSay(PEG_CHAN, wstr);
}



default
{

    state_entry()
    {
        llSetText("NPCs", <1,1,1>,1.0);
        llListenRemove(gListener);
        gListener = llListen(channel, "", "", "");
        llOwnerSay("Listening on channel "+channel);
        ReloadConfig();
        LoadMapData();
        timerRuns=0;
        RescanAvis();
        greetedAvis = [];
        scriptVars = [];
        
        if (autoLoadOnReset)
        {
            llSleep(10);
            doLoadAll();
            llSleep(10); // Need to wait for their listeners attachments to start
            doInitCmds();
            llSleep(10);
        }
        
        llSetTimerEvent(TIMER_INTERVAL);
    }
    
    touch_start(integer num)
    {
        
        if (llDetectedKey(0) != llGetOwner()) return;
        llDialog(llGetOwner(), "Welcome", menuItems, channel);
    }
    

    // This checks the statuses of all avis and performs commands accordingly
    timer()
    {
        integer total = llGetListLength(aviUids);
        integer g;
        integer advanceScript;
        list startedScripts = [];
        if (curVisitors>0)
        for (g=0; g < total ; g++)
        {
                advanceScript =0;
                aviIndex = g;
                npc = llList2Key(aviUids, g);
                string status = llList2String(aviStatus, g); 

                if (status == "follow" || status == "flyfollow")
                {
                    // This NPC is following someone
                    integer stat=llGetAgentInfo(npc);
                    if (stat & AGENT_SITTING)
                    {
                        // We 've been sat. stop following
                        return;
                    }
                    
                    key who = llList2Key(aviFollow, g);
                    list userData = llGetObjectDetails(who, [OBJECT_POS, OBJECT_ROT]);
                    if (llGetListLength(userData) ==0)
                    {
                        // User left or died
                        aviStatus=  []+llListReplaceList(aviStatus, [ "" ], g, g);
                        return;
                        
                    }
                    
                    rotation rot = llList2Rot(userData,1);
                    float ang = llFrand(1.0);
                    vector v = llList2Vector(userData,0) + <-1.9,0,0>*rot;
                    float dist = llVecDist(osNpcGetPos(npc), v);

                    if  (status == "follow" && dist>50.)
                    {
                        osTeleportAgent(npc, v, <1,1,1>);
                    }
                    else if  (dist>4)
                    {
                        //osNpcStopMoveToTarget(npc);
                        if (osIsNpc(who))
                            osSetSpeed(npc, .47);
                        else osSetSpeed(npc, 1.0);
                        if (status == "flyfollow")                
                           osNpcMoveToTarget(npc, v+<0,0,2.>, OS_NPC_FLY );
                        else
                            osNpcMoveToTarget(npc, v, OS_NPC_NO_FLY );
                    }
                }
                else if (status == "wander")
                {
                    if (llGetUnixTime()  > llList2Integer(aviAlarm, g) +1)
                    {
                            integer curNode = llList2Integer(aviNodes, g);
                            integer i;
                            integer shouldMove =1;
                            llMessageLinked(LINK_THIS, -1, "WAYPOINT " + (string)curNode+" "+llList2String(aviNames, g), npc);
                            // avoid looping back to the same script while we are about to leave                            
                            if (llList2Integer(aviPrevNodes, g)>=0)
                            {
                                if (llListFindList(startedScripts, curNode)>=0)
                                {
                                    // dont start the same script simultaneously
                                }
                                else
                                {
                                    string ncName = "_"+curNode+".scr";
                                    if (llGetInventoryType(ncName) == INVENTORY_NOTECARD)
                                    {
                                            startedScripts+= curNode;
                                            ExecScriptLine(llList2String(aviNames, g), "run-notecard "+ncName);
                                            shouldMove =0;
                                    }                                
                                }
                            }
                            
                            if (shouldMove>0)
                            {
                               MoveToNewTarget(g);
                            }
                    }
                }
                else if (status == "godfly")
                {
                    // This NPC is flying around
                    if (llGetUnixTime()  > llList2Integer(aviAlarm, g) +1)
                    {
                        vector nd = (vector)llList2String(flyTargets, (integer)llFrand(llGetListLength(flyTargets)));
                        integer flag = OS_NPC_FLY;
                        osSetSpeed(npc, 0.5);
                        integer theight = 10;
                        vector p = osNpcGetPos(npc);
                        SetScriptAlarm(g, GetWalkTime(llVecDist(p, nd))/2);
                        osNpcMoveToTarget(npc, nd +  <llFrand(1),llFrand(1),theight>, flag);
                        
                    }
                }
                else if (status == "pathf")
                {
                    // Pathfinding - this NPC is following the path to a destination
                    integer avits = llList2Integer(aviAlarm, g);
                    if (llGetUnixTime() > avits)
                    {
                    
                        vector p = osNpcGetPos(npc);
                        string path = llList2String(aviPath, g);
                        llOwnerSay("Path="+path);
                        list pnodes = llParseString2List(path, [":"], []);
                        if (llGetListLength(pnodes)<1)
                        {
                            osNpcSay(npc, "I have arrived");
                            aviStatus =  []+llListReplaceList(aviStatus, [ "" ], g, g);
                            // continue the script (if any), since we reached our destination
                            SetScriptAlarm(g, 0); 
                        }
                        else
                        {
                            integer nextTgt = llList2Integer(pnodes, 0);
                            string ndleft = ":"+llDumpList2String( llList2List(pnodes, 1, llGetListLength(pnodes)), ":");
                            aviPath =  []+llListReplaceList(aviPath, [ ndleft ], g, g);
                            vector v = llList2Vector(wNodes, nextTgt);              
                            SetScriptAlarm(g, GetWalkTime(llVecDist(p, v)));
                            osNpcMoveToTarget(npc, v + <llFrand(1.0),llFrand(1.0), 0.1> , OS_NPC_NO_FLY );
                        }
                    }
                    return;
                }
                else if (status == "prompt")
                {
                    // do nothing
                    jump nexttick;
                }

                
                // Execute the next script line if a script is active
                integer stopNow=0;
                integer k;
                integer scriptIndex = llList2Integer(aviScriptIndex, g);
                while ( scriptIndex>0  && stopNow==0 &&  k++<5) // execute up to 10 lines at once if possible
                {

                    //llOwnerSay("scriptIndex = "+ (string)scriptIndex);
                        integer tsAlarm = llList2Integer(aviAlarm, g);
                        if (tsAlarm >0 && llGetUnixTime() >= tsAlarm ) // The script should continue now
                        {
                            string scriptData = llList2String(aviScriptText, g);
                            string scriptline = GetScriptLine(scriptData, scriptIndex);
                            if (scriptline == "") // End of script
                            {
                                    // This will prevent any further execution
                                    aviScriptIndex =  []+llListReplaceList(aviScriptIndex, [-1], g, g);
                            }
                            else
                            {
                                // Substitute sender with the prompt target, if any
                                string cmd = "! "+ (string)llList2Key(aviTarget, g) +" "+ llList2String(aviNames, g) +" "+ llList2String(aviNames, g) + " "+ scriptline; 
                                stopNow = ProcessNPCCommand(cmd);
                                // Advance script pointer 
                                scriptIndex = llList2Integer(aviScriptIndex, g); 
                                aviScriptIndex =  []+llListReplaceList(aviScriptIndex, [scriptIndex+1], g,g);
                            }
                        }
                        scriptIndex = llList2Integer(aviScriptIndex, g);
                }
                
                @nexttick;
                
                llParticleSystem(
                [
                    PSYS_SRC_PATTERN,PSYS_SRC_PATTERN_EXPLODE,
                    PSYS_SRC_BURST_RADIUS,0,
                    PSYS_SRC_ANGLE_BEGIN,0,
                    PSYS_SRC_ANGLE_END,0,
                    PSYS_SRC_TARGET_KEY,llGetKey(),
                    PSYS_PART_START_COLOR,<1.000000,0.000000,0.000000>,
                    PSYS_PART_END_COLOR,<1.000000,0.000000,0.000000>,
                    PSYS_PART_START_ALPHA,1,
                    PSYS_PART_END_ALPHA,0,
                    PSYS_PART_START_GLOW,0,
                    PSYS_PART_END_GLOW,0,
                    PSYS_PART_BLEND_FUNC_SOURCE,PSYS_PART_BF_SOURCE_ALPHA,
                    PSYS_PART_BLEND_FUNC_DEST,PSYS_PART_BF_ONE_MINUS_SOURCE_ALPHA,
                    PSYS_PART_START_SCALE,<0.500000,0.500000,0.000000>,
                    PSYS_PART_END_SCALE,<4.000000,4.000000,0.000000>,
                    PSYS_SRC_TEXTURE,"",
                    PSYS_SRC_MAX_AGE,0.5,
                    PSYS_PART_MAX_AGE,2,
                    PSYS_SRC_BURST_RATE,1,
                    PSYS_SRC_BURST_PART_COUNT,1,
                    PSYS_SRC_ACCEL,<0.000000,0.000000,0.000000>,
                    PSYS_SRC_OMEGA,<0.000000,0.000000,0.000000>,
                    PSYS_SRC_BURST_SPEED_MIN,0,
                    PSYS_SRC_BURST_SPEED_MAX,0,
                    PSYS_PART_FLAGS,
                        0 |
                        PSYS_PART_EMISSIVE_MASK |
                        PSYS_PART_INTERP_COLOR_MASK |
                        PSYS_PART_INTERP_SCALE_MASK
                ]);
                
        }
        
        timerRuns++;
        if (timerRuns%20==0)
        {
            curVisitors = countVisitors();
        }
    }

    listen(integer chan, string name, key id, string str) {    // WARNING "id" is not the uid of the NPC-sender

        string mes = str;                                
        integer x = llSubStringIndex(str, " ");
        if (x >=0)    mes = llGetSubString(str, 0,x-1);

        if (!(osIsNpc(llGetOwnerKey(id)) || llGetOwnerKey(id)==llGetOwner())) 
        {
            llOwnerSay("Denied access to "+llKey2Name(id)); 
            return; 
        }

        //llOwnerSay("<<" + str);
        if (mes == "!") // Something that has been sent from a Listener of attached to an NPC
        {
            ProcessNPCCommand(str);
            return;
        }   
        else if (mes =="FBALL")
        {
            // A poseball has been found. We have to check if it is transparent. If it is not, then we sit the NPC on it
            list tok = llParseString2List(str, [" "] , [""]);
            string npcname= llList2String( tok, 1);
            integer  idx = GetNPCIndex(npcname);
            if (idx<0) return;
            key unpc = llList2Key(aviUids, idx);
            integer i;
            key ball;
            for (i=2; i < llGetListLength(tok);i++)
            {
                ball = llList2String(tok, i);
                list prop = osGetPrimitiveParams(ball, [PRIM_COLOR, 0]); /// This only works we own the poseball
                float alpha = 1.0;
                if (llGetListLength(prop)>0)  alpha = llList2Float(prop, 1);
                if (alpha >0)
                {
                        jump ballFound;
                }
            } 
            //llOwnerSay(npcname + ": All balls transparent");  
            @ballFound;
            if (ball != NULL_KEY)
            {
                    osNpcStand(unpc);
                    osNpcStopMoveToTarget(unpc);
                    osNpcSit(unpc, ball, OS_NPC_SIT_NOW);
                    aviStatus =  []+llListReplaceList(aviStatus, ["sitting"], idx, idx);
            }
        }
        else if (mes == "SETVAR")
        {
            list tok = llParseString2List(str, [" "] , [""]);
            setVar(llList2String(tok,1), llList2String(tok,2));
        }
        else if (llGetSubString(mes, 0, 7) == "CLICKED|")// Message from map editor HUD
        {
            list ll = llParseString2List(str, ["|"] ,[]);
            string cmd1 = llList2String(ll, 0);
            integer num = llList2Integer(ll, 1);
            if (curPoint!= num)
            {
                prevPoint = curPoint;
                curPoint = num;
            }

             list btns = ["Close", "LinkPegs", "UnlinkPegs", "SetName"];
            llDialog(llGetOwner(), "Current peg: "+ (string)curPoint+ " Previous: "+(string)prevPoint, btns, channel);
            return;
        }
        else if  (llGetSubString(mes, 0, 6) == "MRKKEY|")
        {
            list ll = llParseStringKeepNulls(str, ["|"] ,[]);
            integer num = llList2Integer(ll, 1);
            key k = llList2Key(ll, 2);
            wayKeys = [] + llListReplaceList(wayKeys, [k], num,num);

        }
        else if  (llGetSubString(mes, 0, 6) == "MARKER|")
        {
            list ll = llParseStringKeepNulls(str, ["|"] ,[]);
            string cmd1 = llList2String(ll, 0);
            integer num = llList2Integer(ll, 1);
            vector pos = llList2Vector(ll, 2);
            key tk = llList2Key(ll, 4);
            wayPoints = llListReplaceList(wayPoints, [pos], num,num);

            return;
        }
        else if (mes == "ShowPegDialog")
        {
              list btns = ["Close", "RezPegs", "SaveCards", "AddPeg", "DeletePeg", "LinkPegs", "UnlinkPegs", "ScanPegs", "ClearPegs", "SetName"];
              llDialog(llGetOwner(), "First peg: "+ (string)curPoint+ " Second peg: "+(string)prevPoint, btns, 68);
              return;
        }

        
        if (id != llGetOwner()) return; // Admin commands follow

        if  (mes == "SaveNPC")
        {
            llDialog(llGetOwner(), "Select NPC to save your appearance", llList2List(availableNames, 0,10)+ "more", channel);       

            userInputState = "WAIT_APPNAME";
        }
        else if (mes == "LoadNPC")
        {
            llDialog(llGetOwner(), "Select an NPC to load", llList2List(availableNames, 0,10)+"more", channel);     
            userInputState = "WAIT_AVINAME";
        }
        else if (mes == "RemoveNPC")
        {
            llDialog(llGetOwner(), "Select an NPC to delete",  llList2List(availableNames, 0,10)+ "more", channel); 
            userInputState = "WAIT_REMOVEAVI";
        }
        else if (mes == "UpdateNPC")
        {
            llDialog(llGetOwner(), "Select an NPC to re-save appearance ", llList2List(availableNames, 0,10)+"more", channel);     
            userInputState = "WAIT_UPDATE";
        }
        else if (mes == "RemoveAll")
        {
            avis = osGetAvatarList();
            llSay(0, llList2CSV(avis));
            howmany = llGetListLength(avis);
            integer i;
            for (i =0; i < howmany; i+=3)
            {
                if (osIsNpc(llList2Key(avis, i)))
                {
                    list p = llParseString2List(llKey2Name(llList2Key(avis,i)), [" "], []);
                    doRemoveNpc(llList2String(p, 0));
                    //osNpcStand(llList2Key(avis, i));
                    //osNpcRemove(llList2Key(avis, i));
                }
            }
            aviUids = [];
            aviNames = [];
        }
        else if (mes == "LoadAll")
        {
            llSetTimerEvent(0);
            doLoadAll();
            llSetTimerEvent(TIMER_INTERVAL); 
        }
        else if (mes == "InitCmds")
        {    
            llSetTimerEvent(0);
            doInitCmds();
            llSetTimerEvent(TIMER_INTERVAL);    
        }
        else if (mes == "TimerOnOff")
        {
            timerRunning = !timerRunning;
            llSetTimerEvent(TIMER_INTERVAL*timerRunning);
            llOwnerSay("Timer="+(string)timerRunning);
        }
        else if (mes == "DumpData")
        {
            llOwnerSay("Names="+llList2CSV(aviNames));
            llOwnerSay("Status="+llList2CSV(aviStatus));
            llOwnerSay("Nodes="+llList2CSV(aviNodes));
            llOwnerSay("PrevNodes="+llList2CSV(aviPrevNodes));            
            llOwnerSay("ScriptIndex="+llList2CSV(aviScriptIndex));
            llOwnerSay("Alarm="+llList2CSV(aviAlarm));
            llOwnerSay("Curvisitors="+(string)(curVisitors)+ " Timer=" +timerRunning+" timerRuns="+(string)timerRuns);        
            llOwnerSay("Vars="+llList2CSV(scriptVars));
        }
        else if (mes == "ReConfig")
        {
            ReloadConfig();
            LoadMapData();
        }
        else if (mes == "deflectTo")
        {
            list tok = llParseString2List(str, [" "] , [""]);
            deflectToNode = GetNodeIndexByName(llToLower(llList2String(tok,1)));
            llOwnerSay("Deflecting to #"+(string)deflectToNode);
        }
        else if (mes == "AddPeg")
        {
            vector v = llGetPos();
            list res = llGetObjectDetails(llGetOwner(), [OBJECT_POS]);
            wayPoints += llList2Vector(res, 0);
            llOwnerSay("Added point " + (string)(llGetListLength(wayPoints)));
            llRezObject("peg", v, ZERO_VECTOR, ZERO_ROTATION, llGetListLength(wayPoints)-1);
            giveCommands( llGetListLength(wayPoints)-1);
            return;
        }
        else if (mes == "LinkPegs")
        {
            
            integer i;
            for (i=0; i < llGetListLength(wayLinks); i+=2)
            {
                integer a = llList2Integer(wayLinks,i);
                integer b = llList2Integer(wayLinks,i+1);
                if ((a == curPoint && b == prevPoint ) || (b == curPoint && a== prevPoint ))
                {
                    llOwnerSay("Link exists");
                    return;
                }
            }
            wayLinks += curPoint;
            wayLinks += prevPoint;
            giveCommands(curPoint);
            giveCommands(prevPoint);
        }
        else if (mes == "UnlinkPegs")
        {
            
            integer i;
            for (i=0; i < llGetListLength(wayLinks); i+=2)
            {
                integer a = llList2Integer(wayLinks,i);
                integer b = llList2Integer(wayLinks,i+1);
                if ((a == curPoint && b == prevPoint ) || (b == curPoint && a== prevPoint ))
                {
                    wayLinks = llListReplaceList(wayLinks, [], i, i+1);
                    giveCommands(a);
                    giveCommands(b);
                }
            }
        }
        else if (mes == "ClearPegs")
        {
            llRegionSay(PEG_CHAN, "die");
        }
        else if (mes == "ScanPegs")
        {
            llOwnerSay("Scanning pegs ordered");
            llRegionSay(PEG_CHAN, "REPORT");
        }
        else if (mes == "SetName")
        {
            llTextBox(llGetOwner(), "Set Peg #"+(string)curPoint + " name to: ", channel);
            userInputState="WAIT_PEGNAME";
        }
        else if (mes == "RezPegs")
        {
            list lines = llParseString2List(osGetNotecard("__waypoints"), ["\n"], []);
            integer i;
            wayPoints =[];
            wayNames = [];
            for (i=0; i < llGetListLength(lines); i++)
            {
                list line = llParseString2List(llList2String(lines, i), [","], []);
                vector v =  < llList2Float(line, 0), llList2Float(line, 1), llList2Float(line, 2) >;
                string nname = llList2String(line, 3);
                if (v != ZERO_VECTOR)
                {
                    wayPoints += v;
                    wayNames += nname;
                }
            }
            wayLinks = llParseString2List( llStringTrim(osGetNotecard("__links"), STRING_TRIM)  , ["\n", ","], [" "]);
            llOwnerSay(llList2CSV(wayLinks));
            llRegionSay(PEG_CHAN, "die");
            llSleep(0.5);
            vector pos = llGetPos();
            for (i=0; i < llGetListLength(wayPoints); i++)
                llRezObject("peg", pos, ZERO_VECTOR, ZERO_ROTATION, i);
            llSleep(0.5);
            for (i=0; i < llGetListLength(wayPoints); i++)
                     giveCommands(i);
        }
        else if (mes == "UpdatePegs")
        {
            integer i;
             for (i=0; i < llGetListLength(wayPoints); i++)
                     giveCommands(i);
        }
        else if (mes == "SaveCards")
        {
            integer i=0;
            string scriptText = "";
            if (llGetListLength(wayLinks)==0)
            {
                llOwnerSay("No links created! Not saving cards!");
                return;
            }
            for (i=0; i <  llGetListLength(wayPoints); i++)
            {
                vector v = llList2Vector(wayPoints,i);
                scriptText += (string)v.x+","+(string)v.y+","+(string)v.z +  "," + llList2String(wayNames, i) + "\n";
            }

            string cardName = "__waypoints";
            if (llGetInventoryType(cardName)==INVENTORY_NOTECARD)
            {
                llRemoveInventory(cardName);
                llSleep(0.5);
            }
            osMakeNotecard(cardName,scriptText);
            llOwnerSay(cardName +" Saved");
            cardName = "__links";
            if (llGetInventoryType(cardName)==INVENTORY_NOTECARD)
            {
                llRemoveInventory(cardName);
                llSleep(0.5);
            }
            scriptText = "";
            for (i=0; i <  llGetListLength(wayLinks); i+=2)
                scriptText += llList2String(wayLinks,i)+ ","+llList2String(wayLinks,i+1)+ ",\n";
            osMakeNotecard(cardName,scriptText);
            llOwnerSay(cardName +" Saved");
            llSleep(1);
            LoadMapData();
        }
        else if (userInputState != "" && mes != "")//  Process dialog commands
        {
            if (mes == "more")
            {
                 llDialog(llGetOwner(), "Select an NPC", llList2List(availableNames, 11,-1), channel);
            }
            else
            {
                if (userInputState == "WAIT_APPNAME")
                {
                    osAgentSaveAppearance(llGetOwner(), "APP_"+llToLower(mes));
                    llSay(0,  "Saved Appearance " + llGetOwner() + " -> APP_"+llToLower(mes));
                }
                else if (userInputState == "WAIT_PEGNAME")
                {
                    wayNames = [] + llListReplaceList(wayNames, [llStringTrim(mes, STRING_TRIM)],curPoint, curPoint);
                    giveCommands(curPoint);
                    llOwnerSay("Waypoint  " +(string)curPoint+ " name='"+mes+"'");
                }
                else if (userInputState == "WAIT_AVINAME")
                {
                    doLoadNPC(mes, GetLastName(mes));

                }
                else if (userInputState == "WAIT_UPDATE")
                {
                    integer idx = GetNPCIndex(mes);
                    if (idx >=0)
                    {
                        key uu = llList2Key(aviUids, idx);
                        osNpcSaveAppearance(uu, "APP_"+llToLower(mes));
                        llOwnerSay("Updating  APP_"+llToLower(mes) );
                    }
                    else llOwnerSay("Not found "+mes);
                }
                else if (userInputState == "WAIT_REMOVEAVI")
                {
                    doRemoveNpc(mes);
                }

                userInputState="";
            }
        }
    }
    
    
    link_message(integer lnk, integer num, string command, key npc) // This script is in the object too.
    {
        if (num != -1) // -1 means we sent it
        {
            ProcessNPCCommand(command);
        }
    }

    changed(integer change)
    {
        if (change & (CHANGED_REGION_START | CHANGED_OWNER | CHANGED_REGION))
        {
            llResetScript();
        }
    }    

}