primerib1 Posted January 26 Share Posted January 26 Some functions I keep using again and again, so I thought I'll just share here... Get rest of string after a certain 'needle' Especially to parse something like "<some-key>=<some-string>", which I use a lot for HUD-to-HUD protocol... string SubStringAfter(string haystack, string needle, integer offset) { return llDeleteSubString(haystack, 0, llSubStringIndex(haystack, needle) + offset); } Delete LSD Keys based on regex pattern Until LL implements BUG-233195, I'll keep using this to clean up the LSD from temporary hashtables... DeleteKeys(string pattern) { list lsd_keys = llLinksetDataFindKeys(pattern, 0, 0); integer idx = -(lsd_keys != EMPL); if (idx) do { llLinksetDataDelete(llList2String(lsd_keys, idx)); } while (++idx); } Sleep(-ish) until LSD Key is populated I use this so I can offload heavy-duty stuff to another script, trigger that other script using llMessageLinked(), then just wait until a certain LSD key appears (containing the result[s] from that other script) string WaitForLSD(string lsd_key) { float start = llGetTime(); string value; while ("" == (value = llLinksetDataRead(lsd_key))) { llSleep(0.2); if (10 < (llGetTime() - start)) jump Timeout; } @Timeout; return value; } Check if user / user's active group is allowed You'll need to pre-populate LSD somehow with key-value pair of "_acc:<UUID>":"allow/block" integer Allowed(key avid, key objid) { if (NULL_KEY == objid) objid = llList2Key(llGetAttachedList(avid), 0); string gr_verdict = llLinksetDataRead("_acc:" + (string)llGetObjectDetails(objid, (list)OBJECT_GROUP)); if ("block" == gr_verdict) return FALSE; string av_verdict = llLinksetDataRead("_acc:" + (string)avid); if ("block" == av_verdict) return FALSE; return (("allow" == gr_verdict) || ("allow" == av_verdict)); } Remove item considering attachment state If item is rezzed in-world, die. If it's attached, detach. With optional info message. DeRez(string die_msg, string detach_msg) { // Ensure no 'leftover' hover text llSetLinkPrimitiveParamsFast(LINK_SET, ((list)PRIM_TEXT + "" + <0, 0, 0> + (float)0)); if (llGetAttached()) { if (detach_msg) llOwnerSay(detach_msg); llSleep(1); llRequestPermissions(llGetOwner(), PERMISSION_ATTACH); } else { if (die_msg) llOwnerSay(die_msg); llSleep(2); llDie(); } } // --- and in the state, add the following handler: run_time_permissions(integer perm) { if (perm & PERMISSION_ATTACH) { llDetachFromAvatar(); } } Process configuration from notecards, ignoring empty lines and #comment lines string $CardName; integer $Idx; key $Reader; StartCard(string card_name) { $Reader = llGetNotecardLine(($CardName = card_name), ($Idx = 0)); } // --- in the state, add the following event handler // Trigger using the following line somewhere else: // StartCard("Card1"); dataserver(key req, string data) { if ($Reader == req) { llSetText("Processing configuration ... " + (string)$Idx, <1, 1, 0>, 1); integer is_eof; if (!(is_eof = EOF == data)) { $Reader = llGetNotecardLine($CardName, ++$Idx); if ("" == (data = llStringTrim(data, STRING_TRIM))) return; if (!llSubStringIndex(data, "#")) return; } if ("Card1" == $CardName) { if (is_eof) { // If no additional processing, just trigger read of the next card StartCard("Card2"); } else { // TODO: Process data } return; } if ("Card2" == $CardName) { if (is_eof) { // It's possible we want to do something first before going to the next card // In such case, the trigger for the next card must be inside the function ProcessCard2(); } else { // TODO: Process data } return; } // ... and so on ... } } I'll keep adding to this smorgasbord collection as I end up with oft-used snippets... 1 2 Link to comment Share on other sites More sharing options...
primerib1 Posted January 27 Author Share Posted January 27 (edited) Get Access Level of User / User's Active Group After some (unrelated) discussions about collars & relays in another place, I had an epiphany and realized that the Allowed() function can easily be repurposed to a GetAccessLevel() function. That will allow "multi-level" access, such as giving some people "Trusted" level (nearly all functions), and "Owner" level (all functions). So I transformed the function like so: integer GetAccessLevel(key avatar_id, key obj_id) { integer av_verdict = (integer)llLinksetDataRead("_acc:" + (string)avatar_id); if (av_verdict < 0) return 0; if (obj_id == NULL_KEY) obj_id = llList2Key(llGetAttachedList(avatar_id), 0); integer gr_verdict = (integer)llLinksetDataRead("_acc:" + (string)llGetObjectDetails(obj_id, (list)OBJECT_GROUP)); if (gr_verdict < 0) return 0; if (av_verdict | gr_verdict) { if (av_verdict >= gr_verdict) return av_verdict; else return gr_verdict; } return 0; } Again, you'll need to pre-populate the LSD, assigning a value to the "_acc:<UUID>" keys as follows: Any negative number = explicit block. This takes precedence. Positive number = explicit allow. The function returns the higher between Avatar level and Group level This allows, for instance: "Trusted" users have level "5" "Owner" users have level "10" "SuperUsers" have level "99" Zero happens if the key is not found in LSD, resulting in typecasting an empty string to integer, which results in 0. CAVEAT: My function is simplified in that it treats negative and zero the same: Return zero. So on the calling side, I can simply reject access by testing !GetAccessLevel() (notice the Logical Not operator). If you want to treat zero (that is, unlisted) differently (e.g., you want to implicitly allow anyone except those in the blacklist), you'll have to modify this function. The details on how to do the modification is left as an exercise for the reader 😉 Edited January 27 by primerib1 1 Link to comment Share on other sites More sharing options...
primerib1 Posted February 18 Author Share Posted February 18 Parsing a parameter string Given a message that looks like this: COMMAND|param=val|param=val|param=val I give you two ways to parse the string: Method 1: Convert to LSD-based "dict/hashtable" integer List2Dict(list alist, string dict_name) { // every list element that looks like "<key>=<value>" will be converted to 'dict'; // list elements that do not 'look like that' will be ignored. integer i = -(alist != EMPL); if (!i) return 0; integer count; string dkey = "_d:" + dict_name + ":"; integer eqpos; string elem; do { if (~(eqpos = llSubStringIndex((elem = llList2String(alist, i)), "="))) { // Only enter here if eqpos != -1 (which when bit-negated result in 0) llLinksetDataWrite( dkey + llDeleteSubString(elem, eqpos, -1), llDeleteSubString(elem, 0, eqpos) ); ++count; } } while(++i); return count; } string GetDictItem(string dict_name, string dict_key) { return llLinksetDataRead("_d:" + dict_name + ":" + dict_key); } Invoke it like so: if (!List2Dict(llParseString2List(message, ["|"], [])), "params") { llOwnerSay("Can't parse message to dict!"); } string version = GetDictItem("params", "version"); Drawbacks: Slow compared to Method 2 Need to create a temporary list Benefits: Persistent without consuming heap space Global for all scripts throughout the linkset Method 2: Perform some string-slicing string GetParam(string message, string param, string psep, string vsep) { integer st = llSubStringIndex(message, psep + param + vsep); if (!~st) return ""; // Delete up to and including param separator. // Then find where the next param begins. The value is up to the next param minus one. // If no next param, just use -1 for the llGetSubString later. if (~(st = llSubStringIndex((message = llDeleteSubString(message, 0, st)), psep))) --st; return llGetSubString(message, llSubStringIndex(message, vsep) + 1, st); } Invoke it like so: string version = GetParam(message, "version", "|", "="); Drawbacks: Parse the same string multiple times: Once per each parameter name Need additional step to make it available for all other scripts in the linkset Benefits: Likely faster No temporary list(s) needed Link to comment Share on other sites More sharing options...
Quistess Alpha Posted February 20 Share Posted February 20 (edited) On 2/18/2023 at 8:32 AM, primerib1 said: COMMAND|param=val|param=val|param=val Presuming you have a choice over the message protocol, it's probably better to leverage built-in functions for JSON: // if you're being really persnickety about execution time, define all static lists in global variables: list gCOMMAND = ["COMMAND"]; list gPARAM1 = ["param1"]; list gPARAM2 = ["param2"]; //... // presumably, you get the message from a listen event, or possibly http communication: string message = "{COMMAND=CMD,param1=value1,param2=value2}"; string CMD = llJsonGetValue(message,gCOMMAND); if(CMD=="Command1") { string param1 = llJsonGetValue(message,gPARAM1); string param2 = llJsonGetValue(message,gPARAM2); // and so on, then do something with the parameters. } https://wiki.secondlife.com/wiki/Json_usage_in_LSL You can even convert to JSON from |, = format: integer push_message_to_JSON_LSD(string message, list separators); // { string message = "CMD|param=value|param2=value2"; integer ind = llSubStringIndex(message,"|"); string command = llDeleteSubString(message,ind,-1); // if the message has no "|", the final character will be deleted. message = llDeleteSubString(message,0,ind); if(message) { message = llList2Json(llParseString2List(message,["|","="])); llLinksetDataWrite("Dict:"+command,message); return TRUE; } return false; } string read_JSON_LSD(string cmd, string param) { string ret = llJsonGetValue(llLinksetDataRead("Dict:"+cmd),param); if(ret==JSON_INVALID) return ""; return ret; } //example usage: string message = "CMD|param=value|param2=value2"; push_message_to_JSON_LSD(message,["|","="]); string value = read_JSON_LSD("CMD","param"); // should == "value" from the message. (not debugged) Something like that might be a medium between your method 1 and 2, slightly less efficient than 1 on the read, but possibly more efficient than 1 on the write. Edited February 20 by Quistess Alpha Link to comment Share on other sites More sharing options...
primerib1 Posted February 21 Author Share Posted February 21 On 2/21/2023 at 12:01 AM, Quistess Alpha said: // presumably, you get the message from a listen event, or possibly http communication: string message = "{COMMAND=CMD,param1=value1,param2=value2}"; Ahah that's not a valid JSON. A valid JSON will look like this: string message = "{\"COMMAND\"=\"CMD\",\"param1\"=\"value1\"...}"; So many double-quotes that all need to be escaped 😑 That said, I hate JSON, so I'm staying away from JSON whenever possible. Link to comment Share on other sites More sharing options...
Quistess Alpha Posted February 21 Share Posted February 21 (edited) 1 hour ago, primerib1 said: Ahah that's not a valid JSON. Oops, and my push_to_JSON_LSD function has a few errors as well ( need to use the values in the separators parameter instead of literals, and llParseString needs an extra empty list parameter ). If/when I need to use JSON I never write the literal string anyway, it's safer to generate it with llJsonSetValue or llList2Json. string message; message = llJsonSetValue(message,["command"],"CMD"); message = llJsonSetValue(message,["param1"],"value1"); // etc. 1 hour ago, primerib1 said: That said, I hate JSON, so I'm staying away from JSON whenever possible. JSON seems very 'love it or hate it'. I used to rather dislike it, but I've come around slightly. If you need a key-value store within a string, using native LSL functions is more "natural" than making user functions, and if you're looking for efficiency, the comma-sepparated-value (CSV) conversion functions are pretty robust (correct handling of 3 or 4 valued vectors (vector/rotation), even though they contain commas), if you can handle a fixed argument order. Edited February 21 by Quistess Alpha 1 Link to comment Share on other sites More sharing options...
Love Zhaoying Posted February 21 Share Posted February 21 Ugh, since I use JSON in every script, and now LSD in nearly every new script, I'll avoid the discussion unless someone gets stuck! Link to comment Share on other sites More sharing options...
Coffee Pancake Posted February 22 Share Posted February 22 I've been quietly trying to pretend JSON doesn't exist, I don't think it's noticed me. 2 3 Link to comment Share on other sites More sharing options...
Love Zhaoying Posted February 22 Share Posted February 22 7 hours ago, Coffee Pancake said: I've been quietly trying to pretend JSON doesn't exist, I don't think it's noticed me. Yes, you can keep using lists forever, and never see the benefits. Fun! Link to comment Share on other sites More sharing options...
primerib1 Posted February 23 Author Share Posted February 23 On 2/22/2023 at 3:36 AM, Quistess Alpha said: string message; message = llJsonSetValue(message,["command"],"CMD"); message = llJsonSetValue(message,["param1"],"value1"); // etc. My eyes start to twitch at all the literal lists there... That's why I use a home-brewed protocol because I can simply use string concatenation: string message = CMD + "|param1=" + value1 + "|param2=" + value2 ... Or if I'm feeling fancy I can use "\n" as the separator so: string message = CMD + "\nparam1=" + value1 + "\nparam2=" + value2 ... So if I use "\n" as the parameter separators, the GetParam() call becomes: string v1 = GetParam(message, "param1", "\n", "="); string v2 = GetParam(message, "param2", "\n", "="); ... (Typecast as needed if the parameter is supposed to have a non-string value such as integer, vector, key, etc.) Link to comment Share on other sites More sharing options...
Quistess Alpha Posted February 23 Share Posted February 23 5 minutes ago, primerib1 said: That's why I use a home-brewed protocol because I can simply use string concatenation: Not to belabor the point, but yes there are optimizations you can do depending on context, (I.E. use global variables for list literals) and how you shuffle the (in)efficiency around between execution speed and memory usage is also context dependent (that said, if you want to be space-efficient, I'd argue you shouldn't be trying to make dictionaries in the first place) Link to comment Share on other sites More sharing options...
Love Zhaoying Posted February 23 Share Posted February 23 For my own implementation of something akin to a "GetParam()" call ("GetParm()" in my case) - I am using a string. This is the only non-JSON thing I am really using for much in the current project. The idea is: I needed a "stack". "New" items get appended to the beginning of the string "gParms": gParms = sNewParm + ";" + gParms; And I'm just using a ";" separator. Getting the "top" entry / parameter requires just searching the next ";" character. It's working great - since I have a few "global stacks" (parameter stack, function stack, call stack) that all work this way, I use a different Global for each, and a base function "GetParmBase()" that I can pass the "Global#". "GetParmBase(2)" gets the next entry from one specific global "stack", etc. This turned out to be a nice "balance" because there is no way to do a "stack" in JSON. While JSON has Arrays, there's no "insert" - just "append" and "delete". This "string" implementation works out much, much better than using lists. 1 Link to comment Share on other sites More sharing options...
Love Zhaoying Posted February 23 Share Posted February 23 On 1/26/2023 at 11:29 AM, primerib1 said: Sleep(-ish) until LSD Key is populated I use this so I can offload heavy-duty stuff to another script, trigger that other script using llMessageLinked(), then just wait until a certain LSD key appears (containing the result[s] from that other script) string WaitForLSD(string lsd_key) { float start = llGetTime(); string value; while ("" == (value = llLinksetDataRead(lsd_key))) { llSleep(0.2); if (10 < (llGetTime() - start)) jump Timeout; } @Timeout; return value; } I like this one in particular. I've been thinking about how I want to do something similar, without using the linkset_data() event. Too bad there is no LSL function to say WHEN you want a specific script to be triggered by LSD changes. (Like a listener, but for LSD changes.) 1 Link to comment Share on other sites More sharing options...
primerib1 Posted February 23 Author Share Posted February 23 27 minutes ago, Love Zhaoying said: I like this one in particular. I've been thinking about how I want to do something similar, without using the linkset_data() event. It's particularly useful if you want to invoke another script like a function. For example, this is a snippet from my "main" script: llLinksetDataDelete("_mem"); llMessageLinked(LINK_THIS, 0, "freemem?", "othermenu"); llOwnerSay( "GroomHUD v" + GROOMHUD_VER + " initialized, free mem: " + (string)llGetFreeMemory() + "(main), " + WaitForLSD("_mem") + "(othermenu)" ); llLinksetDataDelete("_mem"); And the related code in the "othermenu" script: link_message(integer sender, integer num, string str, key id) { if ("othermenu" != (string)id) return; // Not for us if ("" == str) return; else if ("freemem?" == str) llLinksetDataWrite("_mem", (string)llGetFreeMemory()); else ... // Handle other messages here } I don't have to write a separate event handler just to handle this one thing. I also don't need to open a listener etc. I'm sure there are more than one way to approach this, but this is the solution I use and so far it has been very good. 1 Link to comment Share on other sites More sharing options...
primerib1 Posted February 24 Author Share Posted February 24 (edited) On 2/24/2023 at 3:04 AM, Love Zhaoying said: I like this one in particular. After some eyeballing and careful counting of bytes, I think this version is more optimized: string WaitForLSD(string lsd_key) { string value; // Total timeout is abs(i * 0.2), in this case abs(-50 * 0.2) == 10 seconds (minimum) integer i = (integer)-50; do { if ((value = llLinksetDataRead(lsd_key))) return value; llSleep(0.2); } while (++i); // Last ditch attempt at reading after the very last sleep return llLinksetDataRead(lsd_key); } I haven't tested this one extensively though. Edit: Come to think of it, if we do llSleep() as the first step, WaitForLSD() can also function as a guaranteed GC trigger. And we can save another function call replacing last line with return ""; ... though at the cost of minimum delay of 0.2 seconds before the function returns, which may or may not be significant according to your use case & situation. Edited February 25 by primerib1 1 Link to comment Share on other sites More sharing options...
primerib1 Posted March 2 Author Share Posted March 2 (edited) Regular Expression Search/Match So I leveraged the only place a regex is available: LSD integer RegexMatch(string regex, string str) { // Backup previous value just in case it's already defined. // If it doesn't exist we'll get an empty string "" which is okay. // Later on when restoring, setting a key value to "" will result in deletion of key // So we won't be consuming LSD space. string prev = llLinksetDataRead(str); llLinksetDataWrite(str, "1"); // Dummy value. But must be a non-empty string list candidates; list needle = (list)str; integer i; // Grab at most 100 candidates. We don't want to run out of heap space while ((candidates = llLinksetDataFindKeys(regex, i, 100))) { // Might want to try to trigger GC here if you keep running out of heap space // Just uncomment the following line: //llSleep(0.02); // Or you can get fancy and just trigger GC every, say, 4th iteration or so // You'll need a separate variable to keep track of the number of iterations, though. if (~llListFindList(candidates, needle)) { i = TRUE; jump regex_match_success; } i += 100; } // If we exit here, that means we've exhausted the LSD and can't find str i = FALSE; @regex_match_success; llLinksetDataWrite(str, prev); return i; } WARNINGS: Because of BUG-233015, there is possibility that if str has an 'illegal' character in it, it might break your item. I did put in a cleanup near the end of the function, but I don't know the full nature of the bug, so it might or might not be effective I haven't tested this extensively, so there might be some buggy behavior. Will try to create errata if I find some bugs. Edited March 2 by primerib1 1 Link to comment Share on other sites More sharing options...
Recommended Posts
Please sign in to comment
You will be able to leave a comment after signing in
Sign In Now