// ===================================================================== // Menu Starter // skimi3d script library // https://grid.skimi3d.com/scripts/menu-starter // // A blue menu that opens when you touch it, does something when you // pick a button, and then gets out of the way properly. // // This one is meant to be read and taken apart, not just used. Menus // are the doorway to nearly everything else you will build, and almost // every home-made one has the same three faults: it listens for ever, // it listens on a channel everybody shares, and it stops working after // nine buttons without saying why. All three are dealt with below, and // the reasons are written next to the fixes. // // Plain LSL. // ===================================================================== // --------------------------------------------------------------------- // Settings // --------------------------------------------------------------------- // The buttons, in the order you want them. Keep each one to about // eleven characters; the viewer cuts off anything longer without // telling anybody. list BUTTONS = [ "Hello", "Sit", "Light on", "Light off", "Who am I" ]; // The line above the buttons. This is where you say what the thing is // and what the buttons do, because the buttons themselves have no room // to explain anything. string HEADING = "What would you like?"; // Seconds the menu waits for an answer before it stops listening. Every // menu needs one of these, and the reason is in the notes at the foot // of this file. float PATIENCE = 60.0; // Who may open it. 0 = everyone, 1 = only the owner, 2 = same group. integer WHO_MAY_OPEN = 0; // --------------------------------------------------------------------- // Everything below here is the machinery // --------------------------------------------------------------------- // Nine buttons to a page, because that is all a dialog holds. Anything // over nine gets paged, with Next and Back added automatically. integer PER_PAGE = 9; integer gChannel = 0; integer gListen = 0; // the handle, so it can be switched off again key gWho = NULL_KEY; integer gPage = 0; // A channel nobody else is using. // // A menu on channel 1, or on any number somebody might have typed, will // answer to whatever else in the region happens to say the same word. // Negative channels cannot be typed into chat at all, and mixing in the // toucher's own key means two people using the same object at once do // not answer each other's menus. integer privateChannel(key who) { return -1 - (integer)("0x" + llGetSubString((string)who, 0, 6)); } // The buttons for one page, with the paging buttons added when there // are more than fit. list page(integer n) { integer total = llGetListLength(BUTTONS); if (total <= PER_PAGE) return BUTTONS; // Room for Next and Back on every page, so seven of the nine slots // are left for real buttons. integer room = PER_PAGE - 2; integer pages = (total + room - 1) / room; if (n < 0) n = pages - 1; if (n >= pages) n = 0; gPage = n; integer from = n * room; integer to = from + room - 1; if (to > total - 1) to = total - 1; return llList2List(BUTTONS, from, to) + ["< Back", "Next >"]; } open(key who, integer n) { // Off with the old one first. A script that opens a second listen // without closing the first leaks them, and a script that has // leaked enough of them stops hearing anything at all. if (gListen) llListenRemove(gListen); gWho = who; gChannel = privateChannel(who); gListen = llListen(gChannel, "", who, ""); llDialog(who, HEADING, page(n), gChannel); llSetTimerEvent(PATIENCE); } // Stop listening. Called when the menu is answered, and again when it // is not. close() { if (gListen) llListenRemove(gListen); gListen = 0; gWho = NULL_KEY; gPage = 0; llSetTimerEvent(0.0); } // --------------------------------------------------------------------- // This is the part you replace. // // Everything above is plumbing that works the same for every menu. // Below is the only bit that is about YOUR object. // --------------------------------------------------------------------- chose(key who, string button) { if (button == "Hello") { llRegionSayTo(who, 0, "Hello yourself."); return; } if (button == "Sit") { llRegionSayTo(who, 0, "There is nothing to sit on yet. Sit Here in this library is the script for that."); return; } if (button == "Light on") { llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_POINT_LIGHT, TRUE, <1.0, 0.9, 0.7>, 0.8, 8.0, 0.6]); return; } if (button == "Light off") { llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_POINT_LIGHT, FALSE, <1.0, 1.0, 1.0>, 0.0, 1.0, 1.0]); return; } if (button == "Who am I") { llRegionSayTo(who, 0, "You are " + llKey2Name(who) + ", and I am " + llGetObjectName() + "."); return; } } integer mayOpen(key who) { if (WHO_MAY_OPEN == 1) return who == llGetOwner(); if (WHO_MAY_OPEN == 2) return llSameGroup(who); return TRUE; } default { state_entry() { close(); } touch_start(integer total) { key who = llDetectedKey(0); if (!mayOpen(who)) { llRegionSayTo(who, 0, "This one is not yours to open."); return; } open(who, 0); } listen(integer channel, string name, key who, string message) { if (message == "Next >") { open(who, gPage + 1); return; } if (message == "< Back") { open(who, gPage - 1); return; } // Answered, so stop listening BEFORE doing the work. If the // work takes a moment, an open menu is an open door. close(); chose(who, message); } timer() { // Nobody answered. Tidy up rather than listening for ever. close(); } on_rez(integer start) { llResetScript(); } } // --------------------------------------------------------------------- // The three faults this is built to avoid // // 1 · LISTENING FOR EVER. A listen left open is not free: every line // of chat anywhere near the object wakes the script up so it can // decide to ignore it. One forgotten listen is nothing; a region // with two hundred vendors each holding one open is a region that // drags for reasons nobody can find. Hence the timer, and hence // closing the listen the moment the menu is answered. // // 2 · A SHARED CHANNEL. Channel 1, or any small number, is a channel // anybody can type on and every other script may be using. The // channel here is worked out from the toucher's own key, so it is // both unguessable and different for every person, which means two // visitors using the same object at once cannot answer each // other's menus. // // 3 · TEN BUTTONS. A dialog holds nine, and the tenth does not appear // and does not complain. Above nine this script pages them, which // costs two of the nine slots and is still better than the button // that silently is not there. // // One more thing that is not a fault but surprises everybody: the // button a visitor pressed comes back as its LABEL, a plain string. // Rename a button and every test against it stops matching. That is // why the labels in chose() are written out in full rather than being // matched by position. // ---------------------------------------------------------------------