TMCP

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 (-1MAX_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 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
==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 (-11)
 p=llVecNorm(p);
 agentAdjust+=[id,p,r]; 
 runAnims();
}
adjustPosition (string name, integer index, vector p, vector r) {
 //positionNames
 integer i=llListFindList (positionNames, [name]); 
 if (-10) {
 //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= 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"], []);
 //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);
 }
 }
}