Frankie Rockett Posted November 10, 2022 Share Posted November 10, 2022 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 More sharing options...
Wulfie Reanimator Posted November 10, 2022 Share Posted November 10, 2022 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 More sharing options...
Frankie Rockett Posted November 10, 2022 Author Share Posted November 10, 2022 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 More sharing options...
Frankie Rockett Posted November 10, 2022 Author Share Posted November 10, 2022 (edited) 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 November 10, 2022 by Frankie Rockett Link to comment Share on other sites More sharing options...
Wulfie Reanimator Posted November 10, 2022 Share Posted November 10, 2022 (edited) 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 November 10, 2022 by Wulfie Reanimator 1 Link to comment Share on other sites More sharing options...
Frankie Rockett Posted November 11, 2022 Author Share Posted November 11, 2022 (edited) 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 November 11, 2022 by Frankie Rockett Tidying up code to aid readability, removing superflous comments etc. Link to comment Share on other sites More sharing options...
Frankie Rockett Posted November 11, 2022 Author Share Posted November 11, 2022 A Footnote: it's fiendishly difficult to test this, requiring as it does a rez area with a dozen or more avatars in the region. Is there any way of faking that environment that anyone knows of? Link to comment Share on other sites More sharing options...
KT Kingsley Posted November 11, 2022 Share Posted November 11, 2022 In the sensor event you could use a loop to populate the list with several copies of your own UUID, or just append the sensor result to itself a couple of times. 1 Link to comment Share on other sites More sharing options...
Frankie Rockett Posted November 11, 2022 Author Share Posted November 11, 2022 (edited) 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 November 11, 2022 by Frankie Rockett Correcting grammar. Link to comment Share on other sites More sharing options...
Innula Zenovka Posted November 11, 2022 Share Posted November 11, 2022 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))); } } } 1 Link to comment Share on other sites More sharing options...
Fenix Eldritch Posted November 11, 2022 Share Posted November 11, 2022 (edited) 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 November 12, 2022 by Fenix Eldritch typos 2 Link to comment Share on other sites More sharing options...
Frankie Rockett Posted November 11, 2022 Author Share Posted November 11, 2022 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 More sharing options...
Recommended Posts
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