TMCP

From Second Life Wiki
Jump to navigation Jump to search

TMCP - Tano's MLP Compatible Pose scripts

  • a drop in replacement for MLPv2
  • configuration notecard compatible
  • Since august 14. 2012 released under the open source BSD license

Collection of scripts as of version 0.37 below.

A much newer version is available through the Marketplace, https://marketplace.secondlife.com/p/TMCP-Developer-Edition/2964601

Resident 'Camden McAndrews' voluntueered as new maintainer. I hope the MLPv2 team will pick it up.


Features

  • MLPv2 notecard & plugin compatible
  • Fast & Stable operation
  • Intelligent menu
  • No poseballs
  • Plenty memory for larger animation sets

Limitations

  • No facial animations (yet)
  • No properties

(Both limitations are relative easily added, yet not implemented in version 0.37)

TMCP Sitmanager

//keeps track of sitted target, position them and animate them.

list animPool; //pool of scripts that are able to pose targets

list poolCache; //try re-use same script for same agent, to avoid reasking permissions

list sitted; //strided, holds: AGENT_KEY, logical offset (`ball number`), [animscript?], current animation?, current position?
integer sittedStride=3;

list sittedAgents;
list sitScripts;

integer MSG_AGENT_UNSIT = 380;
integer MSG_ALL_UNSIT =381;
integer MSG_DISABLE_SITTING =382;
integer MSG_REGISTER_SITSCRIPT =383;
integer MSG_QUERY_SITSCRIPT = 385;
integer MSG_RESET =0;
integer MSG_ENABLE_SITTING=0;
integer MSG_SET_SITTARGET=375;
integer MSG_PLAY_ANIM=390;
integer MSG_STOP_ANIM=391;
integer MSG_AGENT_PLAY_ANIM=395; // to notify the slave script
integer MSG_AGENT_STOP_ANIM=396;
integer MSG_ATTACH_AGENT = 398;
integer MSG_DETACH_AGENT = 399;
integer MSG_RUN_ANIMS=379;

integer MSG_DO_STOP=353;
integer MSG_DO_SWAP=354;
integer MSG_MENU_SWAP=355; 

integer MSG_MENU_META=333;

integer MSG_MLP_SITTED=-11000;
integer MSG_MLP_UNSITTED=-11001;

integer MSG_MODE_BALLUSERS=366;

integer MSG_DO_DEFAULT_POSE = 358;

integer MSG_RLV_GRAB=308;
integer MSG_RLV_RELEASE=309;
integer MSG_RLV_VICTIM_LIST=307;

integer MSG_DO_MENU_CURRENTMENU = 305;






//keys to controlling:
//animname+index is key to both real animation name and it's position.

list agentAnims;

list agentScripts;
list agentScriptCache;

list agentPoseIdx;

list usedIndexes; //logical pose positions, register in here.

integer MAX_SITTING=1;

string currentAnim;
integer currentAnimSeats;

list menuMeta;

integer mode_ballusers;

key grabberID;

list potentionalVictims;
list victimsWithRelay;

list grabbingVictims; //commanded to, but not yet suited
list grabbedVictims; //victims tagged as victim, sitted

integer CAPTURE_RANGE = 35;
integer RELAY_CHANNEL = -1812221819;
integer relay_handle;

integer timer_reason_versioncheck;
integer rlv_victim_dialog_handle;
integer rlv_victim_dialog_channel;

integer timer_versioncheck=50;
integer timer_closechannel=51;
integer timer_closechannel2=53;
integer timer_defaultpose=52;
integer timer_cleargrabbing=55;


assignScript (key id) {
    integer i=llListFindList (agentScripts, [id]);
    if (i>=0) //agent already got assignment
        return;    
    i=llListFindList (agentScriptCache, [id]);
    string candidate;
    if (i>=0) {
        candidate=llList2String(agentScriptCache, i+1);
        if (-1!=llListFindList(agentScripts, [candidate]))
            candidate="";
    }
    if (candidate);else {
        //try to find a new one
        for (i=0; i<llGetListLength(sitScripts); i++) {
            string ss=llList2String(sitScripts, i);
            if (-1==llListFindList(agentScripts, [ss])) {//found free one
                candidate=ss;   
                jump break;
            }
        }
        @break;        
    }
    if (candidate) {
        agentScripts+=[id, candidate];
        i=llListFindList (agentScriptCache, [candidate]);
        if (-1<i) 
            agentScriptCache=llDeleteSubList(agentScriptCache, i-1, i);    
        agentScriptCache+=[id, candidate];
        //llOwnerSay ((string)id+" attached to "+candidate);
        llMessageLinked(LINK_THIS, MSG_ATTACH_AGENT, candidate, id);
    } else {
        llRegionSayTo (id, 0, "Sorry, but there is no available script to animate your avatar.");
    }
}

integer assignPoseIdx (key id) {
    //start counting at 1.. just because we can.
    integer i=llListFindList (agentPoseIdx, [id]);
    if (-1<i)
        return llList2Integer(agentPoseIdx, i+1); //agent already got a pose assigned
    for (i=1; i<100; i++) {
        if (-1==llListFindList(agentPoseIdx, [i])) //found empty one
        {
            agentPoseIdx+=[id, i];   
            //llInstantMessage (id, "You are assigned to pose index "+(string)i);
            return i;
        }
    }
    return 0;
}

dropAgent (key id) {
    //clean up stuff agent had
       
}

stopAnims(key id) {
    llMessageLinked (LINK_THIS, MSG_AGENT_STOP_ANIM, "", id);
}

applyAnims() {
    //apply animation (and position?) for all agents involved
    
    //send list of keys with indexes to position. it should know which anim is current.
    llMessageLinked (LINK_THIS, MSG_RUN_ANIMS, llList2CSV(agentPoseIdx), "");
    //llOwnerSay ("anims/poses "+llList2CSV(agentPoseIdx));
       
}

cleanUp(key id) {
    stopAnims(id);
    integer i=llListFindList (agentScripts, [id]);
    if (-1<i)
        agentScripts = llDeleteSubList (agentScripts, i, i+1);
    i=llListFindList (agentPoseIdx, [id]);
    if (-1<i)
        agentPoseIdx = llDeleteSubList (agentPoseIdx, i, i+1);

    i=llListFindList (grabbedVictims, [id]);
    if (-1<i)
        grabbedVictims = llDeleteSubList (grabbedVictims, i, i);
           
}

setSitTarget() {
  //needs pose info.. so we calculate the first available index, and match info with current pose.. it's for position script, i think.
    //linkedmessage (setsittarget, [posename,] index)..    
    //posename might be cached by positioner and/or set to defaults.
    integer idx=1;
    //find first available
    while (-1<llListFindList(agentPoseIdx, [idx]))
        idx++;
    if (idx>MAX_SITTING) {
        llSitTarget (ZERO_VECTOR, ZERO_ROTATION/llGetRot());   
    } else {
        llSitTarget (<0,0,0.01>, ZERO_ROTATION/llGetRot()); //backup option.
        llMessageLinked(LINK_THIS, MSG_SET_SITTARGET, (string)idx, "");
    }
    
}

integer maySit(key id) {
    if (!mode_ballusers) //everyone
        return TRUE;
    if (id==llGetOwner()) //always
        return TRUE;
    if (mode_ballusers==1) //group
        return llSameGroup(id);
    return FALSE; //nope
}

broadcastVictims() {
    llMessageLinked (LINK_THIS, MSG_RLV_VICTIM_LIST, llList2CSV(grabbedVictims), "");   
}

requestMenu(key id) {
    llMessageLinked (LINK_THIS, MSG_DO_MENU_CURRENTMENU, "", id);      
}

inventarise() {
    //someone sitted or unsitted. do the bookkeeping
    list agents;
    integer i;
    
    //get sitted avatars
    for (i=0; i<=llGetNumberOfPrims(); i++) {
        key id=llGetLinkKey(i);
        if (llGetAgentSize(id)) {
            //this is an agent..
            agents+=id;
            if (-1!=llListFindList(grabbingVictims, [id])) {
                //grabbingVictims=[]; //should really do this somewhere else
                if (-1==llListFindList(grabbedVictims, [id])) {
                    rlv_lock(id);
                    grabbedVictims+=id;
                    broadcastVictims();
                }
            }   
        }
    }
    
    //now cross reference against existing agents
    //first see, who's all gone
    for (i=0;i<llGetListLength(sittedAgents); i++) {
        key id=llList2Key(sittedAgents, i);
        if (-1==llListFindList(agents, [id])) {
            //agent unsitted, remove him
            msgUnsit(id, llList2Integer(agentPoseIdx, llListFindList(agentPoseIdx, [id])+1));
            if (-1!=llListFindList(grabbedVictims, [id])) {
                //rlv catch, unlock it
                rlv_release(id);   
            }
            cleanUp(id);                           
            broadcastVictims();            
        }
    }
    
    //now, lets see who's all added
    for (i=0;i<llGetListLength(agents); i++) {
        key id=llList2Key(agents, i);
        if (-1==llListFindList(sittedAgents, [id])) {
            //freshy added, assign it a script, if possible, and a logical offset
            if (maySit(id)) {
                assignScript(id);
                msgSit(id, assignPoseIdx(id));
            } else {
                llUnSit(id);
                llWhisper (0, "Sorry "+llKey2Name(id)+" but you cannot use this object.");
            }
            //play anim 
            
        }
    }

//    if (sittedAgents != agents)
//        setSitTarget();    
    sittedAgents = agents;
    setSitTarget();
    applyAnims();
    
    // default pose switching on idle
    if (agents==[])
        scheduleEvent (timer_defaultpose, 120, "");
    else
        cancelEvent (timer_defaultpose);
}

makeSwapMenu(key id, integer channel) {
    //offer nice swap menu
    //list sitted avatars
    string text="";
    integer i;
    integer m=10;
    list buttons=["<BACK"];
    if (MAX_SITTING<m)
        m=MAX_SITTING;
    for (i=1; i<=m; i++) {
        integer j=llListFindList(agentPoseIdx, [i]);
        string agent = "<Nobody>";
        key aid;
        if (-1<j) {
            aid=llList2Key(agentPoseIdx, j-1);
            agent=llKey2Name(aid);
        }
        text+=(string)i + ". " + llList2String(menuMeta, i) + " "+agent+"\n";
                   
        if (aid!=id)
            buttons+=["SWAP::"+(string)i];
    }
    llDialog (id, text, buttons, channel);       
}

//consideration: send out sit/unsit events or not.. i think, might as well not. in order not to confuse rlv.
swapAgent (key id, integer newpos) {
    //llSay (0, "Swapping "+llKey2Name(id)+" to position #"+(string)newpos);
    integer o=llListFindList(agentPoseIdx, [id]);
    if (o<0) {
        //llOwnerSay ("swap key not found "+(string)id);
        //agent not found. see if we have a rlv target
        if (llGetListLength(grabbedVictims)) {
            //just default to first best sitted. Stuff gets complex with multiple RLV but who cares that.
            id=llList2Key(grabbedVictims,0);
            o=llListFindList(agentPoseIdx, [id]);
            if (o<0) //OUCH, should nevah happen
                return;            
        }        
        else return;
    }
    integer oldidx=llList2Integer(agentPoseIdx, o+1);
    if (oldidx==newpos) {
        llRegionSayTo (id, 0, "You choosen the same position");
        return;
    }
    agentPoseIdx=llDeleteSubList(agentPoseIdx, o, o+1);
    integer topos=llListFindList(agentPoseIdx, [newpos]);
    if (-1<topos) {
        agentPoseIdx=llDeleteSubList(agentPoseIdx, topos-1, topos) + [llList2Key(agentPoseIdx, topos-1),oldidx];        
    }
    agentPoseIdx += [id, newpos];
    applyAnims();
}

unsitAll() {
    integer i;
    for (i=0; i<=llGetNumberOfPrims(); i++) {
        key id=llGetLinkKey(i);
        if (llGetAgentSize(id))
            llUnSit(id);    
    }   
}

msgSit(key id, integer poseidx) {
    //we start count at 1, mlp at zero. fix right here
    llMessageLinked(LINK_THIS, MSG_MLP_SITTED, (string)(poseidx-1), id);   
}

msgUnsit(key id, integer poseidx) {
    //we start count at 1, mlp at zero. fix right here
    llMessageLinked(LINK_THIS, MSG_MLP_UNSITTED, (string)(poseidx-1), id);   
}

log(string t){
    //llOwnerSay(t);
}

RLVGrabMenu(key id) {
    grabberID = id;
    llSensor("", NULL_KEY, AGENT, CAPTURE_RANGE, TWO_PI);    
//    llInstantMessage (id, "Scanning ... menu in 5.. 4.. 3..");
}

RLVReleaseAll() {
    integer i;
    for (i=0; i<llGetListLength(grabbedVictims); i++) {
        key id=llList2Key(grabbedVictims,i);
        rlv_release (id);
        llWhisper (0,"releasing "+llKey2Name(id));
        //unsit?
        llUnSit(id);
    }
    grabbedVictims=[];
}

rlv_checkversion (key id) {
    rlv_relay(id, "!version=" + (string)1);    //version_handle
}

 
rlv_capture(key avatar)
{
    rlv_relay(avatar, "@sit:" + (string)llGetKey() + "=force");
    //llSay (menu_ban_channel, "- "+(string)avatar);    
    llWhisper (0, llKey2Name(grabberID)+" captures "+llKey2Name(avatar));
}

 
rlv_lock(key avatar)
{
    rlv_relay(avatar, "@unsit=n");
}
 
rlv_release(key avatar)
{
    rlv_relay(avatar, "@unsit=y");
    rlv_relay(avatar, "!release");
    //llSay (menu_ban_channel, "- "+(string)avatar);    
}
 
// write a message to the RLV Relay
rlv_relay(key avatar, string message)
{
    if (avatar != NULL_KEY)
    {
        llSay(RELAY_CHANNEL, llEscapeURL(llGetObjectName()) + "," + (string) avatar + "," + message);
        log("RLV: " + llGetObjectName() + "," + (string) avatar + "," + message);
    }
}

//scheduler

list events = [];
integer nextEvent = 0;

scheduleEvent(integer id, integer time, string data) {
    //adjust timestamp
    time+=llGetUnixTime();
    
    //cancel any previous requests, this case we only want one per type, really
    cancelEvent(id);    
    
    events = llListSort(events + [time, id, data], 3, TRUE);
    setTimer(FALSE);
}

cancelEvent(integer id) {
    //reasonable failsafe as long as you don't use id's that could be current timestamp.    
    integer i=llListFindList(events, [id]);
    if ((-1<i) && ((i % 3) == 1)) {
        //it was an id
        events=llDeleteSubList(events, i-1, i+1);
        
        //recurse when found, delete all with said id... ?
        cancelEvent(id);
        
        setTimer(FALSE);
    }
}

integer setTimer(integer executing) {
    if ((events != []) > 0) { // Are there any list items?
        integer id = llList2Integer(events, 1);
 
        integer time = llList2Integer(events, 0);
        nextEvent = id;
 
        float t = (float)(time - llGetUnixTime());
        if (t <= 0.0) {
            if (executing) return TRUE;
            else t = 0.01;
        }
        llSetTimerEvent(t);
    } else {
        llSetTimerEvent(0.0);
        nextEvent = -1;
    }
    return FALSE;
}

handleEvent(integer id, string data) {

    if (id==timer_versioncheck) {
            //time to display our dialog
            if ([]==victimsWithRelay) {
                llRegionSayTo (grabberID, 0, "No eligible victims found (they must wear a relay).");
                requestMenu(grabberID);
            } else {
                list nn;
                integer i;
                for (i=0; (i<llGetListLength(victimsWithRelay)) && (i<23); i+=2) {
                    string s=llKey2Name(llList2Key(victimsWithRelay, i));
                    if (s)
                        nn+=llGetSubString(s,0,23);                     
                }
                
                llListenControl(rlv_victim_dialog_handle, TRUE);          
                scheduleEvent (timer_closechannel, 90, (string)rlv_victim_dialog_handle);
                llDialog (grabberID, "Choose a victim", nn, rlv_victim_dialog_channel);
                //scheduleEvent (timer_versioncheck, 5, "");                 
            }  

            //set timer to new timeout
            
            //llSetTimerEvent(TIMER_TIMEOUT);
            //llListenControl (version_handle, FALSE);            
            //return;   
    }
    else
    if ((id==timer_closechannel) || (id==timer_closechannel2)) {
        llListenControl ((integer)data, FALSE);            
    }
    else
    if (id==timer_defaultpose) {
        llMessageLinked(LINK_THIS, MSG_DO_DEFAULT_POSE, "", "");        
    }
    else
    if (id==timer_cleargrabbing) {
        grabbingVictims=[];   
    }
    
}


upgrade() {string self = llGetScriptName(); string basename = self; if (llSubStringIndex(self, " ") >= 0) {integer start = 2; string tail = llGetSubString(self, llStringLength(self) - start, -1); while (llGetSubString(tail, 0, 0) != " ") {start++; tail = llGetSubString(self, llStringLength(self) - start, -1);} if ((integer)tail > 0) {basename = llGetSubString(self, 0, -llStringLength(tail) - 1);}} integer n = llGetInventoryNumber(INVENTORY_SCRIPT); while (n-- > 0) {string item = llGetInventoryName(INVENTORY_SCRIPT, n); if (item != self && 0 == llSubStringIndex(item, basename)) {llRemoveInventory(item);}}}

default
{
    state_entry()
    {
        upgrade();
        llOwnerSay ("sitmanager memory free : "+(string)llGetFreeMemory());
        state active;  
    }
}

state active {   
    state_entry() {
        setSitTarget();
        llMessageLinked (LINK_THIS, MSG_QUERY_SITSCRIPT, "", "");
        
        rlv_victim_dialog_channel = 124904342+llFloor(llFrand(42902543));
        rlv_victim_dialog_handle = llListen(rlv_victim_dialog_channel, "", "", "");
        llListenControl (rlv_victim_dialog_handle, FALSE); 
        relay_handle=llListen(RELAY_CHANNEL, "", "", "");
        llListenControl(relay_handle, FALSE);   
    } 
    link_message(integer sn, integer n, string m, key id) {
        
        if (n==MSG_RESET)
            llResetScript();
        else
        if (n==MSG_ENABLE_SITTING) {
            MAX_SITTING=llGetListLength(sitScripts);
            setSitTarget();
        }
        else
        if (n==MSG_AGENT_UNSIT) {
            //unsit agent                
            llUnSit(id);
        }
        else
        if (n==MSG_ALL_UNSIT) {
            //just kick everyone
            unsitAll();    
        }
        else
        if (n==MSG_DISABLE_SITTING) {
            //state inactive;
            MAX_SITTING=0;
            setSitTarget();
        }        
        
        else
        if (n==MSG_REGISTER_SITSCRIPT) {
            if (m)
            if (INVENTORY_SCRIPT==llGetInventoryType(m))
            if (-1==llListFindList(sitScripts, [m])) {
                sitScripts+=m;    
                MAX_SITTING=llGetListLength(sitScripts);
            }
        }
        
        else
        if (n==MSG_PLAY_ANIM) {
            currentAnim=m;
            applyAnims();   
        }
        else
        if (n==MSG_MENU_SWAP) {
            makeSwapMenu (id, (integer)m);   
        }
        else
        if (n==MSG_DO_SWAP) {
            swapAgent(id, (integer)m);   
        }
        else
        if (n==MSG_MENU_META) {
            menuMeta=llParseString2List(m, ["|", " "], []);   
        }
        else
        if (n==MSG_DO_STOP) {
            llUnSit(id); //let changed event handle the rest   
        }
        else
        if (n==MSG_MODE_BALLUSERS) {
            mode_ballusers=(integer)m;   
        }
        else
        if (n==MSG_RLV_GRAB) {
            RLVGrabMenu(id);       
        }
        else
        if (n==MSG_RLV_RELEASE) {
            RLVReleaseAll();
        }
        
    }
    
    changed (integer c) {
        if (c & CHANGED_LINK) {
            //don't even bother about getting sitted or not or leaving..
            //just fetch all current avatars and match them against our list of known avis
            inventarise();   
        }
        
    }
    
    timer() {
        // Clear timer or it might fire again before we're done
        llSetTimerEvent(0.0);
 
        do {
            // Fire the event
            handleEvent(nextEvent, llList2String(events, 2));
 
            // Get rid of the first item as we've executed it
            integer l = events != [];
            if (l > 0) {
                if (l > 3)
                    events = llList2List((events = []) + events, 3, -1);
                else events = [];
            }
 
            // Prepare the timer for the next event
        } while (setTimer(TRUE));
    }    
    
    /*
    timer() {
        /* default pose.. do somewhere else please
        llSetTimerEvent(0.);
        llMessageLinked(LINK_THIS, MSG_DO_DEFAULT_POSE, "", "");   
        */
        /*
    }
    */
    
    no_sensor()
    {        
        llRegionSayTo (grabberID, 0, "No nearby potentional victims found");
        requestMenu(grabberID);
    }
 
    // some av in sensor range
    sensor(integer total_number)
    {
        //llListenControl (rlv_victim_dialog_handle, TRUE);
        //scheduleEvent (timer_closechannel, 120, (string)rlv_victim_dialog_handle);
        llListenControl (relay_handle, TRUE);
        scheduleEvent (timer_closechannel2, 7, (string)relay_handle);
        
        
        potentionalVictims=[];
        victimsWithRelay=[];
        integer i;
        for(i=0; i < total_number; i++)
        {
            key id = llDetectedKey(i);
            string name = llKey2Name(id);
            if (llStringLength(name) > 24)
            {
                name = llGetSubString(name, 0, 23);
            }
            potentionalVictims+=[id, name];
            rlv_checkversion(id);            
        }
 
        // show dialog if list contains names
        /*
        if (llGetListLength(sensor_names) > 0)
        {
            llListenControl(dialog_handle, TRUE);
            llSetTimerEvent(TIMER_TIMEOUT);
            llDialog(avatar_menu, MSG_CHOOSE_AV, sensor_names, dialog_channel);
        }
        */
        scheduleEvent (timer_versioncheck, 5, (string)grabberID);
        llRegionSayTo (grabberID, 0, "Verifying "+(string)total_number+" potentional victims. Menu in 5.. 4.. 3..");          }    
    
    listen (integer ch, string n, key id, string m) {
        if (ch==RELAY_CHANNEL) {
            if (-1!=llSubStringIndex(m, "version=")) {//bit dirty, we might catch other objects too
                //victims+=llGetOwnerKey(id);
                //victim selected, grab it   
                victimsWithRelay+=[llGetOwnerKey(id),llGetSubString(llKey2Name(llGetOwnerKey(id)),0,23)];                
            }            
        }
        else
        if (ch==rlv_victim_dialog_channel) {
            //victim selected, grab it
            integer i=llListFindList (victimsWithRelay, [m]);
            if (-1<i) {
                key vid=llList2Key(victimsWithRelay, i-1);
                rlv_capture (vid);
                if (-1==llListFindList(grabbingVictims, [vid])) {
                    grabbingVictims+=vid;
                }
                scheduleEvent (timer_cleargrabbing, 120, (string)id);                
            }
            requestMenu(grabberID);   
        }
        
    }
}

/*
state inactive {
    state_entry() {
        llSitTarget(ZERO_VECTOR, ZERO_ROTATION);
    }   
    link_message(integer sn, integer n, string m, key id) {
        if (n==MSG_RESET)
            llResetScript();
        else
        if (n=MSG_ENABLE_SITTING)
            state active;
    }    
}
*/

TMCP - pose

key ourAgent;
string currentAnim;

integer hasPerms;

integer MSG_AGENT_PLAY_ANIM=395; // to notify the slave script
integer MSG_AGENT_STOP_ANIM=396;
integer MSG_ATTACH_AGENT = 398;
integer MSG_DETACH_AGENT = 399;
integer MSG_REGISTER_SITSCRIPT =383;
integer MSG_QUERY_SITSCRIPT = 385;
integer MSG_SYNC_ANIM=361;

integer MSG_ADJUST_POSROT=520;
integer MSG_TEMPORARY_POSROT=521;

integer MSG_MODE_ADJUST=368;


vector o=ZERO_VECTOR;
vector r=ZERO_VECTOR;
integer hastimer;

integer is_adjusting;
integer cursormode;


stopAnim() {
    if (hasPerms) if (currentAnim)
        llStopAnimation(currentAnim);   
}

runAnim() {
    if (hasPerms) if (currentAnim)
        llStartAnimation(currentAnim);
}

integer checkPerms() {
    if (llGetPermissionsKey()!=ourAgent) {
        hasPerms = FALSE;
        return FALSE;   
    }
    hasPerms=(PERMISSION_TAKE_CONTROLS|PERMISSION_TRIGGER_ANIMATION) == ((PERMISSION_TAKE_CONTROLS|PERMISSION_TRIGGER_ANIMATION) & llGetPermissions());
    return hasPerms;
}

queryPerms() {
    if (!checkPerms()) {
        //must query them
        llRequestPermissions(ourAgent, PERMISSION_TAKE_CONTROLS|PERMISSION_TRIGGER_ANIMATION);   
    } else {
        stopDefAnims();
        if (currentAnim)
            llStartAnimation(currentAnim);
        getControls();   
    }
}

stopDefAnims() {
    //easy way would be:
    //llStopAnimation("sit");   
    //let's go for the hard way
    list a=llGetAnimationList(ourAgent);
    integer i;
    for (i=0; i<llGetListLength(a); i++)
        llStopAnimation(llList2Key(a,i));
}

register() {
    llMessageLinked (LINK_THIS, MSG_REGISTER_SITSCRIPT, llGetScriptName(), "");   
}

getControls() {
    llTakeControls (CONTROL_UP|CONTROL_DOWN|CONTROL_LEFT|CONTROL_RIGHT|CONTROL_FWD|CONTROL_BACK|CONTROL_ROT_LEFT|CONTROL_ROT_RIGHT, TRUE, FALSE);
    //llInstantMessage (ourAgent, "Adjust your position with PGUP, PGDOWN and cursorkeys. Shift rotates.");   
    whispHelp(ourAgent);
}

parseControls (integer level, integer edge) {

    integer l = level; //hold down
    
    if ((CONTROL_UP|CONTROL_DOWN)==(l & edge & (CONTROL_UP | CONTROL_DOWN))) {
        cursormode=!cursormode;
        whispHelp(ourAgent);
        return;   
    }
    
    if (is_adjusting) {
        integer cm=cursormode;
        if (l & edge & CONTROL_LEFT) 
            cursormode--;
        if (l & edge & CONTROL_RIGHT)
            cursormode++;
        if (cursormode<1)
            cursormode=3;
        if (cursormode>3)
            cursormode=1;
        if (cm!=cursormode)
            whispHelp(ourAgent);        
    }

    
    if (cursormode==1) {//fine adjust    
        if (l & CONTROL_FWD) o+=<0,2,0>;
        if (l & CONTROL_BACK) o+=<0,-2,0>;
        if (l & CONTROL_ROT_LEFT) o+=<-2,0,0>;
        if (l & CONTROL_ROT_RIGHT) o+=<2,0,0>;
        if (l & CONTROL_UP) o+=<0,0,2>;
        if (l & CONTROL_DOWN) o+=<0,0,-2>;
    }
    else
    if (cursormode==2) {//90 degrees rotation    
        if (l & edge & CONTROL_FWD) r+=<0,90,0>;
        if (l & edge & CONTROL_BACK) r+=<0,-90,0>;
        if (l & edge & CONTROL_ROT_LEFT) r+=<0,0,-90>;
        if (l & edge & CONTROL_ROT_RIGHT) r+=<0,0,90>;
        if (l & edge & CONTROL_UP) r+=<90,0,0>;
        if (l & edge & CONTROL_DOWN) r+=<-90,-0,0>;
    }
    else
    if (cursormode==3) {//fine rotation    
        if (l & CONTROL_FWD) r+=<0,1,0>;
        if (l & CONTROL_BACK) r+=<0,-1,0>; 
        if (l & CONTROL_ROT_LEFT) r+=<0,0,-1>;
        if (l & CONTROL_ROT_RIGHT) r+=<0,0,1>;
        if (l & CONTROL_UP) r+=<1,0,0>;
        if (l & CONTROL_DOWN) r+=<-1,0,0>;
    }
    
    //if (l & edge & CONTROL_LEFT) r += <0,0,90>;
    //if (l & edge & CONTROL_RIGHT) r += <0,-90,0>;    
        
    if (!hastimer) {
        llSetTimerEvent (0.25);
        hastimer=TRUE;
    }
}

whispHelp (key id) {
    if (!is_adjusting) {
        if (!cursormode) {
            llRegionSayTo (id, 0, "Press PAGE-UP and PAGE-DOWN simultanious to fine adjust position");
        } else {
            llRegionSayTo (id, 0, "Use the cursor keys and up/down to adjust position. Changes made are temporary.");
        }        
    } else {
        if (!cursormode)
            llRegionSayTo (id, 0, "Use PAGE-UP and PAGE-DOWN to start adjusting");
        if (1==cursormode)
            llRegionSayTo (id, 0, "Mode: position (fine adjust). Use cursor keys and up/down to adjust position. Shift-left or shift-right to change mode");
        if (2==cursormode)
            llRegionSayTo (id, 0, "Mode: rotation (90 degrees) . Use cursor keys and up/down to adjust rotation. Shift-left or shift-right to change mode");
        if (3==cursormode)
            llRegionSayTo (id, 0, "Mode: rotation (fine adjust). Use cursor keys and up/down to adjust rotation. Shift-left or shift-right to change mode");
    }
}

upgrade() {string self = llGetScriptName(); string basename = self; if (llSubStringIndex(self, " ") >= 0) {integer start = 2; string tail = llGetSubString(self, llStringLength(self) - start, -1); while (llGetSubString(tail, 0, 0) != " ") {start++; tail = llGetSubString(self, llStringLength(self) - start, -1);} if ((integer)tail > 0) {basename = llGetSubString(self, 0, -llStringLength(tail) - 1);}} integer n = llGetInventoryNumber(INVENTORY_SCRIPT); while (n-- > 0) {string item = llGetInventoryName(INVENTORY_SCRIPT, n); if (item != self && 0 == llSubStringIndex(item, basename)) {llRemoveInventory(item);}}}


default
{
    state_entry()
    {
        //upgrade();
        register();
    }

    link_message(integer sn, integer n, string m, key id) {
        if (n==MSG_ATTACH_AGENT) {
            if (m==llGetScriptName()) {
                if (ourAgent)
                    stopAnim();
                currentAnim="";   
                ourAgent = id;
                queryPerms();
            }
        }
        else
        if (n==MSG_DETACH_AGENT) {
            if (ourAgent==id) {
                llReleaseControls();
                stopAnim();
            }
            ourAgent=NULL_KEY;
            currentAnim="";   
        }
        else
        if (n==MSG_AGENT_PLAY_ANIM) {
            if (ourAgent==id) {
                stopAnim();
                currentAnim=m;
                runAnim();
            }
        }
        else
        if (n==MSG_AGENT_STOP_ANIM) {
            if (ourAgent==id) {
                stopAnim();
                currentAnim="";    
            }   
        }
        else
        if (n==MSG_SYNC_ANIM) {
            if (ourAgent) {
                stopAnim();
                runAnim();
            }   
        }
        else
        if (n==MSG_QUERY_SITSCRIPT) {
            register();
            //llOwnerSay ("Registering");   
        }
        else
        if (n==MSG_MODE_ADJUST) {
            is_adjusting = (integer)m;
            if (!is_adjusting)
                cursormode=0;
            else
                cursormode=1;
            if (id==ourAgent)
                whispHelp(id);   
        }

    }
    
    run_time_permissions(integer p) {
        if (checkPerms()) {
            stopDefAnims();
            if (currentAnim)
                llStartAnimation(currentAnim);   
            getControls();
        } 
    }
    
    control (key id, integer level, integer edge) {
        if (id!=ourAgent)
            return;
        parseControls (level, edge);   
    }
    
    timer () {
        llSetTimerEvent (0.);
        hastimer=FALSE;
        if (ZERO_VECTOR!=(o+r)) {
            if (is_adjusting)
                llMessageLinked (LINK_THIS, MSG_ADJUST_POSROT, (string)(0.005*o)+"&"+(string)r, ourAgent);    
            else
                llMessageLinked (LINK_THIS, MSG_TEMPORARY_POSROT, (string)(0.005*o)+"&"+(string)r, ourAgent);            
        }
        o=ZERO_VECTOR;
        r=ZERO_VECTOR;
        
    }
    
}

TMCP - positions

//basically, we need a large datastore for our command set
list poseNames;
list poseData;

list positionNames;
list positionData;


integer MSG_DATA_MENU=310;
integer MSG_DATA_TYPE=320;
integer MSG_DATA_POSE=330;
integer MSG_DATA_POSITION=331;
integer MSG_DATA_LM=335;

integer MSG_DO_POSE=350;
integer MSG_DO_LM=351;
integer MSG_DO_SPECIAL=352;

integer MSG_SET_RUNNING=390;
integer MSG_STORAGE_RESET=391;

integer MSG_DATA_READY=309;

integer MSG_RUN_ANIMS=379;

integer MSG_AGENT_PLAY_ANIM=395;

integer MSG_SET_SITTARGET=375;

integer MSG_DUMP_POSITIONS=389;
integer MSG_UPDATE_POS=363;

integer MSG_DO_DEFAULT_POSE = 358;
integer MSG_SET_DEFAULT_POSE = 359;

integer MSG_MLP_SETTING = 1;

integer MSG_ADJUST_POSROT=520;
integer MSG_TEMPORARY_POSROT=521;



string currentPose="default";
list currentAnims;
list currentPositions;

list agentPoseIdx;

list agentAdjust;


string defaultPose="default";

runAnims() {
    list data=agentPoseIdx;
    integer i;
    list idpos;
    for (i=0; i<llGetListLength(data); i+=2) {
        key id=(key)llList2String(data, i);
        integer idx=-1+(integer)llList2String(data, i+1);
        if (id) if (idx>=0) {
            if (idx>=llGetListLength(currentAnims))
                llWhisper (0, "No animation available for "+llKey2Name(id));
            else {
                llMessageLinked (LINK_THIS, MSG_AGENT_PLAY_ANIM, llList2String(currentAnims, idx), id);
                //llOwnerSay ("Playing anim "+llList2String(currentAnims, idx)+" on id "+(string)id);
            }
                
            if (idx/2>=llGetListLength(currentPositions))
                llSay (0, "No position data for "+llKey2Name(id));   
            else {
                //set agents position
                idpos+=[id, (vector)llList2String(currentPositions, idx*2),(vector)llList2String(currentPositions, idx*2+1)];
            }
        } else {
            //llOwnerSay ("Corrupt agent anim position request");   
        }
    }    
    //apply positions
    for (i=0; i<=llGetNumberOfPrims(); i++) {
        integer j=llListFindList(idpos, [llGetLinkKey(i)]);
        if (-1<j) {
            vector pos = llList2Vector(idpos, j+1)+(<0,0,0.42+0.01*(integer)llGetObjectDesc()>/llGetRot());
            vector rot = llList2Vector(idpos, j+2) * DEG_TO_RAD;
            integer l=llListFindList (agentAdjust, [llGetLinkKey(i)]);
            if (-1<l) {
                pos+=llList2Vector (agentAdjust, l+1);
                rot=llRot2Euler(llEuler2Rot(llList2Vector (agentAdjust, l+2)) * llEuler2Rot(rot));                   
            }
            llSetLinkPrimitiveParamsFast(i, [PRIM_POS_LOCAL, pos,PRIM_ROT_LOCAL, llEuler2Rot(rot)]);
            //llOwnerSay ("Setting position for "+llKey2Name(llGetLinkKey(i))+" position: "+(string)pos+" rotation: "+ (string)rot );
        }        
    }
}

setSitTarget(integer poseidx) {
    poseidx--;
    vector v=(vector)llList2String(currentPositions, poseidx*2);
    vector r=(vector)llList2String(currentPositions, poseidx*2+1);
    llSitTarget (v+(<0,0,0.42+0.01*(integer)llGetObjectDesc()>/llGetRot()) , llEuler2Rot(r*DEG_TO_RAD));
    //llOwnerSay ("Sit target #"+(string)poseidx+" "+(string)(v+(<0,0,0.42>/llGetRot()))+" "+(string)(r));
}

string shortString (float n) {
    string s=(string)n;
    while (llStringLength(s)>1) {
        integer hasdot=-1!=llSubStringIndex(s, ".");
        string d=llGetSubString(s,-1,-1);
        if ((hasdot && (d=="0")) || (d=="."))
            s=llDeleteSubString(s,-1,-1);
        else
            jump ok;   
    }
    @ok;
    return s;
}

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

dumpPos() {
    llOwnerSay ("TMCP Position Dump");
    integer i;
    for (i=0; i<llGetListLength(positionNames); i++) {
        //llOwnerSay ("{"+llList2String(positionNames, i)+"} "+llList2String(positionData, i));    
        list v=llParseString2List(llList2String(positionData, i), ["&"], []);
        string s="{"+llList2String(positionNames, i)+"}";
        integer j;
        for (j=0; j<llGetListLength(v); j++)
            //notice that function shortvec takes string as param
            s+=" "+shortVec((vector)llList2String(v, j));        
        llOwnerSay (s);
    }   
}

doPose (string m) {
    if (m!=currentPose)
        agentAdjust=[];
        
            currentPose=m;
            currentAnims=[];
            currentPositions=[];
            integer i=llListFindList(positionNames, [m]);
            integer j=llListFindList(poseNames, [m]);
            currentAnims=llParseString2List (llList2String(poseData, j), ["|"], []);
            currentPositions=llParseString2List (llList2String(positionData, i), ["&"], []);
            
            //fill missing data with defaults
            i = llListFindList(positionNames, ["default"]);
            j = llListFindList(poseNames, ["default"]);
            list animdef=llParseString2List (llList2String(poseData, j), ["|"], []);
            list posdef=llParseString2List (llList2String(positionData, i), ["&"], []);
            currentAnims+=llList2List (animdef, llGetListLength(currentAnims), -1);
            currentPositions+=llList2List(posdef, llGetListLength(currentPositions), -1);            
            
            //this should be obsolete now:
            if (/*(i>=0) && */(j>=0)) {
                //list l=llParseString2List(llList2String(lmAction, i), [","],[]);
                //parse pose data. todo.
                if (i<0) {
                    //need to fetch default data... just try it.
                    i=llListFindList(positionNames, ["default"]);
                }
                //llOwnerSay ("Do pose "+m+" anims "+llList2String(poseData,j)+" data "+llList2String(positionData, i));
            } else {                                
                //llOwnerSay ("No animations defined for "+m);
            }
            
            runAnims();  
}

setAgentPosRot (key id, string data) {
    //set temporary agent adjustment.
    list l=llParseString2List(data, ["&"], []);
    vector p=(vector)llList2String(l,0);
    vector r=DEG_TO_RAD * (vector)llList2String(l,1);
    integer i=llListFindList (agentAdjust, [id]);
    if (-1<i) {
        p+=llList2Vector(agentAdjust, i+1);
        r=llRot2Euler(llEuler2Rot(llList2Vector(agentAdjust, i+2))*llEuler2Rot(r));        
        agentAdjust = llDeleteSubList(agentAdjust, i, i+2);
    }
    if (llVecMag(p)>1)
        p=llVecNorm(p);
    agentAdjust+=[id,p,r];       
    runAnims();
}

adjustPosition (string name, integer index, vector p, vector r) {
                              //positionNames
    integer i=llListFindList (positionNames, [name]); 
    if (-1<i) {
        string data=llList2String(positionData, i);
        list pos=llParseString2List(data, ["&"], []);
        if (index>0) {
            //alter only index   
            integer j=(index-1)*2;
            if (j<llGetListLength(positionData)) {
                vector v=(vector)llList2String(pos, j);   
                v+=p;
                vector R0=(vector)llList2String(pos, j+1);   
                rotation R1=llEuler2Rot(DEG_TO_RAD *R0);
                rotation Rd=llEuler2Rot(DEG_TO_RAD * r);
                R1 = R1 * Rd;
                R0=RAD_TO_DEG * llRot2Euler(R1);                
                pos=llListReplaceList(pos, [shortVec(v),shortVec(R0)], j, j+1);                             
            }
            
        } else {
            //alter all
            integer j;
            for (j=0; j<llGetListLength(pos); j+=2) {
                vector v=(vector)llList2String(pos, j);   
                v+=p;
                vector R0=(vector)llList2String(pos, j+1);   
                rotation R1=llEuler2Rot(DEG_TO_RAD *R0);
                rotation Rd=llEuler2Rot(DEG_TO_RAD * r);
                R1 = R1 * Rd;
                R0=RAD_TO_DEG * llRot2Euler(R1);                
                pos=llListReplaceList(pos, [shortVec(v),shortVec(R0)], j, j+1);                
            }               
        }
        data=llDumpList2String(pos, "&");
        //llOwnerSay ("new pos data for "+name+" : "+data);
        positionData=llListReplaceList (positionData, [data], i, i);
           
    }
}

adjustAllPositions (vector p, vector r) {
    //loop all pose, adjust them
    integer i;
    for (i=0; i<llGetListLength(positionNames); i++) {
        adjustPosition (llList2String(positionNames, i),-1, p,r);
    }   
}

parseSetting (string m, key id) {
    //seperated by =
    //might contain ROT or POS adjustments
    list l=llParseString2List(m, [" = ", " =", "=", "= ","="],[]);
    string cmd=llList2String(l, 0);
    string cmd2=llList2String(l,1);
    if (cmd=="REORIENT") {
        vector v=(vector)llList2String(l,2);
        /*
        list lv=llParseString2List(llList2String(l,2), ["<",",",">"], []); //rotations come as vector too
        llOwnerSay ("P: "+llList2String(l,2)+" : "+llDumpList2String(lv, "*"));
        v.x=(float)llList2String(lv,0);
        v.y=(float)llList2String(lv,1);
        v.z=(float)llList2String(lv,2);
        */
        
        llOwnerSay ("adjust "+cmd2+" "+(string)v);
        if (cmd2=="OFF") {
            adjustAllPositions (0.01*v, ZERO_VECTOR);    
        } else
        if (cmd2=="ROT") {
            adjustAllPositions (ZERO_VECTOR,v);    
        }
        
        doPose(currentPose);
        //runAnims();
    }    
}

adjustPosRot (key id, string data) {
    list l=llParseString2List(data, ["&"], []);
    integer i=llListFindList (agentPoseIdx, [(string)id]);
    //llOwnerSay ("posrot "+(string)i+" "+(string)id+" "+data);
    if ((-1<i)&&(llGetListLength(l)==2)) {
        integer lidx=llList2Integer (agentPoseIdx, i+1);
        adjustPosition (currentPose, lidx, (vector)llList2String(l,0), (vector)llList2String(l,1));
        doPose(currentPose);        
    }
}


upgrade() {string self = llGetScriptName(); string basename = self; if (llSubStringIndex(self, " ") >= 0) {integer start = 2; string tail = llGetSubString(self, llStringLength(self) - start, -1); while (llGetSubString(tail, 0, 0) != " ") {start++; tail = llGetSubString(self, llStringLength(self) - start, -1);} if ((integer)tail > 0) {basename = llGetSubString(self, 0, -llStringLength(tail) - 1);}} integer n = llGetInventoryNumber(INVENTORY_SCRIPT); while (n-- > 0) {string item = llGetInventoryName(INVENTORY_SCRIPT, n); if (item != self && 0 == llSubStringIndex(item, basename)) {llRemoveInventory(item);}}}

default 
{
    state_entry()
    {
        upgrade();
    }

    link_message (integer sn, integer n, string m, key id) {
        if (n==MSG_DATA_POSE) {
            //add posedata    
            poseNames+=m;
            poseData+=(string)id;
        } else
        if (n==MSG_DATA_POSITION) {
            positionNames+=m;
            positionData+=(string)id;            
        } else
        if (n==MSG_SET_DEFAULT_POSE) {
                defaultPose=m;
        }
        else
        if (n==MSG_SET_RUNNING)
            state running;
    }
}

state running {
    state_entry()
    {
        llOwnerSay ("Position free memory: "+(string)llGetFreeMemory());
        doPose(defaultPose);
    }
    link_message (integer sn, integer n, string m, key id) {
        if (n==MSG_DO_POSE) {
            doPose(m);
        } else
        if (n==MSG_STORAGE_RESET)
            llResetScript();
        else
        if (n==MSG_RUN_ANIMS) {
            agentPoseIdx=llCSV2List(m);
            runAnims();    
        }
        else
        if (n==MSG_SET_SITTARGET) {
            setSitTarget((integer)m);    
        }
        else
        if (n==MSG_DUMP_POSITIONS) {
            dumpPos();
        }
        else
        if (n==MSG_UPDATE_POS) {
            runAnims();       
        }
        else
        if (n==MSG_SET_DEFAULT_POSE)
            defaultPose=m;
        else
        if (n==MSG_DO_DEFAULT_POSE)
            doPose(defaultPose);
        else
        if (n==MSG_MLP_SETTING) {
            parseSetting (m, id);   
        }
        else
        if (n==MSG_ADJUST_POSROT) {
            adjustPosRot(id, m);      
        }
        if (n==MSG_TEMPORARY_POSROT) {
            setAgentPosRot(id, m);      
        }

    }
    
}

TMCP - actions

//basically, we need a large datastore for our command set
list lmNames;
list lmAction;

integer MSG_DATA_MENU=310;
integer MSG_DATA_TYPE=320;
integer MSG_DATA_POSE=330;
integer MSG_DATA_LM=335;

integer MSG_DO_POSE=350;
integer MSG_DO_LM=351;
integer MSG_DO_SPECIAL=352;

integer MSG_DO_SOUND=356;

integer MSG_SET_RUNNING=390;
integer MSG_STORAGE_RESET=391;
integer MSG_DATA_READY=309;



upgrade() {string self = llGetScriptName(); string basename = self; if (llSubStringIndex(self, " ") >= 0) {integer start = 2; string tail = llGetSubString(self, llStringLength(self) - start, -1); while (llGetSubString(tail, 0, 0) != " ") {start++; tail = llGetSubString(self, llStringLength(self) - start, -1);} if ((integer)tail > 0) {basename = llGetSubString(self, 0, -llStringLength(tail) - 1);}} integer n = llGetInventoryNumber(INVENTORY_SCRIPT); while (n-- > 0) {string item = llGetInventoryName(INVENTORY_SCRIPT, n); if (item != self && 0 == llSubStringIndex(item, basename)) {llRemoveInventory(item);}}}

default
{
    state_entry()
    {
        upgrade();
    }

    link_message (integer sn, integer n, string m, key id) {
        if (n==MSG_DATA_LM) {
            //add action    
            lmNames+=m;
            lmAction+=(string)id;
        } else
        if (n==MSG_SET_RUNNING)
            state running;
    }
}

state running {
    state_entry()
    {
        llOwnerSay ("Action manager free memory : "+(string)llGetFreeMemory());    
    }    
    link_message (integer sn, integer n, string m, key id) {
        if (n==MSG_DO_LM) {
            integer i=llListFindList(lmNames, [m]);
            if (i>=0) {
                list l=llParseString2List(llList2String(lmAction, i), [","],[]);
                integer set=(integer)llList2String(l,1);    
                integer num=(integer)llList2String(l,2);    
                //string mess=llStringTrim(llList2String(l,3),STRING_TRIM);
                string mess=llDumpList2String(llList2List(l,3,-1), ",");
                integer nomenu=(integer)llList2String(l,0);    
                //perform requested action
                llMessageLinked (set, num, mess, id);
                
                if (!nomenu) //this will fail when not found, which it shouldnt.
                    llMessageLinked (LINK_THIS, 370, "", id);
            }   
        } else
        if (n==MSG_DO_SOUND) {
            //LM list was abused for sound info
            integer i=llListFindList(lmNames, [m]);
            if (i>=0)
                llTriggerSound(llList2String(lmAction, i), 1.);
        }
        else
        if (n==MSG_STORAGE_RESET)
            llResetScript();
    }
    
}

TMCP - menurender

string version = "TMCP v0.3x";

//Render menu

list menuNames;
list menuEntries; //menus that appear in 'auto' menu
list menuDetails;

//some hardcoded stuff that this script adds, not any menu. prefill.
list itemNames=["<<<",">>>","<BACK"];
list itemTypes=[7,7,7];

list menuHistory;
string currentMenu;
string currentPose="default";
string defaultPose="default";


integer channel=999; 
integer listenhandle;

integer mode_chat=1;
integer mode_adjust=0;
integer mode_menu=3; //user
integer mode_ball=0; //all

integer hibernate;

list rlvVictims;


integer TYPE_MENU=1;
integer TYPE_POSE=2;
integer TYPE_LM_NOMENU=3;
integer TYPE_LM_MENU=4;
integer TYPE_IGNORE=5;
integer TYPE_SPECIAL=6;
integer TYPE_UNKNOWN=7;
integer TYPE_SOUND=8;


integer MSG_DATA_MENU=310;
integer MSG_DATA_MENU_DETAILS=311;

integer MSG_DATA_TYPE=320;
integer MSG_DATA_POSE=330;
integer MSG_DATA_POSITION=331;
integer MSG_DATA_LM=335;

integer MSG_DO_POSE=350;
integer MSG_DO_LM=351;
integer MSG_DO_SPECIAL=352;

integer MSG_DO_STOP=353;
integer MSG_DO_SWAP=354;
integer MSG_MENU_SWAP=355;
integer MSG_DO_SOUND=356;

integer MSG_SET_RUNNING=390;
integer MSG_STORAGE_RESET=391;
integer MSG_DATA_READY=309;

integer MSG_DATA_START_READ=301;

integer MSG_MENU_META=333;

integer MSG_DUMP_POSITIONS=389;
integer MSG_MODE_ADJUST=368;
integer MSG_MODE_CHAT=367;
integer MSG_MODE_BALLUSERS=366;
integer MSG_MODE_OFF=365;

integer MSG_UPDATE_POS=363;
integer MSG_ALL_UNSIT =381;

integer MSG_SET_DEFAULT_POSE = 359;
integer MSG_DO_DEFAULT_POSE = 358;

integer MSG_RLV_GRAB=308;
integer MSG_RLV_RELEASE=309;
integer MSG_RLV_VICTIM_LIST=307;

integer MSG_DO_MENU_CURRENTMENU = 305;

//incoming
integer MSG_DO_MENU_COMMAND = -12002;

verbose(string text) {
    //llOwnerSay (text);
    if (mode_chat)
        llSay (0, text);
}

string onOff(integer value) {
    if (value) return "On"; else return "Off";   
}

string aclMode(integer value) {
    if (0==value)
        return "ALL";
    else
    if (1==value)
        return "GROUP";
    else
    if (2==value)
        return "OWNER";
    else
    if (3==value)
        return "USER";
    else
        return "UNKNOWN";
}

reset(integer resetSelf) {
    integer i;
    for (i=0; i<llGetInventoryNumber(INVENTORY_SCRIPT); i++) {
        string s=llGetInventoryName(INVENTORY_SCRIPT, i);
        if (-1<llSubStringIndex(s, "~TMCP ")) {
            if (s!=llGetScriptName()) {
                llResetOtherScript(s);    
            }   
        }
    }
    if (resetSelf)
        llResetScript();        
}

setRunning(integer running) {
    integer i;
    for (i=0; i<llGetInventoryNumber(INVENTORY_SCRIPT); i++) {
        string s=llGetInventoryName(INVENTORY_SCRIPT, i);
        if (-1<llSubStringIndex(s, "~TMCP ")) {
            if (s!=llGetScriptName()) {
                llSetScriptState (s, running);
            }   
        }
    }
}


integer hasDialogAccess(key id) {
    
    //OWNER ALWAYS
    if (id==llGetOwner())
        return TRUE;
        
    if (-1<llListFindList(rlvVictims, [(string)id]))
        return FALSE;
        
    //ALL
    if ((!mode_menu)/* || (id==llGetOwner())*/)
        return TRUE; //little speed optimalization to avoid call to samegroup
        
    //USER
    if (mode_menu==3) {
        //user must be sitted to access, or no-one sitted at all
        integer someSit=FALSE;
        integer i;
        for (i=0; i<=llGetNumberOfPrims(); i++) {
            if (llGetLinkKey(i)==id)
                return TRUE;
            if (llGetAgentSize(llGetLinkKey(i)))
                someSit=TRUE;   
        }
        return (!someSit);
    }
    
    //GROUP
    return (mode_menu==1) && llSameGroup(id);
}

integer hasMenuAccess (key id, string menu) {
    //supposed to be trimmed already?
    if (id==llGetOwner())
        return TRUE;
    string access=
        llStringTrim(
            llList2String(
                llParseString2List(
                    llList2String(menuDetails, 1+llListFindList(menuDetails, [menu]) )
                , ["|"], [])
            , 0)
        , STRING_TRIM);
    if (access=="OWNER") {
        return id==llGetOwner();
    }
    if (access=="GROUP") {
        return (id==llGetOwner()) || llSameGroup(id);        
    }
    if (access=="USER") {
        integer i;
        for (i=0; i<=llGetNumberOfPrims(); i++)
            if (llGetLinkKey(i)==id)
                return TRUE;
        return FALSE;   
    }
    //defaults to all.. learn to edit notecards if you made an error.
    //future options might be a name or some. dunnow.
    return TRUE;
}

makeMenu(string name, key id) {
    
    if (id); else return; //might be invoked by remote scripts. can't pop menu's to none-avi's.
    
    //present user a fine menu   
    integer idx=0;
    
    integer offset;
    
    if ((name==">>>") || (name=="<<<")) {
        //offset making
        //rename to the last found menu
        /*
        if (name==">>>")
            offset+=10;
        if (name=="<<<")
            offset-=10;                   
        */
        menuHistory+=name; //next step will fix this
        //llOwnerSay ("history: "+(string)menuHistory);
    }
    
    integer ho=llGetListLength(menuHistory)-1;
    while ((ho>1) && (-1<llListFindList(["<<<", ">>>"], [llList2String(menuHistory, ho)])) ) {
        //adjust offset
        string s=llList2String(menuHistory, ho);
        if (s=="<<<")
            offset-=10;
        if (s==">>>")
            offset+=10;
        name=llList2String(menuHistory, ho-1);
        ho--;            
    }
    
        
    if (name) {
        idx=llListFindList(menuNames, [name]);
        if (idx<0) {            
            //llOwnerSay ("No menu entry for "+name);
            return;
        }
        
    } else {
        //make it main menu
        name=llList2String(menuNames, 0); //assume at least 1 menu item please.. else its bugged anyways.
        menuHistory=[];
    }

    currentMenu=name;
        
    integer i=llListFindList (menuHistory, [name]);
    if (i>=0) {
        //truncate rest from history, maybe we went back.
        menuHistory = llList2List(menuHistory, 0, i);   
    } else 
        menuHistory += name;

        
    list present=llCSV2List(llList2String(menuEntries, idx));

    if (llAbs(offset)>=(llGetListLength(present))) offset=0;
                                
    integer o=offset;
    while (o>0) {
        menuHistory += ">>>";
        o-=10;   
        if (o<0)
            o=0;
    }
    while (o<0) {
        menuHistory += "<<<";
        o+=10;   
    }
    
    string h=">>";
    for (i=0; i<llGetListLength(menuHistory); i++)
        h+="> "+llList2String(menuHistory,i)+" ";
    
    
    if (currentPose)
        h+="\n\nCurrent pose: "+currentPose;
    //todo somewhere. some better wrap around.
    
    
    if ((offset) || (llGetListLength(present)>12))
        present = llList2List(present, offset, offset+8) + ["<<<", ">>>", "BACK"];        
    
    if (llGetListLength(rlvVictims)) {
        h+="\nCaptured (RLV): ";
        for (i=0; i<llGetListLength(rlvVictims); i++)
            h+=llKey2Name((key)llList2String(rlvVictims, i))+"\n";
    }    
    
    //got our filled up menu here? prolly need to sort it, for now just present it
    //llOwnerSay (name+":"+llList2CSV(present));
    
    //invert our list
    list p;
    for (i=0; i<12; i+=3)
        p = p+llList2List (present, -i-3, -i-1);
    
    llDialog (id, version+"\n"+h, p, channel);
}

processCommand (key id, string message) {
        integer typeidx=llListFindList (itemNames, [message]);
        string param;
        integer newMenu=TRUE;
        
        if (typeidx<0) {
            list l=llParseString2List(message, ["::"], []);
            message=llList2String(l,0);
            param=llList2String(l,1);
            typeidx=llListFindList (itemNames, [message]);
            //llOwnerSay ("Message "+message+" got param "+param);            
        }
        
        if (typeidx<0) {
            //llOwnerSay ("Error - no menu entry for "+message);
            return;    
        }
        
           
        integer type=llList2Integer(itemTypes, typeidx);
        //llOwnerSay ("Type for "+message+" is "+(string)type);
        if (type==TYPE_MENU) {
            if (hasMenuAccess(id, message)) {
                makeMenu(message, id);           
                newMenu=FALSE;                
            } else {
                llRegionSayTo (id, 0, "Access to menu "+message+" is denied.");
                //makeMenu(currentMenu, id);                   
                //return;
            }
        }
        else
        if (type==TYPE_POSE) {
            llMessageLinked (LINK_THIS, MSG_DO_POSE, message, "");
            currentPose=message;
            llMessageLinked (LINK_THIS, MSG_MENU_META,llList2String(menuDetails, 1+llListFindList(menuDetails, [currentMenu])), "");            
        }
        else
        if (type==TYPE_LM_NOMENU) {
            llMessageLinked (LINK_THIS, MSG_DO_LM, message, id);
            newMenu=FALSE;
        }
        else
        if (type==TYPE_LM_MENU) {
            llMessageLinked (LINK_THIS, MSG_DO_LM, message, id);
        }
        else
        if (type==TYPE_SOUND) {
            llMessageLinked (LINK_THIS, MSG_DO_SOUND, message, id);               
        }
        else
        //some built in messages
        if (type=TYPE_SPECIAL) {
        
            if  (message=="SWAP") {
                
                if ((integer)param) {            
                    llMessageLinked (LINK_THIS, MSG_DO_SWAP, param, id);
                }
                else {
                    llMessageLinked (LINK_THIS, MSG_MENU_SWAP, (string)channel, id);             
                    newMenu=FALSE;
                }
            }
            else
    
            if  (message=="STOP") {
                llMessageLinked (LINK_THIS, MSG_DO_STOP, "", id);
                newMenu=FALSE;
            }
            else
                
            if  (message=="BACK") {
                //check menu history
                string newM="";
                integer i=llGetListLength (menuHistory)-1;            
                while ((i>0) && (-1<llListFindList(["<<<", ">>>"], [llList2String(menuHistory, i)]))) i--;
                if (i>0) {
                    menuHistory=llList2List(menuHistory, 0, i);
                    newM=llList2String(menuHistory,i-1);                        
                }
                makeMenu(newM, id);
                newMenu=FALSE;
            }
            else
            if ((message=="<<<") || (message==">>>")) {
                //previous and next. let menumaker solve this.
                makeMenu(message, id);
                newMenu=FALSE;
            }
            else
            if (message=="<BACK") {
                //makeMenu(currentMenu, id);
            }
            else
            //anything we don't know off for some reason. include it anyways.
            if (message=="DUMP") {
                llMessageLinked (LINK_THIS, MSG_DUMP_POSITIONS, "", "");                   
            }
            else
            if (message=="CHAT") {
                mode_chat=!mode_chat;
                llMessageLinked (LINK_THIS, MSG_MODE_CHAT, (string)mode_chat, id);
                llRegionSayTo (id, 0, "Chat "+onOff(mode_chat));   
            }
            else
            if (message=="ADJUST") {
                mode_adjust=!mode_adjust;
                llMessageLinked (LINK_THIS, MSG_MODE_ADJUST, (string)mode_adjust, id);                
                llRegionSayTo (id, 0, "Adjusting : "+onOff(mode_adjust));   
            }
            else
            if (message=="MENUUSERS") {
                mode_menu++;
                if (mode_menu>3)
                    mode_menu=0;
                llRegionSayTo (id, 0, "Menu access set to "+aclMode(mode_menu));                   
            }
            else
            if (message=="BALLUSERS") {
                mode_ball++;
                if (mode_ball>2)
                    mode_ball=0;
                llRegionSayTo (id, 0, "Usage access set to "+aclMode(mode_ball));                   
                llMessageLinked (LINK_THIS, MSG_MODE_BALLUSERS, (string)mode_ball, id);                                
            }
            else
            if (-1<llListFindList (["RESET", "RESTART", "RELOAD"], [message])) {
                llSay (0, "Reset sequence started... Touch me again in a few seconds to reload the configuration.");
                reset(TRUE);   
                newMenu=FALSE;
            }
            else
            if (message=="Z") {
                if ("+"==llGetSubString(param,0,0))
                    param=llGetSubString(param,1,-1);                
                llSetObjectDesc((string)((integer)llGetObjectDesc()+(integer)param)+llGetSubString(llGetObjectDesc(), llStringLength((string)((integer)llGetObjectDesc())), -1));
                llMessageLinked (LINK_THIS, MSG_UPDATE_POS, (string)mode_ball, id);                
            }
            else
            if (message=="OFF") {
                state Off;
                newMenu=FALSE;
            }
            else
            if (message=="SAVE") {
                defaultPose=currentPose;
                llMessageLinked (LINK_THIS, MSG_SET_DEFAULT_POSE, currentPose, "");
   
            }
            else
            if (message=="GRAB") {
                llMessageLinked (LINK_THIS, MSG_RLV_GRAB, "", id);
                newMenu=FALSE;                               
            }
            else
            if (message=="RELEASE") {
                llMessageLinked (LINK_THIS, MSG_RLV_RELEASE, "", id);                            
            }
            else {
                //forward to whoever handles it
                llMessageLinked (LINK_THIS, MSG_DO_SPECIAL, message, id);            
            }
        }  
                  
        if (newMenu)
            makeMenu(currentMenu, id);            
}


upgrade() {string self = llGetScriptName(); string basename = self; if (llSubStringIndex(self, " ") >= 0) {integer start = 2; string tail = llGetSubString(self, llStringLength(self) - start, -1); while (llGetSubString(tail, 0, 0) != " ") {start++; tail = llGetSubString(self, llStringLength(self) - start, -1);} if ((integer)tail > 0) {basename = llGetSubString(self, 0, -llStringLength(tail) - 1);}} integer n = llGetInventoryNumber(INVENTORY_SCRIPT); while (n-- > 0) {string item = llGetInventoryName(INVENTORY_SCRIPT, n); if (item != self && 0 == llSubStringIndex(item, basename)) {llRemoveInventory(item);}}}

default
{
    state_entry()
    {
        upgrade();
        state WaitTouch;
    }
}

state WaitTouch
{
    state_entry()
    {
        reset(FALSE);
        llSay (0,"TMCP resetted - Touch me to load the configuration");
    }    
    touch_start (integer n) {
        llSay (0,"TMCP startup - reading data");
        state reading_data;   
    }
}

    
state reading_data
{    
    state_entry() {
        llMessageLinked (LINK_THIS, MSG_DATA_START_READ, "", "");   
    }
    link_message (integer sn, integer n, string m, key id) {
        if (n==MSG_DATA_MENU) {
            //add action    
            menuNames+=m;
            menuEntries+=(string)id;
        } else
        if (n==MSG_DATA_MENU_DETAILS) {
            //add action    
            menuDetails+= [m, (string)id];
        } else
        
        if (n==MSG_DATA_TYPE) {
            //set type
            itemNames+=m;
            itemTypes+=(integer)((string)id);   
        }
        if (n==MSG_SET_RUNNING)
            state running;
    }
}

state running {
    state_entry()
    {
        llOwnerSay ("Menu ready. "+(string)llGetFreeMemory());
        channel=-(integer)llFrand(1309123)-492345;
        listenhandle=llListen (channel, "", "", "");
    }
    
    touch_start(integer total_number)
    {
        key id=llDetectedKey(0);
        if (!hasDialogAccess(id)) {
            if (mode_chat)
                llWhisper (0, "Sorry "+llKey2Name(id)+", but menu usage is limited to "+aclMode(mode_menu));
            return;   
        }        
        makeMenu("",id);
    } 
    
    listen (integer ch, string name, key id, string message) {
        
        if (!hasDialogAccess(id)) {
            if (mode_chat)
                llWhisper (0, "Sorry "+llKey2Name(id)+", but menu usage is limited to "+aclMode(mode_menu));
            return;   
        }
        
        if (hibernate) {
            setRunning(TRUE);
            hibernate=FALSE;   
        }
        
        //llSetTimerEvent (3600.);
        
        processCommand (id, message);
        
    }
    
    link_message (integer sn, integer n, string m, key id) {
        if (n==MSG_DO_MENU_COMMAND) {
            processCommand (id, m);    
        }   
        else
        if (n==MSG_DO_DEFAULT_POSE) {
            currentPose = defaultPose;   
        }
        else
        if (n==MSG_RLV_VICTIM_LIST) {
            rlvVictims=llCSV2List(m);   
        }
        else
        if (n==MSG_DO_MENU_CURRENTMENU) {
            makeMenu(currentMenu, id);   
        }
    }
    
    timer() {
        //inactivity detected, sleep all other scripts.        
        llSetTimerEvent(0.);
        hibernate=TRUE;
        setRunning(FALSE);   
    }
}

state Off {
    state_entry() {
        llMessageLinked (LINK_THIS, MSG_ALL_UNSIT, "", "");    
        llWhisper (0,"Switching off in 10... 9...");
        llSleep (5.0);
        setRunning(FALSE);        
        llSleep (5.0);
        llSitTarget(ZERO_VECTOR, ZERO_ROTATION);        
    }   
    
    touch_start(integer n) {
        key id=llDetectedKey(0);
        if (id==llGetOwner())
            state running;
        if ((mode_menu==1) && llSameGroup(id))
            state running;
        llRegionSayTo (id, 0, "Sorry, but this object is switched OFF. Ask the owner to turn it on.");
    }
    
    state_exit() {
        setRunning(TRUE);
        hibernate=FALSE; 
        llWhisper (0, "Switching on");
          
    }
    
}

TMCP - notecard reader

//read MLP notecards
//see how much memory headroom we got after for other stuff

integer notecardidx;
key notecardkey;
integer notecardtype;
integer notecardline;
string notecardname;
list positions;
list tempMenu;
list menuNames;
list menuEntries; //menus that appear in 'auto' menu
list toMenu; //menus with pointers
list linkedMsg;

integer hasReset=FALSE; //indicates if there is a reset option in *some* menu. If absent, we add it for safety.


integer TYPE_MENU=1;
integer TYPE_POSE=2;
integer TYPE_LM_NOMENU=3;
integer TYPE_LM_MENU=4;
integer TYPE_IGNORE=5;
integer TYPE_SPECIAL=6;
integer TYPE_UNKNOWN=7;
integer TYPE_SOUND=8;

integer MSG_DATA_MENU=310;
integer MSG_DATA_MENU_DETAILS=311;
integer MSG_DATA_TYPE=320;
integer MSG_DATA_POSE=330;
integer MSG_DATA_POSITION=331;
integer MSG_DATA_LM=335;

integer MSG_DO_POSE=350;
integer MSG_DO_LM=351;
integer MSG_DO_SPECIAL=352;

integer MSG_DO_STOP=353;
integer MSG_DO_SWAP=354;

integer MSG_SET_RUNNING=390;
integer MSG_STORAGE_RESET=391;
integer MSG_DATA_READY=309;

integer MSG_DATA_START_READ=301;

verbose(string text) {
    llOwnerSay (text);
}

startNotecard() {
    integer i;
    notecardline=0;    
    for (;notecardidx<llGetInventoryNumber(INVENTORY_NOTECARD); notecardidx++) {                
        //loop until we found appropiate card
        notecardname=llGetInventoryName(INVENTORY_NOTECARD, notecardidx);
        if (llGetSubString(notecardname,0,9)==".MENUITEMS") {
            //neato. go fetch it
            notecardtype=1;
            notecardkey=llGetNotecardLine(notecardname, 0);
            verbose ("Reading menu from "+notecardname);
            return;            
        }
        if (llGetSubString(notecardname,0,9)==".POSITIONS") {
            //neato. go fetch it
            notecardtype=2;
            notecardkey=llGetNotecardLine(notecardname, 0);
            verbose ("Reading positions from "+notecardname);
            return;            
        }     
        verbose ("Skipping "+notecardname);
        
    }
    //still here? then we'r done
    //verbose ("Done reading all notecards");
    postFixMenu();
    sendMenuItems();
    llSleep (2.0);
    llMessageLinked (LINK_THIS, MSG_SET_RUNNING, "", ""); //set to running
    verbose ("Ready.");
    //llOwnerSay ("Notecard reader free memory: "+(string)llGetFreeMemory());
}

addPose (string name, list data) {
    //add pose info.. link to position info exactly when again?   
    //positions+=[name, data];
    llMessageLinked (LINK_THIS, MSG_DATA_POSE, name, llDumpList2String(data, "|"));       
}

addMenuDetails (string name, list data) {
    //add menu details.. access, color of balls.
    llMessageLinked (LINK_THIS, MSG_DATA_MENU_DETAILS, name, llDumpList2String(data, "|"));       
}


addLM (string name, string data) {
    llMessageLinked (LINK_THIS, MSG_DATA_LM, name, data);   
}

setType (string name, integer type) {
    llMessageLinked (LINK_THIS, MSG_DATA_TYPE, name, (string)type);       
}

parseMenu() {
    //parse our temporary menu to a real menu;   
    //this is where the real stuff happens.
    integer i;
    string menuName=""; //MLP 1 compatability: don't add stuff to menu as long as menu name is empty. add poses though.
    list options;
    for (i=0; i<llGetListLength(tempMenu); i++) {
        string data=llList2String(tempMenu, i);
        integer posspace=llSubStringIndex(data, " ");
        string cmd=data;
        string param="";
        string option="";
        if (posspace>0) {
            cmd=llGetSubString(data,0,posspace-1);
            param=llGetSubString(data, posspace+1, -1);   
        }
        list paramsTmp=llParseString2List(param, ["|"], []);
        integer j;
        list params;
        for (j=0; j<llGetListLength(paramsTmp); j++)
            params+=llStringTrim(llList2String(paramsTmp, j), STRING_TRIM);
        //params are now nice seperated on |, still allowed to contain spaces in the middle but trimmed from the outside.
        string p1=llList2String(params, 0);
        string p2=llList2String(params, 1);
        string p3=llList2String(params, 2);
        //got our set of command and optional parameters now. let's test them.
        if (cmd=="MENU") {
            addMenuDetails (p1, llDeleteSubList(params,0,0));
            //menuEntries+=p1;
            menuName=p1; //additional params hint the number of available seats            
            setType (p1, TYPE_MENU);  
        } else
        if (cmd=="POSE") {
            addPose (p1, llDeleteSubList(params,0,0));   
            option = p1;
            setType (p1, TYPE_POSE);
        } else
        if (cmd=="SOUND") {
            addLM (p1, p2);   
            option = p1;
            setType (p1, TYPE_SOUND);
        } else
        if (-1 != llListFindList (["BACK", "STOP","SWAP","RESET","RESTART", "MENUUSERS", "BALLUSERS","ADJUST", "DUMP", "CHAT", "RELOAD", "OFF"], [cmd])) {
            //recognized simple options
            option = cmd;   
            setType (cmd, TYPE_SPECIAL);
            if (-1!= llListFindList (["RESET","RESTART", "RELOAD"], [cmd]))
                hasReset=TRUE;
        } else
        if (-1 != llListFindList (["SAVE", "CHECK"], [cmd])) {
            //recognized simple options
            option = cmd;   
            setType (cmd, TYPE_IGNORE);
        } else
        if (cmd=="TOMENU") {
            if (p1!="-")
                toMenu+=p1;
            option=p1;   
        } else
        if (cmd=="LINKMSG") {
            //linkedMsg+=[p1,p2];
            addLM (p1, p2);
            option=p1;
            if ((integer)p2)
                setType (p1, TYPE_LM_NOMENU);
            else
                setType (p1, TYPE_LM_MENU);
        } else
        if (-1<llListFindList(["Z+", "Z-"], [llGetSubString(cmd, 0, 1)])) {
            //Z command, we'll rewrite it
            option = "Z::"+llGetSubString(cmd, 1, -1);
            setType ("Z", TYPE_SPECIAL);
        }
        else
        if (-1<llListFindList (["GRAB", "Grab.Fem"], [cmd])) {
            option = "GRAB";
            setType ("GRAB", TYPE_SPECIAL);               
        }
        else
        if (-1<llListFindList (["Grab.Male","RELEASE"], [cmd])) {
            option = "RELEASE";
            setType ("RELEASE", TYPE_SPECIAL);                           
        }
        //todo.. fill in a bunch of MLP commands here, implemented or not
        else
        { //we checked all commands. Anything defined here is considered undefined and we list it just as is.
            option = cmd;
            //don't set type. it may be defined elsewhere.
            //setType (cmd, TYPE_UNKNOWN);            
        }
        if (menuName) if (option)
            options+=option;
    }
    tempMenu=[];
    if (options!=[]) {
        llOwnerSay ("Menu "+menuName +" ["+(string)llGetListLength(options)+"]");
        //menuAll+=options+"!--!";
        //menuAll+=[menuName,llList2CSV(options)];
        menuNames+=menuName;
        menuEntries+=llList2CSV(options);
    }
}

addMenuItem(string data) {
    integer posslash=llSubStringIndex(data, "//");
    if (posslash>=0) {
        data=llDeleteSubString(data, posslash, -1);
    }
    data=llStringTrim(data,STRING_TRIM);
    //llOwnerSay ("menu data: "+data);
    
    if (data=="") //comment or some but not empty line, silently ignore
        return;
    
    if (llGetSubString (data,0,3+1)=="MENU ") {
        //what a surprise, new menu. finish old one.
        parseMenu();
    }
    
    //what's left, add it to our temp menu. We'l parse it later.
    tempMenu+=data;
}

postFixMenu() {
    //on failed configurations - add some default stuff
    if (!llGetListLength(menuNames)) {
        
        llOwnerSay ("Warning - no reset function found in menu cards. Adding menu 'Reset'.");
        //add a default menu with a reset button.
        
        addMenuItem ("MENU Reset...|OWNER");
        addMenuItem ("RESET");
        parseMenu();
    }
    
    
    //dont add mainmenu
    list entries=llDeleteSubList(menuNames,0,0);
    
    integer i;
    
    //llOwnerSay ("PF entries: "+(string)entries);
    //llOwnerSay ("PF tomenu: "+(string)toMenu);    
    
    
    for (i=0; i<llGetListLength(toMenu); i++) {        
        integer j=llListFindList (entries, [llList2String(toMenu, i)]);
        if (j>-1) {
            entries=llDeleteSubList(entries, j, j);
        }        
    }
    //llOwnerSay ("PF entries after: "+(string)entries);
    
    list MainMenu=llCSV2List(llList2String(menuEntries, 0));
    for (i=0; i<llGetListLength(entries); i++) {
        integer j=llListFindList(MainMenu, ["-"]);
        if (j>-1) {
            MainMenu=llListReplaceList(MainMenu, [llList2String(entries, i)], j, j);            
        }   
    }
    
    i=llListFindList(MainMenu, ["-"]);
    while (-1<i) {
        MainMenu=llDeleteSubList(MainMenu, i, i);
        i=llListFindList(MainMenu, ["-"]);               
    }
    
    if (MainMenu)
        menuEntries = llListReplaceList(menuEntries, [llList2CSV(MainMenu)], 0, 0);
}

sendMenuItems() {
    //llOwnerSay ("Applying menu information");
    
    integer i;
    for (i=0; i<llGetListLength(menuNames); i++) {
        llMessageLinked (LINK_THIS, MSG_DATA_MENU, llList2String(menuNames, i), llList2String(menuEntries, i));            
        llSleep (0.25);
    }    
    
    llMessageLinked (LINK_THIS, MSG_DATA_READY, "Done", "TTP");    
}

addPosition(string data) {
    //some memory optimalization possible here in last half.
    integer curlyopen = llSubStringIndex(data, "{");
    integer curlyclose = llSubStringIndex(data, "}");
    if ((curlyopen<0) || (curlyclose<curlyopen))
        return;
    string pose=llGetSubString(data, curlyopen+1, curlyclose-1);
    data=llGetSubString(data, curlyclose+1, -1);
    list posrotsraw=llParseString2List(data, [">"], []);
    //llOwnerSay ("raw pos data "+data+" convert to "+(string)posrotsraw);
    
    list posrots;
    integer i;
    for (i=0; i<llGetListLength(posrotsraw); i++) {
        if (-1<llSubStringIndex (llList2String(posrotsraw,i), "<")) {
            list v=llParseString2List(llList2String(posrotsraw,i), ["<", ",", " "], []);         
            posrots += <(float)llList2String(v,0), (float)llList2String(v,1),(float)llList2String(v,2)>;
        }
    }
    //llOwnerSay ("position data for "+pose+" : "+(string)posrots);
    //positions+=[pose, llList2CSV(posrots)];
    llMessageLinked (LINK_THIS,MSG_DATA_POSITION, pose, llDumpList2String(posrots,"&"));
}



upgrade() {string self = llGetScriptName(); string basename = self; if (llSubStringIndex(self, " ") >= 0) {integer start = 2; string tail = llGetSubString(self, llStringLength(self) - start, -1); while (llGetSubString(tail, 0, 0) != " ") {start++; tail = llGetSubString(self, llStringLength(self) - start, -1);} if ((integer)tail > 0) {basename = llGetSubString(self, 0, -llStringLength(tail) - 1);}} integer n = llGetInventoryNumber(INVENTORY_SCRIPT); while (n-- > 0) {string item = llGetInventoryName(INVENTORY_SCRIPT, n); if (item != self && 0 == llSubStringIndex(item, basename)) {llRemoveInventory(item);}}}


default 
{
    state_entry() {        
        upgrade();
        //state ReadMenu;           
    }
    
    link_message (integer sn, integer n, string m, key id) {
        if (n==301)
            state ReadMenu;
    }
    
}

state ReadMenu
{
    state_entry()
    {
        llMessageLinked (LINK_THIS, MSG_STORAGE_RESET, "", ""); //reset data storage scripts
        llSleep (2.0);
        //fetch first notecard, go on from there
        startNotecard ();
    }
    
    dataserver(key id, string data) {
        //llOwnerSay ("card "+notecardname+" line "+(string)notecardline+" data "+data);
        
        if (id!=notecardkey)
            return; //not for us
            
        if (data==EOF) {
            if (notecardtype==1)
                parseMenu(); //clean up any menu leftovers
            //read next notecard
            notecardidx++;
            startNotecard();
        } else   
        {
            if (notecardtype==1)
                addMenuItem(data);
            else
                addPosition(data);
            notecardkey=llGetNotecardLine(notecardname, ++notecardline);

        }
    }
}

TMCP RLV extensions (optional)

//Changes by Tano Toll for seamless MLCP compatability, and to dodge 'double locking':

//*sit and unsit events do not trigger an RLV command (master script already does this)
//*identifier is changed to object name, url encoded.
//*delayed but automatic notecard read on inventory change

// ----------------------------------------------------------------------------------
// MLPV2 Plugin for RLV Script V1.02
//
// Use with a notecard named .RLV with the format:
// Pose|Ball no.: 0,1...*|@RLV-Command
// e.g.
// Missionary|1|@unsit=n
// stand|*|!release
// ----------------------------------------------------------------------------------
// Copyright (c) 2010, Jenni Eales. All rights reserved.
// ----------------------------------------------------------------------------------
// Redistribution and use in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met:
//
//    * Redistributions of source code must retain the above copyright notice,
//      this list of conditions and the following disclaimer.
//    * Redistributions in binary form must reproduce the above copyright notice,
//      this list of conditions and the following disclaimer in the documentation
//      and/or other materials provided with the distribution.
//    * The names of its contributors may not be used to endorse or promote products
//      derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
// IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
// OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
// OF SUCH DAMAGE.
//
// Changes:
// ----------------------------------------------------------------------------------
// Version 1.02
// - use | as seperator
// - support multiple lines for one pose
// - additional link message for direct call from ball menu
//
// Version 1.01
// - use ; instead of | as seperator
//
// Version 1.0
// - first version released
// ----------------------------------------------------------------------------------
//
// constants
string NOTECARD         = ".RLV"; // name of config notecard
integer DEBUG           = FALSE;  // switch debug on/off
integer RELAY_CHANNEL   = -1812221819; // channel for RLV relay
float TIMER_SEC         = 1800.0; // 30 min.
string CMD_RELEASE      = "!release";  // release command
integer LM_NUMBER       = -1819;  // number for link message
 
// internal use
key kQuery;
integer iLine;
 
string pose = "stand"; // pose set by the Ball Menu
list poses    = [];    // configured poses
list balls    = [];    // configured balls
list commands = [];    // configured commands
list avatars  = [];    // avatars sitting on balls

integer wants_init;
 
// log a message
log(string message)
{
    if(DEBUG) llOwnerSay(message);
}
 
// only for testing purposes
log_data()
{
    log("List:");
    integer total_number = llGetListLength(commands);
    integer i;
    for(i=0; i<total_number; i++)
    {
        log("- " + llList2String(poses, i) + ": (" + llList2String(balls, i) + ") "+ llList2String(commands, i));
    }
}
 
// write a message to the RLV Relay
relay(key avatar, string message)
{
    list tokens = llParseString2List(message, ["|"], [""]);
    integer total_number = llGetListLength(tokens);
    integer i;
    for(i=0; i<total_number; i++)
    {
        string token = llList2String(tokens, i);
        log("RLV: MLPV2," + (string) avatar + "," + token);
        llSay(RELAY_CHANNEL, llEscapeURL(llGetObjectName())+"," + (string) avatar + "," + token);
    }
}
 
// find commands for pose and play it for avatars on ball
rlv(string pose)
{
    log("command for: " + pose);
    llSetTimerEvent(TIMER_SEC);
 
    integer total_number = llGetListLength(poses);
    integer i;
    for(i=0; i<total_number; i++)
    {
        if (pose == llList2String(poses, i))
        {
            string ball = llList2String(balls, i);
            log("command for pose: " + pose + " (" + ball + ")");
            if (ball == "*")
            {
                integer b;
                for (b=0; b<6; b++)
                {
                    // get avatar for ball index
                    key avatar = llList2Key(avatars, b);
                    if(avatar != NULL_KEY) relay(avatar, llList2String(commands, i));
                }
            }
            else
            {
                key avatar = llList2Key(avatars, (integer) ball);
                if(avatar != NULL_KEY) relay(avatar, llList2String(commands, i));
            }
        }
    }
}
 
// new find: pose and ball needs to match
integer find_pose(string pose, string ball)
{
    integer total_number = llGetListLength(poses);
    integer i;
    for(i=0; i<total_number; i++)
    {
        if (pose == llList2String(poses, i) && ball == llList2String(balls, i))
        {
            return i;
        }
    }
    return -1;
}
 
// add a pose/ball/command entry
add_pose(string pose, string ball, string command)
{
    // look for existing entry for pose
    integer found = find_pose(pose, ball);
    if (found == -1)
    {
        // add a new pose
        poses += [pose];
        balls += [ball];
        commands += [command];
 
        log("Added from " + NOTECARD + ": " + pose + " | " + ball + " | " + command);
    }
    else
    {
        // append to existing pose
        string c = llList2String(commands, found) + "|" + command;
        commands = llListReplaceList(commands, [c], found, found);
 
        log("Added " + command + " to pose " + pose);
    }
}
 
// parse and store a line
process_line(string line)
{
    list tokens = llParseString2List(line, ["  |  ","  | "," |  "," | "," |","| ","|"],[""]);
    if(llGetListLength(tokens) < 3) return;
 
    string pose = llList2String(tokens, 0);
    string ball = llList2String(tokens, 1);
 
    integer total_number = llGetListLength(tokens);
    integer i;
    for(i=2; i<total_number; i++)
    {
        add_pose(pose, ball, llList2String(tokens, i));
    }
}
 
// check if object is in the inventory
integer exists_notecard(string notecard) {
    return llGetInventoryType(notecard) == INVENTORY_NOTECARD;
}
 
// initialize me
initialize()
{
    log("Initializing...");
    poses = [];
    balls = [];
    commands = [];
    avatars = [NULL_KEY, NULL_KEY, NULL_KEY, NULL_KEY, NULL_KEY, NULL_KEY];
 
    if (exists_notecard(NOTECARD))
    {
        log("Reading " + NOTECARD + "...");
        iLine = 0;
        kQuery = llGetNotecardLine(NOTECARD, iLine);
    }
    else
    {
        llOwnerSay("Not found: " + NOTECARD );
    }
}
 
upgrade() {string self = llGetScriptName(); string basename = self; if (llSubStringIndex(self, " ") >= 0) {integer start = 2; string tail = llGetSubString(self, llStringLength(self) - start, -1); while (llGetSubString(tail, 0, 0) != " ") {start++; tail = llGetSubString(self, llStringLength(self) - start, -1);} if ((integer)tail > 0) {basename = llGetSubString(self, 0, -llStringLength(tail) - 1);}} integer n = llGetInventoryNumber(INVENTORY_SCRIPT); while (n-- > 0) {string item = llGetInventoryName(INVENTORY_SCRIPT, n); if (item != self && 0 == llSubStringIndex(item, basename)) {llRemoveInventory(item);}}}
 
 
default
{
    // on state entry initialize me
    state_entry()
    {
        upgrade();
        
        initialize();
    }
 
    // reset on Inventory change
    changed(integer change)
    {
        if (change & CHANGED_INVENTORY) {
            wants_init=TRUE;
            llSetTimerEvent (15.0);
        }
    }
 
    // read a line from notecard
    dataserver(key _query_id, string _data) {
        // were we called to work on notecard?
        if (_query_id == kQuery) {
            // this is a line of our notecard
            if (_data == EOF) {
                if (DEBUG) log_data();
                log(NOTECARD + " read.");
            } else {
                    // increment line count
                    // data has the current line from this notecard
                    if(_data != "") {
                        process_line(_data);
                    }
 
                // request next line
                // read another line when you can
                iLine++;
                kQuery = llGetNotecardLine(NOTECARD, iLine);
            }
        }
    }
 
    timer()
    {
        if (wants_init) {
            wants_init = FALSE;
              initialize();        
            llSetTimerEvent (0.0);
            return;    
        }
        
        integer b;
        for (b=0; b<6; b++)
        {
            // get avatar for ball index
            key avatar = llList2Key(avatars, b);
 
            // if not null send command
            if(avatar != NULL_KEY) relay(avatar, CMD_RELEASE);
        }
        llSetTimerEvent(0.0);
    }
 
    // link message from MLPV2
    link_message(integer sender_number, integer number, string message, key id)
    {
        /*
        // pose from ball menu selected
        if (number == 0 && message == "POSEB") {
            pose = (string)id;
            rlv(pose);
            return;
        }
        */
 
        // direct call from ball menu
        // configure in .MENUITEMS with
        // LINKMSG Resctriction | 0,-1,-1819,Restriction
        if (number == LM_NUMBER) {
            //llOwnerSay ("RLV: -- "+message);
            pose = message;
            rlv(pose);
            return;
        }
 
        // unsit avatar
        if (number == -11001)
        {
            integer ball = (integer) message;
            log("Avatar unsit from ball " + (string) ball +": " + (string) llList2Key(avatars, ball));
            //relay(llList2Key(avatars, ball), CMD_RELEASE);
            avatars = llListReplaceList(avatars, [NULL_KEY], ball, ball);
        }
 
        // sit avatar
        if (number == -11000)
        {
            list tokens = llParseString2List(message, ["|"], [" "]);
            integer ball = llList2Integer(tokens, 0);
            log("Avatar sit on ball " + (string) ball +": " + (string) id);
            avatars = llListReplaceList(avatars, [id], ball, ball);
            //rlv(pose);
        }
    }
}