skimi3d Grid Log in Visit the grid
All scripts
Signs and text

Menu Starter

A working blue menu built to be taken apart. It pages past nine buttons, listens on a channel nobody else has, and stops listening when it should.

Goes inWhatever should have a menu
Setup4 to set
Code259 lines
Open toOpen to everyone

What it does

Touching it opens a dialog; picking a button runs a piece of code you replace. Everything around that is the plumbing every menu needs and most home-made ones get wrong: the listen is closed the moment the menu is answered and again if it is not, the channel is worked out from the toucher's own key so two visitors cannot answer each other's menus, and a list longer than nine buttons is paged instead of silently losing its tenth. The one function you are meant to rewrite is marked, and the reasons behind each decision are written next to it rather than left for you to find out.

What you can build with it

The starting point for a vendor, a teleport board, a colour picker, a rezzer with options, a door with an access list, a game, a tip jar with amounts, a light with scenes. Menus are the doorway to nearly everything, so this is less a finished thing than the first two hundred lines of the next thing you build.

How to use it

1 Drop the script into any prim and touch it. The example menu works as it stands.
2 Change BUTTONS to the buttons you want, and HEADING to the line above them.
3 Scroll down to chose(). That one function is the whole of your object; everything else is plumbing.
4 Write one if for each button, matching the label exactly. The button comes back to you as its label, so renaming a button breaks the test.
5 Add more than nine buttons and watch it page them. Two of the nine slots go to Back and Next.

Settings you can change

4

Edit these at the top of the script. Everything works on the defaults · change them only when you need to.

BUTTONS The buttons, in the order you want them. Keep each to about eleven characters, because the viewer cuts off anything longer without saying so. five example buttons
HEADING The line above the buttons. This is where you explain what the buttons do, because the buttons have no room to explain themselves. What would you like?
PATIENCE Seconds the menu waits for an answer before it stops listening. Every menu needs one of these; a listen left open wakes the script for every word spoken nearby. 60.0
WHO_MAY_OPEN 0 for everyone, 1 for the owner alone, 2 for anyone wearing the same group tag. 0
The code LSL · 259 lines · updated 8 Sep 2026 Download .lsl
// =====================================================================
//  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.
// ---------------------------------------------------------------------

Copy it, make a new script inworld, paste over what is there and save. Nothing here phones home.