Dialogs/Next Engine.lsl

620 lines
No EOL
21 KiB
Text

/*
Copyright Aria's Creations 2024
Dialog Module - Next Engine
v1.0.080124.1829
08-2024 INITIAL RELEASE
* Finished hooking up listener and timer
* Add ability to replace menu buttons (Toggles)
06-2024 INITIAL VERSION
* Manage memory more efficiently than all other current systems
* Channels for dialogs will always be negative
* User-input prompts will use a positive channel and inform the user the channel number for manual longer input
* Event signal on link message when requesting only a channel number
* Two helper functions allow serializing and deserializing menus from the linkset_data
* Menus are not stored in memory, only a pointer to it
* Menus consist of a linksetdata json object
* In-memory objects consist of channel number, menu name pointing to LSD, ID listening to
* ID may not be a user for listen only. In this case the menu name is blank.
* Dynamic Menus.
* Menu Construction. We as part of the startup process, send a signal to all scripts asking for menu / registration. All subsequent buttons will be treated as possible submenus, a signal will be sent for them recursively to build up the menu hierarchy.
*/
integer LINK_SIGNAL_GEN_CHANNEL = 0601241;
integer LINK_SIGNAL_CHANNEL_BACK = 0601242;
integer LINK_SIGNAL_SHOW_MENU = 0601243;
integer LINK_SIGNAL_MENU_TIMEOUT = 0601244;
integer LINK_SIGNAL_REREGISTER_MENUS= 0601245;
integer LINK_SIGNAL_QUERY_MENU = 0602241;
integer LINK_SIGNAL_REGISTER_MENU = 0602242;
integer LINK_SIGNAL_RESET = 0602243;
integer LINK_SIGNAL_MENU_DATA = 0602244;
integer LINK_SIGNAL_MENU_BACK = 0801241;
integer LINK_SIGNAL_REPLACE_BUTTON = 0804241;
string PREVIOUS_MENU = "<--";
string EXIT_MENU = "-exit-";
string NEXT_MENU = "-->";
integer g_iDebugIndent=0;
returnMenu(key kID, string sMenu, integer iPage, string sReply) {
llMessageLinked(LINK_SET, LINK_SIGNAL_MENU_BACK, llList2Json(JSON_OBJECT, ["menu", sMenu, "page", iPage, "reply", sReply]), kID);
}
integer DEBUG_ENABLED() {
return FALSE;
}
DEBUG_FUNC(integer iEnter, string sLabel, list lParams) {
if(!DEBUG_ENABLED()) return;
if(iEnter){
llOwnerSay(MakeIndent() + "ENTER " + sLabel + " [" + llList2CSV(lParams) + "]");
g_iDebugIndent++;
} else {
g_iDebugIndent --;
if(g_iDebugIndent<0)g_iDebugIndent=0;
llOwnerSay(MakeIndent() + "LEAVE " + sLabel + " [" + llList2CSV(lParams) + "]");
}
}
DEBUG_STMT(integer iEnter, string sLabel) {
if(!DEBUG_ENABLED()) return;
if(iEnter) {
llOwnerSay(MakeIndent() + "STMT " + sLabel);
g_iDebugIndent ++;
}else {
g_iDebugIndent--;
if(g_iDebugIndent<0)g_iDebugIndent=0;
llOwnerSay(MakeIndent() + "RET " + sLabel);
}
}
DEBUG(string sMsg) {
if(!DEBUG_ENABLED()) return;
llOwnerSay(MakeIndent() + " > " + sMsg);
}
string MakeIndent()
{
integer i = 0;
string sIndent = "";
for(i = 0;i<g_iDebugIndent;i++){
sIndent += " ";
}
return "[" + llGetScriptName() + "] " + sIndent;
}
string SLURL(key kID){
return "secondlife:///app/agent/"+(string)kID+"/about";
}
integer IsLikelyUUID(string sID)
{
if(sID == (string)NULL_KEY)return TRUE;
if(llStringLength(sID)==32)return TRUE;
key kID = (key)sID;
if(kID)return TRUE;
if(llStringLength(sID) >25){
if(llGetSubString(sID,8,8)=="-" && llGetSubString(sID, 13,13) == "-" && llGetSubString(sID,18,18) == "-" && llGetSubString(sID,23,23)=="-") return TRUE;
}
return FALSE;
}
integer IsLikelyAvatarID(key kID)
{
if(!IsLikelyUUID(kID))return FALSE;
// Avatar UUIDs always have the 15th digit set to a 4
if(llGetSubString(kID,8,8) == "-" && llGetSubString(kID,14,14)=="4")return TRUE;
return FALSE;
}
integer IsListOfIDs(list lIDs)
{
integer i=0;
integer end = llGetListLength(lIDs);
for(i=0;i<end;i++){
if(IsLikelyUUID(llList2String(lIDs,i)))return TRUE;
}
return FALSE;
}
integer generateChannel(integer iPositive) {
integer iRand = llRound(llFrand(0xFFFF));
if(iPositive) {
return llAbs(iRand);
}else
{
if(iRand > 0) return -iRand;
else return iRand;
}
}
// This function serializes a named menu to the buffer
saveMenu(string sName, list lButtons, list lUtilityButtons, integer iMenuVersion, string sMenuText) {
llLinksetDataWrite("menus." + sName, llList2Json(JSON_OBJECT, [
"buttons", llList2Json(JSON_ARRAY, lButtons),
"utility", llList2Json(JSON_ARRAY, lUtilityButtons),
"version", iMenuVersion, // This flag is used to know if on startup a script needs to overwrite this menu definition,
"prompt", sMenuText
]));
}
updateMenu(string sName, string sJson) {
llLinksetDataWrite("menus." + sName, sJson);
}
// This function deserializes a menu from the buffer
string readMenu(string sName) {
string sMenu = llLinksetDataRead("menus." + sName);
return sMenu;
}
integer STRIDE = 7;
integer STRIDE_ID = 0;
integer STRIDE_PATH = 1;
integer STRIDE_CHANNEL = 2;
integer STRIDE_TEXT = 3;
integer STRIDE_PAGE = 4;
integer STRIDE_HANDLE = 5;
integer STRIDE_TIMER = 6;
list g_lListeners = [];
integer MENU_TIMER = 30;
integer GENERAL_TIMER = 120;
startListen(key kAv, integer iChannel, string sMenuName, string sMenuText, integer iTimer) {
integer iIndex=llListFindList(g_lListeners, [kAv]);
if(iIndex == -1) {
integer iListener = llListen(iChannel, "", kAv, "");
g_lListeners += [kAv, sMenuName, iChannel, sMenuText, 0, iListener, llGetUnixTime() + iTimer];
}else {
stopListen(kAv);
startListen(kAv, iChannel, sMenuName, sMenuText, iTimer);
}
}
stopListen(key kAv) {
integer iIndex = llListFindList(g_lListeners, [kAv]);
if(iIndex != -1) {
g_lListeners = llDeleteSubList(g_lListeners, iIndex, iIndex+STRIDE-1); // Stride is 6 - ID, path, channel, text, page, listen handle
}
}
updatePage(key kID, integer iPage) {
integer iIndex = llListFindList(g_lListeners, [kID]);
if(iIndex!=-1) {
g_lListeners = llListReplaceList(g_lListeners, [iPage], iIndex+STRIDE_PAGE, iIndex+STRIDE_PAGE);
}
}
integer getListenTimer(key kID) {
integer iIndex = llListFindList(g_lListeners, [kID]);
if(iIndex != -1) {
integer iVal = llList2Integer(g_lListeners, iIndex+STRIDE_TIMER);
return iVal;
}else return -1;
}
integer getPageNumber(key kID) {
integer iIndex = llListFindList(g_lListeners, [kID]);
if(iIndex != -1) {
return llList2Integer(g_lListeners, iIndex + STRIDE_PAGE);
} else return -1;
}
updateTimer(key kID, integer iNewTimer) {
integer iIndex = llListFindList(g_lListeners, [kID]);
if(iIndex != -1) {
g_lListeners = llListReplaceList(g_lListeners, [llGetUnixTime() + iNewTimer], iIndex + STRIDE_TIMER, iIndex + STRIDE_TIMER);
}
}
string getMenuID(key kID) {
integer iIndex = llListFindList(g_lListeners, [kID]);
if(iIndex != -1) {
return llList2String(g_lListeners, iIndex + STRIDE_PATH);
} else return "";
}
integer getChannel(key kID) {
integer iIndex = llListFindList(g_lListeners, [kID]);
if(iIndex!=-1) {
return llList2Integer(g_lListeners, iIndex+STRIDE_CHANNEL);
} else return -1;
}
string CreateBlankMenu() {
return llList2Json(JSON_OBJECT, [
"prompt", "",
"buttons", "[]",
"utility", llList2Json(JSON_ARRAY, []),
"version", "1"
]);
}
string AddMenuButton(string sJson, string sButton) {
list lButtons = llJson2List(llJsonGetValue(sJson, ["buttons"]));
lButtons += [sButton];
return llJsonSetValue(sJson, ["buttons"], llList2Json(JSON_ARRAY, lButtons));
}
integer jsonValueExists(string sJson, list lElems) {
if(llJsonValueType(sJson, lElems) == JSON_INVALID) return FALSE;
else return TRUE;
}
integer hasPreviousPage(integer page){
if(page == 1) {
return FALSE;
}else return TRUE;
}
integer hasNextPage(integer page, integer maxPages) {
if(page == maxPages) return FALSE;
else return TRUE;
}
list getNavigatorButtons(integer page, integer max) {
if(hasPreviousPage(page)) {
if(hasNextPage(page,max)) {
return [PREVIOUS_MENU, EXIT_MENU, NEXT_MENU];
}else return [PREVIOUS_MENU, EXIT_MENU, " "];
}else {
if(hasNextPage(page,max)) {
return [" ", EXIT_MENU, NEXT_MENU];
}else return [" ", EXIT_MENU, " "];
}
}
// This function takes the json menu as a parameter, calculates the maximum number of pages from total buttons 12 - utility - page buttons if applicable
integer calcMaxPages(string sMenu) {
// Retrieve buttons and utility lists from JSON
list lButtons = llJson2List(llJsonGetValue(sMenu, ["buttons"]));
list lUtility = llJson2List(llJsonGetValue(sMenu, ["utility"]));
// Combine buttons and utility lists
list lFinal = lButtons + lUtility;
// Calculate the total number of buttons
integer totalButtons = llGetListLength(lFinal);
// Check if the total buttons exceed the limit for a single page
if (totalButtons > 12) {
// Define constants
integer navigation = 3; // 3 navigation buttons (previous, next, and exit)
integer utility = llGetListLength(lUtility); // Number of utility buttons
integer buttons = llGetListLength(lButtons); // Number of actual buttons
// Calculate available space for actual buttons on each page
integer availableSpacePerPage = 12 - navigation - utility;
// Calculate the total number of pages needed
integer maxPages = (integer)llCeil((float)buttons / (float)availableSpacePerPage);
return maxPages;
} else {
// If total buttons are 12 or less, only one page is needed
return 1;
}
}
string getPage(string sMenu, integer iPage, integer iCheckUUID) {
DEBUG_FUNC(TRUE, "getPage", [sMenu, iPage, iCheckUUID]);
// Retrieve buttons and utility lists from JSON
list lButtons = llJson2List(llJsonGetValue(sMenu, ["buttons"]));
list lUtility = llJson2List(llJsonGetValue(sMenu, ["utility"]));
// Define constants
integer navigation = 3; // 3 navigation buttons (previous, next, and page indicator)
integer utility = llGetListLength(lUtility); // Number of utility buttons
integer buttonsPerPage = 12 - navigation - utility; // Available space for actual buttons on each page
// Calculate the total number of pages using the existing function
integer totalPages = calcMaxPages(sMenu);
// Ensure iPage is within valid range
if (iPage < 1) iPage = 1;
if (iPage > totalPages) iPage = totalPages;
// Calculate start and end indices for the buttons on the requested page
integer startIndex = (iPage - 1) * buttonsPerPage;
integer endIndex = startIndex + buttonsPerPage - 1;
// Get the actual buttons to be displayed on this page
list lPageButtons = [];
// Add utility buttons to the page
lPageButtons += lUtility;
// Add the buttons for the specified page
integer i;
for (i = startIndex; i <= endIndex && i < llGetListLength(lButtons); ++i) {
lPageButtons += llList2String(lButtons, i);
}
// Add navigation buttons
lPageButtons = getNavigatorButtons(iPage, totalPages) + lPageButtons;
// Loop over the buttons, check if UUID, replace with a number instead if UUID
string sExtraText = " ";
if(!iCheckUUID) jump returnMenu;
i =0;
integer end = llGetListLength(lPageButtons);
integer idNum = 0;
for(i=0;i<end;i++) {
string sButton = llList2String(lPageButtons, i);
if(IsLikelyAvatarID(sButton)) {
lPageButtons = llListReplaceList(lPageButtons, [(string)idNum], i,i);
idNum++;
sExtraText += SLURL(sButton) + "\n";
}
}
@returnMenu;
DEBUG("Buttons list: " + llList2CSV(lPageButtons));
string jsReply=llList2Json(JSON_OBJECT, ["text", sExtraText, "buttons", "~!~" + llDumpList2String(lPageButtons, "~!~") + "~!~"]);
DEBUG_FUNC(FALSE, "getPage", [jsReply]);
return jsReply;
}
AppendMenuButton(string sMenu, string sButton) {
string json = readMenu(sMenu);
AddMenuButton(json, sButton);
updateMenu(sMenu, json);
}
default
{
state_entry() {
llLinksetDataDeleteFound("menus", "");
llMessageLinked(LINK_SET, LINK_SIGNAL_QUERY_MENU, "root", "");
llLinksetDataDeleteFound("colors", "");
// Query color menu buttons and color pairs. Stride 3 instead of 2
}
timer() {
integer i = 0;
integer end = llGetListLength(g_lListeners);
for(i=0;i<end;i+=STRIDE) {
key kID = llList2Key(g_lListeners, i + STRIDE_ID);
integer iTimer = getListenTimer(kID);
if(llGetUnixTime() > iTimer) {
stopListen(kID);
llRegionSayTo(kID, 0, "Listener Timed Out");
return;
}
}
}
changed(integer t) {
if(t & CHANGED_INVENTORY) {
llSleep(2);
llResetScript();
}
}
listen(integer c,string n,key i,string m) {
// Get the menu path
DEBUG_FUNC(TRUE, "listen", [i,m]);
string sMenuID = getMenuID(i);
// Get the current page number
integer iPage = getPageNumber(i);
DEBUG("Get current page number for menu " + sMenuID + ": " + (string)iPage);
string jsMenu = "";
if(sMenuID != "") jsMenu = readMenu(sMenuID);
DEBUG("Menu Json: " + jsMenu);
integer iMaxPages = 0;
if(sMenuID != "") iMaxPages = calcMaxPages(jsMenu);
DEBUG("Get max number of pages: " + (string)iMaxPages);
if(m == PREVIOUS_MENU) {
DEBUG_STMT(TRUE, "Previous Menu");
// Update the time remaining
updateTimer(i, MENU_TIMER);
iPage--;
if(iPage < 0) iPage=0;
string sPage = getPage(jsMenu, iPage, TRUE);
list lPageButtons = llParseString2List(llJsonGetValue(sPage, ["buttons"]), ["~!~"], []);
updatePage(i, iPage);
string sAppend = llJsonGetValue(sPage, ["text"]);
if(iMaxPages > 1) sAppend += "\n\nPage " + (string)iPage + "/" + (string)(iMaxPages-1);
llDialog(i, llJsonGetValue(jsMenu, ["prompt"]) + sAppend, lPageButtons, getChannel(i));
DEBUG_STMT(FALSE, "Previous Menu");
} else if(m == NEXT_MENU) {
DEBUG_STMT(TRUE, "Next Menu");
// Update the time remaining
updateTimer(i, MENU_TIMER);
iPage++;
if(iPage < iMaxPages-1) iPage=iMaxPages-1;
string sPage = getPage(jsMenu, iPage, TRUE);
list lPageButtons = llParseString2List(llJsonGetValue(sPage, ["buttons"]), ["~!~"], []);
updatePage(i, iPage);
string sAppend = llJsonGetValue(sPage, ["text"]);
if(iMaxPages > 1) sAppend += "\n\nPage " + (string)iPage + "/" + (string)(iMaxPages-1);
llDialog(i, llJsonGetValue(jsMenu, ["prompt"]) + sAppend, lPageButtons, getChannel(i));
DEBUG_STMT(FALSE, "Next Menu");
} else if(m == " ") {
DEBUG_STMT(TRUE, "Empty Button");
// Update the time remaining
updateTimer(i, MENU_TIMER);
string sPage = getPage(jsMenu, iPage, TRUE);
list lPageButtons = llParseString2List(llJsonGetValue(sPage, ["buttons"]), ["~!~"], []);
updatePage(i, iPage);
string sAppend = llJsonGetValue(sPage, ["text"]);
if(iMaxPages > 1) sAppend += "\n\nPage " + (string)iPage + "/" + (string)(iMaxPages-1);
llDialog(i, llJsonGetValue(jsMenu, ["prompt"]) + sAppend, lPageButtons, getChannel(i));
DEBUG_STMT(FALSE, "Empty Button");
} else if(m == EXIT_MENU) {
DEBUG_STMT(TRUE, "Exit Menu");
stopListen(i);
DEBUG_STMT(FALSE, "Exit Menu");
} else {
DEBUG_STMT(TRUE, "Process Response");
// Stop listening, and send a signal
string sPage = "";
if(jsMenu != "") sPage = getPage(jsMenu, iPage, FALSE);
DEBUG("Parse Buttons");
// Check Message, if message is found in list, it isn't UUID.
// If message is not found, it is likely uuid
list lButtons = llParseString2List(llJsonGetValue(sPage, ["buttons"]), ["~!~"], []);
integer iIndex = llListFindList(lButtons, [m]);
DEBUG("Buttons : " + llDumpList2String(lButtons, " ~ "));
if(iIndex==-1 && sMenuID != "") {
// Loop over to find the index
integer iIDNum = (integer)m;
integer x = 0;
integer xe = llGetListLength(lButtons);
integer iID = 0;
for(x=0;x<xe;x++) {
string sButton = llList2String(lButtons, x);
if(IsLikelyAvatarID(sButton)) {
if(iIDNum == iID) {
returnMenu(i, sMenuID, iPage, sButton);
jump epr;
}
iID ++;
}
}
} else {
// Send reply signal
returnMenu(i, sMenuID, iPage, m);
}
@epr;
stopListen(i);
DEBUG_STMT(FALSE, "Process Response");
}
DEBUG_FUNC(FALSE, "listen", []);
}
link_message(integer s,integer n,string m,key i) {
if(n == LINK_SIGNAL_GEN_CHANNEL) {
integer iChan = generateChannel((integer)m);
startListen(i, iChan, "", "", GENERAL_TIMER);
llMessageLinked(LINK_SET, LINK_SIGNAL_CHANNEL_BACK, (string)iChan, "");
} else if(n == LINK_SIGNAL_SHOW_MENU) {
string jsMenu = readMenu(llJsonGetValue(m,["menu"]));
integer iChan = generateChannel(FALSE);
startListen(i, iChan, llJsonGetValue(m,["menu"]), llJsonGetValue(jsMenu, ["prompt"]), MENU_TIMER);
// Show the dialog window now after constructing pages if needed.
integer maxPages = calcMaxPages(jsMenu);
// Now show the dialog for page 1
integer iPage = 1;
if(jsonValueExists(m, ["page"])) iPage = (integer)llJsonGetValue(m,["page"]);
string jsPage = getPage(jsMenu, iPage, TRUE);
list lPageButtons = llParseString2List(llJsonGetValue(jsPage, ["buttons"]), ["~!~"], []);
updatePage(i, iPage);
string sAppend = llJsonGetValue(jsPage, ["text"]);
if(maxPages>1) sAppend += "\n\nPage " + (string)iPage+"/" + (string)(maxPages-1);
//llOwnerSay("DEBUG\n" + llJsonGetValue(sMenu, ["prompt"]) + sAppend + "\n\n" + llJsonGetValue(sPage, ["buttons"]));
llDialog(i, llJsonGetValue(jsMenu, ["prompt"])+sAppend, lPageButtons, getChannel(i));
} else if(n == LINK_SIGNAL_REGISTER_MENU) {
string sMenuJson = llLinksetDataRead("menus." + (string)i);
if(sMenuJson == "" ) {
// No current menu
sMenuJson = CreateBlankMenu();
sMenuJson = AddMenuButton(sMenuJson, m);
}else sMenuJson = AddMenuButton(sMenuJson, m);
llLinksetDataWrite("menus."+(string)i, sMenuJson);
llSleep(0.5);
llMessageLinked(LINK_SET, LINK_SIGNAL_QUERY_MENU, (string)i + "/" + m, "");
} else if(n == LINK_SIGNAL_RESET) {
llResetScript();
} else if(n == LINK_SIGNAL_MENU_DATA) {
// This signal is used to replace metadata, like the menu text, or utility buttons.
// Based on what values are set in the json object, we will replace those elements in the actual menu
string sJson = readMenu(i);
if(jsonValueExists(m, ["utility"])) {
sJson = llJsonSetValue(sJson, ["utility"], llJsonGetValue(m, ["utility"]));
}
if(jsonValueExists(m, ["prompt"])) {
sJson = llJsonSetValue(sJson, ["prompt"], llJsonGetValue(m, ["prompt"]));
}
llLinksetDataWrite("menus." + (string)i, sJson);
} else if(n == LINK_SIGNAL_REPLACE_BUTTON) {
string jsMenu = readMenu(llJsonGetValue(m, ["menu"]));
list lButtons = llJson2List(llJsonGetValue(jsMenu, ["buttons"]));
integer iIndex = llListFindList(lButtons, [llJsonGetValue(m,["btn"])]);
if(iIndex != -1) {
lButtons = llListReplaceList(lButtons, [llJsonGetValue(m, ["newBtn"])], iIndex, iIndex);
jsMenu = llJsonSetValue(jsMenu, ["buttons"], llList2Json(JSON_ARRAY, lButtons));
updateMenu(llJsonGetValue(m,["menu"]), jsMenu);
}
}
}
linkset_data(integer iAction, string sKey, string sValue) {
if(iAction == LINKSETDATA_RESET) {
llMessageLinked(LINK_SET, LINK_SIGNAL_REREGISTER_MENUS, "", "");
llResetScript();
}
}
}