Don't click here unless you want to be banned.

LSL Wiki : LibraryGeneralMenuEngine

HomePage :: PageIndex :: RecentChanges :: RecentlyCommented :: UserSettings :: You are crawl338.us.archive.org

General Menu Engine

This is a library that allows LSL programmers to easily create dialog-based objects (instead of the laggy listen-based ones). It communicates with link messages with your script (in the same prim). It is somewhat in the similar domain as DialogModule, but it's quite a bit different.
This is the first version of my script, so feel free to comment on it, and I might enhance it in the future!

The theory is that you need to 'define' your hierarchic menu system (in a "menudefs" notecard), then you can use just easy 'doMenu("THEMENUNAME");' calls to start a menu, and you get the result with a link message.

The library opens listens for 60 seconds maximum (timeout) or only as long as is needed (until user presses a choice), so it should generate as less lag as possible.

The menudefs notecard goes like this:
MENU DEFAULT
TEXT ~ Main Menu ~\nSelect a function
TOMENU SETTITLE Set title
OPTION Start vote
TOMENU CLITEM Clear Item
OPTIONASK Reset

Where:
* MENU <MENUID> -- defines the menu ID, that you can later pass to the doMenu() function, also returned in the results
* TEXT <textTextText> -- the text displayed in the menu
* TOMENU <MenuID> <ButtonText> -- the users sees 'ButtonText' on a button, upon pressing, s/he'll get to the menu with ID: MenuID. You can call that a submenu.
* OPTION <ButtonText> -- An option, upon press, the MenuID and the ButtonText gets returned
* OPTIONASK <ButtonText> -- Same as option, but will ask Yes/No before returning the result

The result, returned in the 'string' part of a linkmessage (number: LM_OPTIONDATA) looks like this:
* MENUID|ButtonText


A MenuEngine-enhanced script and object contains:
* MenuEngine script - the library, used with link messages
* menudefs - notecard defining the menus
* your custom script - using the library with link messages, and containing the 'interface' code for easiness

If you want to create a menu-enhanced object
* Have your script in the object, and copy & paste the interface code from the start of Menu demo included below
* Put my MenuEngine script in the object
* Create your menudefs notecard, and put it in the object (or update it)
* IMPORTANT: RESET the MenuEngine script - it will (re)read the menudefs, that takes a few seconds (5-10)

The inteface library provides these functions:
* resetMenu() -- resets and reloads the menu notecard
* doMenu( key user, string menuname ) -- show MENUNAME ID menu to user USER, and get the response with linkMessages
* readString( key user, string var, string prompt ) -- show a PROMPT to user and read a string from chat
* customMenu( key user, string menuname, string message, list buttons ) -- make a dialog for user USER, with a pseudo menuid MENUNAME, the dialog will say message and have BUTTONS for buttons

Also: another example for using this library: General Menu Engine Example 1

Issues:
* Concurrency: only one person can really use the dialogs at a time. The last popped-up dialog handling will clear the listen to the previous user. Most of the time, this is not really a problem, though, but be aware of it.

History:
1.0.2: custom menu function added



The "MenuEngine" script:
// Menu Engine - v 1.0.2
// by Zonax Delorean
// License: BSD, but please provide patches/bugfixes if you can!

integer LM_DOMENU   = 10001;
integer LM_OPTIONDATA = 10002;
integer LM_MENULOADED = 10003;
integer LM_RESETMENUSYSTEM = 10004;
integer LM_READSTRING = 10005;
integer LM_READSTRINGDATA = 10006;
integer LM_CUSTOMMENU   = 10007;

string confNoteName = "menudefs";
integer cLine;
key cqid;

integer MC_TEXT = 1;
integer MC_TOMENU = 2;
integer MC_OPTION = 3;
integer MC_OPTIONASK = 4;
integer MC_END = 999;
// ...

integer numMenus;
list menudata;
list menuOffsets;
integer MENUREC_SIZE = 4;

integer debug = FALSE;

emitMsgSender( integer num, string msg ) {
    if (debug) debugSay("Emit MSG: chn#"+(string)num+" : "+msg);
    llMessageLinked(llGetLinkNumber(),num,msg,cUser);
}
emitMsgGlobal( integer num ) {
    if (debug) debugSay("Emit MSG Global: chn#"+(string)num+" : ");
    llMessageLinked(llGetLinkNumber(),num,"",NULL_KEY);
}

debugSay( string st ) {
    llSay(0,st);
}

string clMenuID;
list clButtons;
list clActions;
string clMenuText;

// read data
startReadConfig() {
    numMenus = 0;
    menudata = [];
    menuOffsets = [];
    clMenuID = "";
    clButtons = [];
    clActions = [];
    clMenuText = "";

    cLine = 1;
    cqid = llGetNotecardLine( confNoteName, cLine - 1 ); // request first line
}

commitEntry() {
    // [ menuID, startOffset, startOffsetActions, endOffset ]
    integer sofs = llGetListLength(menudata);
    menuOffsets += [ clMenuID, sofs, sofs + 1 + llGetListLength(clButtons), sofs + 1 + llGetListLength(clButtons) + llGetListLength(clActions) ];
    menudata += [ clMenuText ];
    menudata += clButtons;
    menudata += clActions;
    
    clMenuID = "";
    clMenuText = "";
    clButtons = [];
    clActions = [];
}

readConfigLine( string data ) {
    if (data != EOF) {    
        integer spos = llSubStringIndex(data," ");
        if (spos==-1) jump LreadNextLine; // not a line with space

        string cmd = llGetSubString( data, 0, spos - 1);
        data = llGetSubString( data, spos+1, -1);
        
        integer cmdid = llListFindList(["MENU","TEXT","TOMENU","OPTION","OPTIONASK"],[cmd]);
        if (cmdid==-1) jump LreadNextLine; // not a command
        
        if (cmdid==0) { // MENU
            if (clMenuID!="") commitEntry();
            clMenuID = data;                        
        }
        else if (cmdid==1) { // TEXT
            if (clMenuText!="") clMenuText += "\n";
            list tmpl = llParseString2List(data,["\\n"],[]);
            data = llDumpList2String(tmpl,"\n");
            clMenuText += data;
        }
        else if (cmdid==2) { // TOMENU <MenuID> <ButtonText> or TOMENU <MenuID>
            integer wpos = llSubStringIndex(data," ");
            string tomenuid = data;
            string btext = data;
            if (wpos>=0) {
                tomenuid = llGetSubString(data,0,wpos - 1);
                btext = llGetSubString(data,wpos+1,-1);
            }
        
            clButtons  += [ btext ];
            clActions  += [ MC_TOMENU, tomenuid ];
        }
        else if (cmdid==3) { // OPTION <ButtonText>
            clButtons  += [ data ];
            clActions  += [ MC_OPTION, data ];
        }
        else if (cmdid==4) { // OPTIONASK <ButtonText>
            clButtons  += [ data ];
            clActions  += [ MC_OPTIONASK, data ];
        }    

@LreadNextLine; // jump point
        cLine++;
        cqid = llGetNotecardLine( confNoteName, cLine - 1 );
    } else {
        if (clMenuID!="") commitEntry();
        numMenus = llGetListLength(menuOffsets) / MENUREC_SIZE;
        configLoaded();   
    }
}

//
// gets called when the config is done loading
//
configLoaded() {
    if (debug) {
        debugSay("Menu data loaded, mem: "+(string)llGetFreeMemory());
        llInstantMessage(llGetOwner(),"OFS: "+llDumpList2String(menuOffsets,","));
        llInstantMessage(llGetOwner(),"DATA: "+llDumpList2String(menudata,","));
    }

    // signal a message to the main prog
    emitMsgGlobal(LM_MENULOADED);
}

string cMenuName;
integer cMenuNum;
integer CHATBASECHANNEL = 100050;
integer cOfs; // current menu start offset
key cUser; // current user
integer nList;
integer isListening = FALSE;
float RESPONSE_TIMEOUT = 60.0;
list cButtons;
list cActions;
integer cMenuType;
integer MT_ASK = 2;
integer MT_NORMAL = 1;
integer MT_READSTRING = 3;
integer MT_CUSTOM = 4;
integer cSenderID;

clearListens() {
    if (isListening) {
        llListenRemove(nList);
        isListening = FALSE;
    }
}

integer getMenuNum( string menuid ) {
    integer c;
    for (c=0;c<numMenus;c++)
        if (llList2String(menuOffsets,c*MENUREC_SIZE) == menuid)
            return(c);
    return(-1);
}

// load a data for a menu and start a dialog
doMenu( key user,  string menuname ) {
    if (debug) debugSay("Do menu: "+menuname);
    integer mnum = getMenuNum(menuname);
    if ( mnum == -1 ) { // menu not found
        llSay(0,"Error: no such menu: "+menuname);
        return;
    }    

    cOfs = llList2Integer( menuOffsets, mnum * MENUREC_SIZE + 1 );
    integer actOfs = llList2Integer( menuOffsets, mnum * MENUREC_SIZE + 2 );
    integer actEnd = llList2Integer( menuOffsets, mnum * MENUREC_SIZE + 3 );

    cMenuNum = mnum;
    cMenuName = menuname;
    cUser = user;
    cButtons = llList2List( menudata, cOfs+1, actOfs - 1 );
    cActions = llList2List( menudata, actOfs, actEnd );
    cMenuType = MT_NORMAL; // normal menu

    clearListens();    
    nList = llListen( CHATBASECHANNEL + cMenuNum, "", user, "");
    isListening = TRUE;
    llDialog( user, llList2String( menudata, cOfs ), cButtons, CHATBASECHANNEL + cMenuNum );
    llSetTimerEvent(RESPONSE_TIMEOUT);
}


// param: <TEXT>
doAskMenu( key user, string param ) {    
    cMenuType = MT_ASK;
    cButtons = [ "Yes", "No" ];
    cActions = [ param ];
    clearListens();    
    nList = llListen( CHATBASECHANNEL + cMenuNum, "", user, "");
    isListening = TRUE;
    llDialog( user, param, cButtons, CHATBASECHANNEL + cMenuNum );
    llSetTimerEvent(RESPONSE_TIMEOUT);   
}

////////////////////////////////////////////
default
{
    state_entry()
    {
        if (debug) llSay(0, "Loading menu... mem: "+(string)llGetFreeMemory());
        startReadConfig();
    }

    dataserver(key query_id, string data) {
        if (query_id==cqid) readConfigLine(data);
    }
    
    listen( integer channel, string name, key id, string message ) {
        llSetTimerEvent(0.0);
        clearListens();

        if (cMenuType==MT_ASK) {
            if (message=="Yes") {
                string param = llList2String( cActions, 0 ); // special case
                emitMsgSender(LM_OPTIONDATA,llDumpList2String([ cMenuName, param ],"|"));                
            }
            return;
        }
        if (cMenuType==MT_CUSTOM) {
            emitMsgSender(LM_OPTIONDATA,llDumpList2String([ cMenuName, message ],"|"));
            return;
        }
        if (cMenuType==MT_READSTRING) {
            string param = llList2String( cActions, 0 ); // special case
            emitMsgSender(LM_READSTRINGDATA,llDumpList2String([ param, message ],"|"));
            return;
        }

        integer c;
        integer nb = llGetListLength(cButtons);
        integer fnd = -1;
        for (c=0;c<nb;c++) {
            if (llList2String(cButtons,c)==message) {
                fnd = c;
                jump doneSR;
            }
        }
        @doneSR;
        if (fnd>=0) {
            integer act = llList2Integer( cActions, fnd * 2 );
            string param = llList2String( cActions, fnd * 2 + 1 );
            if (debug) {
                debugSay("r: "+(string)act+" "+param);
            }
            if (act==MC_TOMENU) {
                string tomenu = param;
                integer spos = llSubStringIndex(tomenu," ");
                if (spos>=0) tomenu = llGetSubString(tomenu,0,spos - 1);

                doMenu(id,tomenu);
                return;
            }
            if (act==MC_OPTION) {
                emitMsgSender(LM_OPTIONDATA,llDumpList2String([ cMenuName, param ],"|"));
                return;
            }
            if (act==MC_OPTIONASK) {
                doAskMenu(id,param);
                return;
            }

        }
    }
    
    link_message(integer sender_num, integer num, string str, key id) {
        if (num==LM_DOMENU) {
            cSenderID = sender_num;
            doMenu(id,str);
            return;
        }
        if (num==LM_READSTRING) {
            cSenderID = sender_num;
            cMenuType = MT_READSTRING;

            string message = str;
            string retval  = str;
            integer spos = llSubStringIndex(str," ");
            if (spos>=0) {
                message = llGetSubString( str, spos+1, -1 );
                retval = llGetSubString( str, 0, spos - 1 );
            }
            cActions = [ retval ];
            nList = llListen( 0, "", id, ""); // listen from the user only
            isListening = TRUE;
            llSay(0,message);
            llSetTimerEvent(RESPONSE_TIMEOUT);   
            return;
        }
        if (num==LM_CUSTOMMENU) {
            list par = llParseString2List(str,["~|~"],[]);

            if (llGetListLength(par)<2) {
                llSay(0,"Error: not enough parameters for custom menu");
                return;                
            }

            cMenuName = llList2String(par,0);
            string message = llList2String(par,1);
            cUser = id;
            cButtons = llList2List( par, 2, llGetListLength(par) - 1 );
            cMenuType = MT_CUSTOM;

            clearListens();    
            nList = llListen( CHATBASECHANNEL, "", cUser, "");
            isListening = TRUE;
            llDialog( cUser, message, cButtons, CHATBASECHANNEL );
            llSetTimerEvent(RESPONSE_TIMEOUT);
            return;
        }
        if (num==LM_RESETMENUSYSTEM) {
            llResetScript();
        }
    }
    
    timer() {
        llSetTimerEvent(0.0);
        clearListens();
    }
}

The "menudefs" notecard:
MENU DEFAULT
TEXT ~ Main Menu ~\nSelect a function
TOMENU SETTITLE Set title
OPTION Start vote
TOMENU CLITEM Clear Item
OPTIONASK Reset

MENU SETTITLE
TEXT Set title for which item?
OPTION 1
OPTION 2
OPTION 3

MENU CLITEM
TEXT Clear title and picture for which item?
OPTION 1
OPTION 2
OPTION 3

MENU INVOTE
TEXT Voting in progress...
OPTIONASK Clear votes
OPTIONASK Close voting

MENU VOTEDONE
TEXT Vote closed
OPTIONASK Restart voting
OPTION Tell results

A "MenuDemo" script:
// Menu demo program

//--- Menu library interface start-------------
integer LM_OPTIONDATA = 10002;
integer LM_MENULOADED = 10003;
integer LM_READSTRINGDATA = 10006;

resetMenu() {
    llMessageLinked(llGetLinkNumber(),10004,"",NULL_KEY); // 10004: LM_RESETMENUSYSTEM
}
doMenu( key user, string menuname ) {
    llMessageLinked(llGetLinkNumber(),10001,menuname,user); // 10001: LM_DOMENU
}
readString( key user, string var, string prompt ) {
    llMessageLinked(llGetLinkNumber(),10005,var+" "+prompt,user); // 10005: READSTRING
}
customMenu( key user, string menuname, string message, list buttons ) {
    llMessageLinked(llGetLinkNumber(),10007,llDumpList2String([ menuname, message ] + buttons,"~|~"),user);
}
//---Menu Library inteface end----------

default
{
    state_entry()
    {
        //resetMenu();
    }
    
    link_message(integer sender_num, integer num, string str, key id) {
        if (num==LM_OPTIONDATA) {
            llSay(0,"MenuOutput: "+str);
        }
        if (num==LM_MENULOADED) {
            llSay(0,"Menu has just been (re)loaded");   
        }
        if (num==LM_READSTRINGDATA) {
            integer wpos = llSubStringIndex(str,"|");
            string varname = llGetSubString(str,0,wpos - 1);
            string response = llGetSubString(str,wpos+1,-1);
            llSay(0,"Variable: "+varname+" String read: "+response);
        }
    }

    touch_start(integer total_number)
    {
        doMenu(llDetectedKey(0),"DEFAULT");

        // other things you can try (uncomment it)
        //customMenu(llDetectedKey(0),"CUST1","Test custom menu",["Test A","Test B","Test C"]);
        //readString(llDetectedKey(0),"FAVCOLOR","Please enter your favorite color in chat:");
    }
}

script library | examples
There are 2 comments on this page. [Display comments/form]