Jump to content

Help Modifying a Menu Function


Frankie Rockett
 Share

You are about to reply to a thread that has been inactive for 522 days.

Please take a moment to consider if this thread is worth bumping.

Recommended Posts

Hi
I'm asking for help with a modification I made to Rolig's excellent multi-page Menu system which is in the LSL Script Library here:
https://community.secondlife.com/forums/topic/181045-a-simple-multipage-dialog-menu-system/#comment-1382741

I am using it in a drone to select nearby avatars. I want it to return both the selected Av Name (displayed on the buttons) and also the associated UUID. To that end I modified her code to save name/uuid pairs, and to step through the 'Buttons' List in 2's rather than in 1's. It all works fine - as long as there are 9 or fewer avatars in range. If there are more and it needs to prepare a second menu 'page', it crashes with the following message broadcast to all: "llDialog: button labels must be 24 or fewer characters long". Clearly, and understandably, something is going wrong with the paging it seems to me. This is all at the edge of my understanding, so I tried some simple minded approaches like doubling her 9's and 11's to 18 and 22 respectively, but that didn't help. Days of trying to debug this are sending me bananas.  So please, can anyone see where I've gone wrong here?!

list gNames;
integer gMenuPosition;  // Index number of the first button on the current page
integer gLsn;   // Dialog Listen Handle
list avatarsInRegion;
key id;
string name;




list uListReverse( list vLstSrc ){
    integer vIntCnt = (vLstSrc != []);
    while (vIntCnt){
        vLstSrc += llList2List( vLstSrc, (vIntCnt = ~-vIntCnt), vIntCnt );
    }
    return llList2List( vLstSrc, (vLstSrc != []) >> 1, -1 );
}



Menu()
{
    integer Last;
    list Buttons;
    integer All = llGetListLength(gNames);
    if(gMenuPosition >= 9)   //This is NOT the first menu page
    {
        Buttons += "    <----";
        if((All - gMenuPosition) > 11)  // This is not the last page
        {
            Buttons += "    ---->";
        }
        else    // This IS the last page
        {
            Last = TRUE;
        }            
    }    
    else if (All > gMenuPosition + 9) // This IS the first page
    {
        if((All - gMenuPosition) > 11)  // There are more pages to follow
        {
            Buttons += "    ---->";
        }
        else    // This IS the last page
        {
            Last = TRUE;
        }            
    }
    else    // This is the only menu page
    {
        Last = TRUE;
    }
    if (All > 0)
    {
        integer b;
        integer len = llGetListLength(Buttons);
        len = len/2;
        // This bizarre test does the important work ......        
        //for(b = gMenuPosition + len + Last - 1 ; (len < 12)&&(b < All); ++b)    
        for(b = gMenuPosition + len + Last - 1 ; (len < 12)&&(b < All); b=b+2)
        {
            Buttons = Buttons + [llList2String(gNames,b)];
            len = llGetListLength(Buttons);
        }
    }
    gLsn = llListen(-12345,"","","");    
    llSetTimerEvent(30.0);
    
//        integer i;
//        integer length = llGetListLength(Buttons);
//        do
//            llOwnerSay(llList2String(Buttons, i) );
//        while(++i < length);    
    
    
    llDialog(llGetOwner(),"Targets within range:\n", Buttons, -12345);
}



default
{

    
    
// ---------------------------- S Y S T E M   F U N C T I O N S ----------------------------    
    
    listen(integer channel, string name, key id, string msg)
    // Listens to and services all menu buttons.
    {
        llListenRemove(gLsn);
        llSetTimerEvent(0.0);
        if (~llSubStringIndex(msg,"---->"))
        {
            gMenuPosition += 10;
            Menu();
        }
        else if (~llSubStringIndex(msg,"<----"))
        {
            gMenuPosition -= 10;
            Menu();
        }
        else
        {
            //Do whatever the selected button directs..... your choice
            integer n = llListFindList(gNames, [msg]);
            if(~n)
            { // shorthand for "if it is an item in the list".  If it is, 
                key k = llList2Key(gNames, n + 1);//find the next item,
                //and thus you have the avatar's uuid.

                 list test = [msg];
                 gNames = uListReverse(gNames);
                 integer NameIndex = llListFindList(gNames, test);
                 NameIndex --;
                 NameIndex = NameIndex / 2;
//llOwnerSay("DEBUG MENU choice made at position " + NameIndex + " / " + msg + " / " + (string)k);
                 llMessageLinked(LINK_THIS, NameIndex, msg, k);
            }
        }
    }
    
    
    
    timer()
    {
        llSetTimerEvent(0.0);
        llListenRemove(gLsn);
    }



    link_message(integer sender_num, integer num, string msg, key id)
    // Services request for targeting from main script.
    {
        if (msg == "target" && num ==0 ) // if num==0 ie if sending script was 'Drone 166'
        {
            llListenRemove(gLsn);
            gMenuPosition = 0;
            gNames = [];
            llSensor("", NULL_KEY, AGENT, 96.0, PI);
        }
    }
    
    
    
    sensor( integer detected )
    // Assembles list of Avatars detected by sensor function.
    {
        while(detected--)
        {
             gNames = gNames + llDetectedName(detected) + llDetectedKey(detected);
        }
        Menu();
    }
}    

Thank you for your thoughts.

Link to comment
Share on other sites

2 minutes ago, Frankie Rockett said:

It all works fine - as long as there are 9 or fewer avatars in range. If there are more and it needs to prepare a second menu 'page', it crashes with the following message broadcast to all: "llDialog: button labels must be 24 or fewer characters long". Clearly, and understandably, something is going wrong with the paging it seems to me. This is all at the edge of my understanding

The error message is referring to the content of the button text. Avatar names can be longer than 24 characters, so you can't use them directly. There's a few general ways to work around it, but they're all a bit complicated to implement.

In short, either you always take the first 24 characters of the avatar name and use that, or you use indexes for the button labels.

The first method can make things more complicated when processing the button choice, and the second method requires you to display a list that correlates each index to avatar names.

Link to comment
Share on other sites

Hi Wulfie
I'm not sure that's the issue here. In a previous iteration of this script I used a shortening function and verified that it works. It didn't help. Plus of course it worked with long names (despite not displaying all characters) as long as there were 9 or fewer avatars in a region.

I am thinking the variable gMenuPosition might be involved (possibly). Perhaps if things get out of step with a 'page 2' then maybe the function starts offering uuids instead of names to the buttons? Just my latest guess.

Link to comment
Share on other sites

Minor update:
I discovered that if I remove NameIndex = NameIndex / 2; (which I added in error thinking as Names are now 50% of the content of the list, then I should halve the count of that list - wrong!) then things improve. Specifically, I get the first full page (9 avatars) listed, plus a "---->" arrow for page 2. Going to page 2 either crashes with the original error message ( "llDialog: button labels must be 24 or fewer characters long"), or it lists a mish-mash of names from page 1 and 2, with no 'go back' button ( < ---- ) so I think I fixed one issue, but the paging is still broken here.

Edited by Frankie Rockett
Link to comment
Share on other sites

2 hours ago, Frankie Rockett said:

Minor update:
I discovered that if I remove NameIndex = NameIndex / 2; (which I added in error thinking as Names are now 50% of the content of the list, then I should halve the count of that list - wrong!) then things improve. Specifically, I get the first full page (9 avatars) listed, plus a "---->" arrow for page 2. Going to page 2 either crashes with the original error message ( "llDialog: button labels must be 24 or fewer characters long"), or it lists a mish-mash of names from page 1 and 2, with no 'go back' button ( < ---- ) so I think I fixed one issue, but the paging is still broken here.

I think you're mixing up correlation and causation.

The documentation says that llDialog will shout an error "if any button's length is longer than 24 bytes." It's not a problem if the button name is too long to be displayed visually.. it just can't reach 25 bytes.

The first page might work perfectly fine if a person with a long name isn't on that page (even if they are on sim). When you move to the next page and a new list of buttons is generated (with the long name), the error is shouted.

Edited by Wulfie Reanimator
  • Like 1
Link to comment
Share on other sites

Hi

Ok I took onboard your helpful observation and I added back in a function to shorten the names fed to llDialogue at 14 characters, which works well and the dreaded shouted error message has gone. HOwever I clearly had a secondary issue (now the primary) which still relates to the paging of the menu. What happens is, if there are 11 avatars are fewer (the Menu displays maximum) then everything works fine. The menu displays, no errors, and the selection passes back the name and the uuid to the main script. However, if there are MORE than 11, causing a 2nd menu page to be available, that page only lists uuids. Also, both the normally populated page and the 2nd uuid buttoned page are ineffective in passing back names and uuids. Can you see where my logic is broken here?

 

// Drone 166 Target's Menu

list gNames;
integer gMenuPosition;  // Index number of the first button on the current page
integer gLsn;   // Dialog Listen Handle
list avatarsInRegion;
key id;
string name;
string shortname = "";



string StringTruncate(string text, integer length)
{
    if (length < llStringLength(text))
        return llGetSubString(text, 0, length - 2);

    // else
        return text;
}


list uListReverse( list vLstSrc ){
    integer vIntCnt = (vLstSrc != []);
    while (vIntCnt){
        vLstSrc += llList2List( vLstSrc, (vIntCnt = ~-vIntCnt), vIntCnt );
    }
    return llList2List( vLstSrc, (vLstSrc != []) >> 1, -1 );
}



Menu()
{
    integer Last;
    list Buttons;
    integer All = llGetListLength(gNames);
    if(gMenuPosition >= 9)   //This is NOT the first menu page
    {
        Buttons += "    <----";
        if((All - gMenuPosition) > 11)  // This is not the last page
        {
            Buttons += "    ---->";
        }
        else    // This IS the last page
        {
            Last = TRUE;
        }            
    }    
    else if (All > gMenuPosition + 9) // This IS the first page
    {
        if((All - gMenuPosition) > 11)  // There are more pages to follow
        {
            Buttons += "    ---->";
        }
        else    // This IS the last page
        {
            Last = TRUE;
        }            
    }
    else    // This is the only menu page
    {
        Last = TRUE;
    }
    if (All > 0)
    {
        integer b;
        integer len = llGetListLength(Buttons);

        // This bizarre test does the important work ......        
        //for(b = gMenuPosition + len + Last - 1 ; (len < 12)&&(b < All); ++b)    
        for(b = gMenuPosition + len + Last - 1 ; (len < 12)&&(b < All); b=b+2)
        {
            shortname = StringTruncate(llList2String(gNames,b), 14);
//            Buttons = Buttons + [llList2String(gNames,b)];
            Buttons = Buttons + shortname;
            len = llGetListLength(Buttons);
        }
    }
    gLsn = llListen(-12345,"","","");    
    llSetTimerEvent(40.0);
     
    llDialog(llGetOwner(),"Targets within range:\n", Buttons, -12345);
}



default
{

    
    
// ---------------------------- S Y S T E M   F U N C T I O N S ----------------------------    
    
    listen(integer channel, string name, key id, string msg)
    // Listens to and services all menu buttons.
    {
        llListenRemove(gLsn);
        llSetTimerEvent(0.0);
        if (~llSubStringIndex(msg,"---->"))
        {
            gMenuPosition += 20;
            Menu();
        }
        else if (~llSubStringIndex(msg,"<----"))
        {
            gMenuPosition -= 20;
            Menu();
        }
        else
        {
            //Do whatever the selected button directs..... your choice
            integer n = llListFindList(gNames, [msg]);
            if(~n)
            { // shorthand for "if it is an item in the list".  If it is, 
                key k = llList2Key(gNames, n + 1);//find the next item,
                //and thus you have the avatar's uuid.

                 list test = [msg];
                 gNames = uListReverse(gNames);
                 integer NameIndex = llListFindList(gNames, test);
                 NameIndex --;
                 NameIndex = NameIndex / 2;
//llOwnerSay("DEBUG MENU choice made at position " + NameIndex + " / " + msg + " / " + (string)k);
                 llMessageLinked(LINK_THIS, NameIndex, msg, k);
            }
        }
    }
    
    
    
    timer()
    {
        llSetTimerEvent(0.0);
        llListenRemove(gLsn);
    }



    link_message(integer sender_num, integer num, string msg, key id)
    // Services request for targeting from main script.
    {
        if (msg == "target" && num ==0 ) // if num==0 ie if sending script was 'Drone 166'
        {
            llListenRemove(gLsn);
            gMenuPosition = 0;
            gNames = [];
            llSensor("", NULL_KEY, AGENT, 96.0, PI);
        }
    }
    
    
    
    sensor( integer detected )
    // Assembles list of Avatars detected by sensor function.
    {
        while(detected--)
        {
            gNames = gNames + llDetectedName(detected) + llDetectedKey(detected);
        }
        Menu();
    }
}    

Your observations are very welcome, as I've gone cross eyed, as is often the case when I rely on code someone else wrote :(

Edited by Frankie Rockett
Tidying up code to aid readability, removing superflous comments etc.
Link to comment
Share on other sites

Thank you KT! I was going down a rabbit hole of thinking maybe I need a small army of NPCs or something. Your suggestion is far more practical.

Another question here, picking up on Wulfie's point about shortening the names. I had been doing a simple text 'chop' of the first 24 characters in a name, but I realise that's not the same as the first 24 bytes. Casting around for how I might do that, the wiki entry on llDialog seems to have a perfect answer and quotes the following:

Quote

This snippet can be used to truncate the string without giving an error: llBase64ToString(llGetSubString(llStringToBase64(theString), 0, 31))

Just to aid my understanding, why is the parameter '31' included here? How does 31 relate to 24 UTF-8 encoded bytes?

Thank you.

Edited by Frankie Rockett
Correcting grammar.
Link to comment
Share on other sites

22 hours ago, Wulfie Reanimator said:

I think you're mixing up correlation and causation.

The documentation says that llDialog will shout an error "if any button's length is longer than 24 bytes." It's not a problem if the button name is too long to be displayed visually.. it just can't reach 25 bytes.

The first page might work perfectly fine if a person with a long name isn't on that page (even if they are on sim). When you move to the next page and a new list of buttons is generated (with the long name), the error is shouted.

I'm a great fan of the second method and, if anyone's interested, this is how I would do it (somewhat inconsistent naming system because the menu function was originally adapted from one of either Rolig's or Void Singer's examples, and i've never got round completely to converting it to my way of doing things)

integer iMax;
integer iHandle;
integer iChannel;

key kToucher;
key kChosenUUID;
list lNames;
list lUUIDs;
list lTemp;
list lDialogLabels;
list lMenuChoices;

list lMenuButtons( integer vIntPag )
{
    integer vIdxBeg = 10 * (~-vIntPag);          //-- 10 * (vIntPag - 1), enclose "~-X" in parens to avoid LSL bug
    integer vIdxMax = -~(~([] != lMenuChoices) / 10); //-- (llGetListLength( lMenuChoices ) - 1) / 10 + 1
    list vLstRtn =
      llListInsertList(
        llList2List( lMenuChoices, vIdxBeg, vIdxBeg + 9 ), //-- grab 10 dialog buttons
        (list)("  <<---(" + (string)(vIntPag + (-(vIntPag > 1) | vIdxMax - vIntPag)) + ")"), //-- back button
        -1 ) + //-- inserts back button at index 9, pushing the last menu item to index 10
      (list)("  (" + (string)(-~((vIntPag < vIdxMax) * vIntPag)) + ")--->>"); //-- add fwd button at index 11
    
    return //-- fix the order to L2R/T2B
      llList2List( vLstRtn, -3, -1 ) + llList2List( vLstRtn, -6, -4 ) +
      llList2List( vLstRtn, -9, -7 ) + llList2List( vLstRtn, -12, -10 );
}

string strCaption = "Please choose someone:";
string strChoice;
default {
  state_entry() {
  
  }

  attach(key id) {
    if(id){
      llRequestPermissions(id, PERMISSION_TAKE_CONTROLS);//needed to run in no script areas
    }
  }

  run_time_permissions(integer perm) {
    if(perm & PERMISSION_TAKE_CONTROLS){
      llTakeControls(CONTROL_ML_LBUTTON, FALSE, TRUE);
    }
  }

  touch_end(integer num_detected) {

    kToucher = llDetectedKey(0);
    lTemp = llGetAgentList(AGENT_LIST_REGION, []);//build list of avatars on the region

    lNames =[];//clear the lists of existing contents
    lUUIDs= [];
    lDialogLabels=[];
    lMenuChoices=[];

    iMax = -llGetListLength(lTemp);
    do{ //loop through lTemp and populate the lists
      key k = llList2Key(lTemp,iMax);
      lNames +=[llGetDisplayName(k)];
      lUUIDs +=[k];
    }
    while(++iMax);

    integer counter = 0 ;
    iMax = llGetListLength(lNames);
    do{
        lDialogLabels += [(string)(counter + 1)+": "+llList2String(lNames, counter)+"\n"];
        lMenuChoices +=[(string)(counter + 1)];
    }
    while(++counter < iMax);
    llOwnerSay("lMenuChoices is "+llList2CSV(lMenuChoices));

    llListenRemove(iHandle); // close 
    iChannel = llGetUnixTime()*-1;
    iHandle = llListen(iChannel, "", "", "");

      string str = "";
      integer i;
      //Display the first 10 sites in dialog
      for (i=0;i<10;++i)
      {
          str += llList2String(lDialogLabels,i);
      }

      llDialog(kToucher, strCaption+"\n"+str, lMenuButtons(1), iChannel);

  }

  listen(integer channel, string name, key id, string message) {
    llListenRemove(iHandle); // close 
    if(!llSubStringIndex(message, "  ")){//if the first two characters of the message are leading spaces, then it's the forward or back button
      string str  = "";
      integer menu =  (integer)llGetSubString( message, -~llSubStringIndex( message, "(" ), -1 );
      integer i;
      for (i=(10*(menu - 1));i<(10*menu);++i){
          str += llList2String(lDialogLabels,i);
      }
      iHandle = llListen(iChannel, "", "", "");
      llDialog(kToucher, strCaption+"\n"+str, lMenuButtons(menu), iChannel);
    }
    else{
        integer n = (integer)message -1;
        llRegionSayTo(kToucher, 0, "You chose: " + strChoice = llList2String(lNames,n)+", uuid "+(string)(kChosenUUID = llList2Key(lUUIDs, n)));
    }
  }
}

 

  • Like 1
Link to comment
Share on other sites

14 hours ago, Frankie Rockett said:

Just to aid my understanding, why is the parameter '31' included here? How does 31 relate to 24 UTF-8 encoded bytes?

I'm a bit green to this area, but here's what I think is going on: (please correct me if I get anything wrong)

UTF-8 can represent characters using a variable number of bytes, between 1 (8bits) and 4 (32bits). Common ASCII characters for example only need 1 byte, while the more interesting unicode symbols can require up to 4.

Base64 works by expanding 3 bytes of arbitrary binary data into 4 bytes (padding any extra space with "=" characters). The output is printed as alphanumeric characters plus a few other symbols. But keep in mind that it's always going to output in multiples of 4 characters.

So the bespoke code snippet is taking your input string and encoding it to base64. It then takes a substring of that from index 0 to 31... basically taking the first 32 characters of that newly encoded base64 string. It then converts that substring back to UTF-8.

As it turns out, a base64 string of 32 characters long is just enough to contain the first 24 bytes worth of the original UTF-8 input string. Consider this breakdown:

string theString = "abcdefghijklmnopqrstuvwxyz";
llOwnerSay("1:"+theString);
llOwnerSay("2:"+llStringToBase64(theString));
llOwnerSay("3:"+llGetSubString(llStringToBase64(theString), 0, 31));
llOwnerSay("4:"+llBase64ToString(llGetSubString(llStringToBase64(theString), 0, 31)));

/*
1:abcdefghijklmnopqrstuvwxyz
2:YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=
3:YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4
4:abcdefghijklmnopqrstuvwx
*/

The input string is 26 ASCII characters (and 26 bytes) long and so the resulting base64 string is 36 bytes long. Remember that base64 encodes 3 bytes into 4 byte segments. So breaking it down further...

abc  def  ghi  jkl  mno  pqr  stu  vwx  yz
└┬┘  └┬┘  └┬┘  └┬┘  └┬┘  └┬┘  └┬┘  └┬┘  └┬┘
┌┴─┐ ┌┴─┐ ┌┴─┐ ┌┴─┐ ┌┴─┐ ┌┴─┐ ┌┴─┐ ┌┴─┐ ┌┴─┐ 
YWJj ZGVm Z2hp amts bW5v cHFy c3R1 dnd4 eXo=

We can see how each block of 3 characters (which are 1 byte each) fits into their corresponding 4 byte base64 segment. So we can take the first 8 segments (32 characters/bytes) and convert it back to UTF-8 which gives us the original first 24 bytes, and as such, the first 24 ASCII characters of the input string.

And this could be considered the "worst" case, where every character from the input sting was 1 byte. If the input string had characters that were larger than 1 byte, the resulting conversion would give us fewer characters in the end.

I hope I explained that correctly...

Edited by Fenix Eldritch
typos
  • Like 2
Link to comment
Share on other sites

Fenix!

That was a phenomenal explanation! I do hope your abilities are not confined to this forum and that you're a teacher or lecturer in RL with your gift for elucidation benefiting many, because that really was patient clarity incarnate and so perfectly well unpacked. I want to send you more than thanks, but also applause! Sincerely, I understand everything you explained and am thrilled to have this new, richer understanding. Thank you so very much!

Thank you to Innula too for her alternative formulation of the Menu approach. Next time I may well start from your model.

Thank you to everyone who helped me navigate these choppy waters !

Link to comment
Share on other sites

You are about to reply to a thread that has been inactive for 522 days.

Please take a moment to consider if this thread is worth bumping.

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now
 Share

×
×
  • Create New...