Jump to content

A Smorgasbord of Script Snippets


primerib1
 Share

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

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

Recommended Posts

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...

  • Like 1
  • Thanks 2
Link to comment
Share on other sites

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 by primerib1
  • Like 1
Link to comment
Share on other sites

  • 4 weeks later...

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

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 by Quistess Alpha
Link to comment
Share on other sites

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

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 by Quistess Alpha
  • Like 1
Link to comment
Share on other sites

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.)

  • Like 1
Link to comment
Share on other sites

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

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.

 

  • Like 1
Link to comment
Share on other sites

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.)

  • Like 1
Link to comment
Share on other sites

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.

  • Thanks 1
Link to comment
Share on other sites

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 by primerib1
  • Like 1
Link to comment
Share on other sites

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 by primerib1
  • Like 1
Link to comment
Share on other sites

You are about to reply to a thread that has been inactive for 493 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...