Jump to content

Can scripts (or maybe objects in which they're embedded) have persistent, readable, writeable memory?


LoneWolfiNTj
 Share

Recommended Posts

I'd like to improve my visitor counter and security orb by providing them with non-volitile memory, that is, memory that doesn't go "poof" on "recompile script" or "reset script".

For read-only memory, that's easy: put notecards in the object's inventory. I use that approach with my radios (which have a "radio stations" notecard in them which people can edit using their "Edit" tool), and also with my security orb (which has "White", "Black", and "Admin" lists on notecards which a user can edit).

But what about non-volatile read-write memory??? I'd love to be able to do that, but I don't know a way.

Sure, a user can edit a notecard, but can a script? I haven't found any LSL functions that edit notecards. Can this be done?

If not, are there other ways for a script to have non-volatile memory that it can edit?

  • Like 1
Link to comment
Share on other sites

No, there's no way a script can edit a notecard.

One thing you can do is to use a separate data storage script that doesn't recompile when the main script does. Another is to use the key-value pairs feature of experiences. And then there's external data storage accessible using HTTP, like Google Sheets.

For small amounts of data you can use prim attributes like floating text and the description field, or you can encode values in things like colour  or even shape, size and rotation attributes.

Link to comment
Share on other sites

cUeslM2.png

If you're a premium member you can create an experience and use experience keys.

Otherwise, Plan B ..

You can put some data in objects description or as invisible hovertext, just be aware its not much (off the top of my head .. 128 bytes? and limited to ascii ) and these may accept more data than is actually saved when the attachment is removed or the avatar relogs (although it may persist so long as the object exists, so you could overload the field, reset the script and get it back).  There is also lots of primitive params that can be used to smuggle away numeric values, etc.

This isn't going to be much data so don't be expecting to store great long lists of keys, of course you can always add more prims and use them as extra memory.

It's not much data .. and probably more work than Plan C.

just getting your code to talk to an external web service.

  • Like 1
Link to comment
Share on other sites

46 minutes ago, KT Kingsley said:

No, there's no way a script can edit a notecard.

Rats. 😞 I really wish LL would implement that; that would be sooo convenient.

47 minutes ago, KT Kingsley said:

... key-value pairs feature of experiences ... external data storage ...

Nope, I'd like to keep it "all in SL", and available to anyone who buys an object from me, even if they're not a paying member.

49 minutes ago, KT Kingsley said:

...separate data storage script that doesn't recompile...

Ok, that will have to be my way forward, apparently.

Ok, I wrote this script:


// Data Storage Script. DON'T RECOMPILE OR RESET THIS.

list data;

default
{
    state_entry()
    {
        llSetText
        (
            "DATA STORAGE DEVICE.\n"
          + "DO NOT RESET OR RECOMPILE SCRIPT.",
            <1,0,0>,
            1
        );
        llListen(1354268192, "", NULL_KEY, "");
    }

    touch_end(integer total_number)
    {
        integer n = llGetListLength(data);
        integer i = 0;
        for ( i = 0 ; i < n ; ++i )
        {
            llOwnerSay(llList2String(data, i));
        }
    }

    listen(integer ch, string nm, key id, string tx)
    {
        data += [tx];
    }
}

One-way data trap; data goes in, and can be retrieved and printed, but cannot be cleared (other than by violating the floating text by recompiling or resetting the script).

I tried running the data-storage script on the same prim that has a script that collects data and sends it (via channel 1354268192) to the data storage script:

default
{
    state_entry()
    {
        ; // Nothing needs to be done.
    }

    touch_end(integer total_number)
    {
        llSay(1354268192, llGetTimestamp());
    }
}

But it didn't work. Can two scripts on the same prim not communicate with each other?

But perhaps it's better to have them as separate objects anyway, not even linked, so if the "Data Send" (or "Visitor Counter" or "Security Orb") script gets reset, the Data Storage script doesn't. What's the comm range? I think I saw "20m" mentioned somewhere? And memory capacity is about 64KB if I remember right?

Link to comment
Share on other sites

46 minutes ago, LoneWolfiNTj said:

Can two scripts on the same prim not communicate with each other?

Not with a listen() event* but that's a relatively inefficient way to do it anyway, compared with the link_message() event, which is what you'd want to use here…

… assuming you really want to use a separate script for memory storage. The disadvantage is that users are apt to simply reset all scripts in an object (because the Build menu has a handy choice to do that) which would wipe the memory in both scripts at the same time.

To clarify about Experience persistent store (the "key value pair" storage), the end user doesn't need to be premium, but they do need to be on land that enables the Experience for which the script was compiled. And technically, the developer doesn't need to be a premium subscriber either, but they do need to be included by a premium subscriber as a collaborator in their Experience.

[ETA: I meant to address this part, too;

46 minutes ago, LoneWolfiNTj said:

But perhaps it's better to have them as separate objects anyway, not even linked, so if the "Data Send" (or "Visitor Counter" or "Security Orb") script gets reset, the Data Storage script doesn't. What's the comm range? I think I saw "20m" mentioned somewhere? And memory capacity is about 64KB if I remember right?

Right, that would avoid the object-level reset problem, assuming the users are really willing to keep two objects rezzed for a simple visitor counter. The range of llSay() is indeed 20m; for llWhisper() it's 10m and llShout() is about 100m, however the more efficient way to do this is using llRegionSayTo() where the UUID of the recipient object is specified, which prevents waking up scripts in other in-range objects (and keeps the sim from having to figure out which objects are in-range in the first place).

_____________________
* as documented with the listen() event: 

  • A prim cannot hear/listen to chat it generates.
Edited by Qie Niangao
  • Like 1
Link to comment
Share on other sites

1 hour ago, Qie Niangao said:

assuming the users are really willing to keep two objects rezzed for a simple visitor counter

Or several visitor counters or security orbs (or other data collectors), all talking on the same channel to the same Data Storage Device. That way a user doesn't have to poll multiple devices, just click a single Data Storage Device to see all the data.

1 hour ago, Qie Niangao said:

llRegionSayTo() where the UUID of the recipient object is specified

I don't know what the UUIDs of the rezzed objects are going to be, though. Are they the same as parent in inventory? Wait, let me test that. Nope, two copies of my Data Storage Device rezzed 1 second and 1 meter apart have very different UUIDs. So, they're copy specific and unknowable in advance.

So I think I'll go with having the data collector(s) use llRegionSay (rather than llRegionSayTo) to say the data to the Data Storage Device, using some very negative channel number such as -1354268192. That way a user could have 1 central storage device collecting data from 17 senders scattered all over a 1-sim private island.

Now I only have to worry about memory exhaustion. Perhaps when memory gets nearly-full, have the Data Storage Device delete the first (oldest) 10 lines from the data list. Ya, that should work. I'm expecting lines of < 100 ASCII characters per line, so should be able to store over 600 lines. Perhaps simplify it to "whenever # of lines reachs 610, delete oldest 10 lines". Would work for data such as "TimeStamp LegacyName Coordinates". Each line of visitor-counter or security-orb data might read like "2021-10-14T06:28:01.889720Z FriedGreenTomatoes Resident (113, 92, 34)". That's only 69 bytes. So, ya, I could store 600 lines of such data easy.

Of course it would take ages to scroll through a list of 600 lines of such text dumped by llOwnerSay to a user's Nearby Chat box. Maybe have a menu with buttons "Limit" (to set max # of lines of data stored, range 10-600) and "Print" (to print all stored data). That way someone  wanting to see just "last 10 people to pry around my house" wouldn't be inundated with data, but a paranoid music club bouncer could see a list of the last 600 people to visit the club. Ya, I think I'll do that.

Link to comment
Share on other sites

1 hour ago, LoneWolfiNTj said:

I don't know what the UUIDs of the rezzed objects are going to be, though. Are they the same as parent in inventory? Wait, let me test that. Nope, two copies of my Data Storage Device rezzed 1 second and 1 meter apart have very different UUIDs. So, they're copy specific and unknowable in advance.

Right, not in advance, but there are several ways to get the sender to know the recipient's UUID. One common way is for one or the other or both to be rezzed by something that keeps track of the UUID of objects it rezzes (or, if the recipient rezzes the sender, then that sender can know the recipient's UUID from llGetObjectDetails()' OBJECT_REZZER_KEY.

One reason (besides efficiency) to favor llRegionSayTo when convenient is that it avoids any chance of the wrong recipient getting the message. That can also be avoided by making sure every recipient is addressed over a unique channel, perhaps by embedding some bits from a hash of the owner key, for a receiving object with the same owner as the sender(s) and unique among receivers in range.

Link to comment
Share on other sites

You could try playing with Google sheets, which has been discussed several times during the last year or so in these forums, I think.

It wouldn't be completely synchronous, since you'll have to set the target G-sheet to publish updates to a CSV file, from which your inworld LSL scripts could read the data when they needed it.

If you simply want to read and write data (e.g. urls for radio stations, player's current score in a game) I'd use LL's experience tools and KVP.    If you want to be able to read or analyse the data yourself, send it to G-sheets.   Or use both methods, to be safe.

Use KVP if you want to know when a particular resident last visited the store.   Use GSheets if you want a visitor list.

Edited by Quartz Mole
  • Like 2
  • Thanks 1
Link to comment
Share on other sites

18 hours ago, Qie Niangao said:

avoids any chance of the wrong recipient getting the message

Now that I think about it, that could be a problem if I'm using llRegionSay for, say, a visitor lister or security system, and someone on a different parcel in the same region also buys my security system. The two systems will use the same channel #, so every Data Store in the region will collect all data from every Visitor Scanner, including those in other parcels. Worst-case scenario: a person flying over a house gets teleported home because he/she falsely triggers a security system on a different property? I'm not sure if that's possible, but that would be bad. Ugh.

I have the basics of a "Multi-Point Visitor Lister" system working quite well right now: I scattered Visitor Scanners all over a sandbox, and they all reported back to a central Data Store, which recorded the timestamps, legacy names, and region coordinates of every sighting of an avatar by every scanner (4 people scattered hundreds of meters apart).

However, for this to be practical and ethical, I need to differentiate "JohnSmith Resident's security system" from "JillJones Resident's security system" so they don't cross-talk. I don't like the idea of talking to specific UUIDs for this purpose because it limits the ways in which the devices can be rezzed, and I want users to be able to rez as many scanners and data stores from their inventory as they like. But using the owner's UUID to alter the channel number seems an excellent idea; I think I'll do that.

Ah, I think this should do:

default
{
    state_entry(){;}
    touch_start(integer n)
    {
        integer own_hash = llHash((string)llGetOwner());
        integer channel  = -1 * own_hash;
        llOwnerSay((string)channel);
    }
}

It says "-1598157784" in my case, and it should be different for every owner, but the same for every object rezzed by that owner. Which is just what I need.

Edit, a couple hours later: Or, better, make the hash specific to Owner + Object, as in this example script from the wiki article on llHash():

integer PickChannel()
{
    key      owner  = llGetOwner();
    string   object = llGetObjectName();
    integer  channel = -1 * llHash((string)owner + object);
    return channel;
}

 

Edited by LoneWolfiNTj
added alternate script
Link to comment
Share on other sites

a caveat on the wiki for llHash is that the probability of a collision is quite high, the greater the number of open listeners. The wiki estimates that for 1,000 scripts on the server each using a unigue channel the chances of a collision is 1 in 10,000 

so what we should do is also check that the sender has the same owner as the listener. Example:

listen (..., key id, ...)
{
   if (llGetOwnerKey(id) == llGetOwner())
   {
      .. sender of message has the same owner as the listener
   }

}

 

a further caveat with llHash is that it returns both positive and negative values, and it can return 0, so we want to account for these. Example to ensure a channel in the negative range <= -1 using owner uuid and a unique app token known to both sender and listener scripts:
 

string APP_TOKEN = "app token is something unique to my app known to both sender and listener";

// ensure channel <= -1
integer channel = -1 * ((llHash((string)llGetOwner() + APP_TOKEN) & 0x7FFFFFE) + 1);
Edited by Mollymews
acc
  • Like 2
  • Thanks 1
Link to comment
Share on other sites

7 hours ago, Mollymews said:
integer channel = -1 * ((llHash((string)llGetOwner() + APP_TOKEN) & 0x7FFFFFE) + 1);

 

Why &0x7FFFFFFE rather than 0x7FFFFFFF? (also I think you're missing an F? ) . . . Oh, to make it even instead of odd so adding 1 will never get you to 0. . .

Couldn't you just set the sign bit?

integer channel = (llHash((string)llGetOwner() + APP_TOKEN) | 0x80000000);

 

  • Thanks 1
Link to comment
Share on other sites

3 hours ago, Quistess Alpha said:

Why &0x7FFFFFFE rather than 0x7FFFFFFF? (also I think you're missing an F? ) . . . Oh, to make it even instead of odd so adding 1 will never get you to 0. . .

Couldn't you just set the sign bit?

integer channel = (llHash((string)llGetOwner() + APP_TOKEN) | 0x80000000);

 

yes i missed a F sorry.   Should be 0x7FFFFFFE

i went the long way as that was how it was presented in the OP. When we use -1 * then we have to mask out the high and low bits and add 1

when we don't then

-1 * 0 = 0

-1 * 0xFFFFFFFF = 1

 

Edited by Mollymews
remove a statement which might not be true
  • Like 1
Link to comment
Share on other sites

4 minutes ago, Mollymews said:

| 0x80000000 is more efficient.

The trivial efficiency aside, I was more thinking that masking/setting just one bit rather than two, doubles your effective address range and makes collisions even more unlikely, which was another theme of earlier posts (or maybe it was another thread. . .)

Link to comment
Share on other sites

1 hour ago, Quistess Alpha said:

The trivial efficiency aside, I was more thinking that masking/setting just one bit rather than two, doubles your effective address range and makes collisions even more unlikely, which was another theme of earlier posts (or maybe it was another thread. . .)

yes this is correct.  n | x80000000, alternatively . -1 | n (cross the alternate out as is not true)

either of these does what you say

the range is -1 to -power(2, 31)+1

whereas -1 * range + 1 is -1 to -power(2,30)

 

Edited by Mollymews
arith clarity
  • Like 1
Link to comment
Share on other sites

15 hours ago, Mollymews said:

check that the sender has the same owner as the listener

Great idea! Thanks, I'll implement that.

15 hours ago, Mollymews said:

caveat with llHash is that it returns both positive and negative values, and it can return 0

I already thought of that last night, did some testing, and found that it returned values in the range of a C type "int32_t" (-2147483648 through  +2147483647), so I wrote the following to force the channel # to be in the closed range [ -1999999999 , -1000000000 ] :

key     own_key   = NULL_KEY;    // key  of owner
string  own_nam   = "";          // name of owner
string  mfg       = "";          // manufacturer
string  system    = "";          // name of the system this object is a part of
string  object    = "";          // name of ths object
string  SysID     = "";          // ID string for this system
string  ObjID     = "";          // ID string for this object
integer channel   = 0;           // comm channel

init()
{
    llSetText
    (
        "DATA STORAGE DEVICE.\n"
      + "DO NOT RESET OR RECOMPILE SCRIPT.",
        <1,0,0>,
        1
    );
    own_key = llGetOwner();
    own_nam = llKey2Name(own_key);
    mfg     = "'s Convoluted Sentience ";
    system  = "Visitor System";
    object  = " Data Store";
    SysID   = own_nam + mfg + system;
    ObjID   = SysID + object;
    channel = llHash(SysID);
         if ( channel > 0           ) {channel -= 2000000000;}
    else if ( channel > -1000000000 ) {channel -= 1000000000;}
    llListen(channel, "", NULL_KEY, "");
    llOwnerSay("Now listening on channel " + (string)channel + ".");
    llOwnerSay(ObjID + " is now initialized.");
}

 

  • Like 1
Link to comment
Share on other sites

On 10/15/2021 at 5:19 AM, LoneWolfiNTj said:

Now that I think about it, that could be a problem if I'm using llRegionSay for, say, a visitor lister or security system, and someone on a different parcel in the same region also buys my security system. The two systems will use the same channel #, so every Data Store in the region will collect all data from every Visitor Scanner, including those in other parcels. Worst-case scenario: a person flying over a house gets teleported home because he/she falsely triggers a security system on a different property? I'm not sure if that's possible, but that would be bad. Ugh.

 

So you don't mix up two instances of the same object, belonging to different  owners, you may want to keep an eye on llGetOwnerKey(id) when tracking messages.   

Checking the parcel details for the parcel on which an object is located will often tell you what you need to know, too.

You might also want to look at the various land ownership functions.

Edited by Innula Zenovka
  • Like 1
Link to comment
Share on other sites

1 hour ago, Innula Zenovka said:

llGetOwnerKey(id)

Yep, in the last few days I've incorporated that (and other techniques) to keep my objects talking only to who they should, without having to rely on the UUIDs of individual objects (which solves some problems, but causes others).

I start by making the channel # based on a llHash() of a string consisting of (OwnerName + ManufacturerName + SystemName). For example, "Innula Zonovka's Convoluted Sentience Turnip Smashing System". That way, every object in that system is using the same channel #, but objects owned or created by others or in different systems would use different channel numbers.

Then in my listen() I check for both channel # and owner:

if (ch == channel && llGetOwnerKey(id) == llGetOwner())
{
    ...do stuff...
}

to make sure I'm on the right channel and talking to an object with the same owner. That seems to be working well so far.

  • Like 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...