Difference between revisions of "LlLinksetDataWrite"

From Second Life Wiki
Jump to navigation Jump to search
m (Added caveat note regarding combined linkset datastore exceeding maximum capacity)
m
 
(12 intermediate revisions by 4 users not shown)
Line 8: Line 8:
When these functions are called, the [[linkset_data]] event is triggered in all scripts running in the linkset with an action of [[Template:LSL_Constants_Linkset_Data|LINKSETDATA_UPDATE]], or [[Template:LSL_Constants_Linkset_Data|LINKSETDATA_DELETE]] if the pair is deleted.
When these functions are called, the [[linkset_data]] event is triggered in all scripts running in the linkset with an action of [[Template:LSL_Constants_Linkset_Data|LINKSETDATA_UPDATE]], or [[Template:LSL_Constants_Linkset_Data|LINKSETDATA_DELETE]] if the pair is deleted.


The linkset datastore can contain up to 65536 bytes (64 kB) of data and has no impact on script memory usage aside from the functions and events used to interact with it.  Every pair written to the datastore consumes a number of bytes in the datastore equal to the length of {{LSLP|name}} plus the length of {{LSLP|value}}, plus an additional 32 bytes if written using '''llLinksetDataWriteProtected'''.
The linkset datastore can contain up to 131072 bytes (128 KiB) of data and has no impact on script memory usage aside from the functions and events used to interact with it.  Every pair written to the datastore consumes a number of bytes in the datastore equal to the length of {{LSLP|name}} plus the length of {{LSLP|value}}, plus an additional 32 bytes if written using '''llLinksetDataWriteProtected'''.


|constants={{LSL Constants Linkset_Data Returns}}
|constants={{LSL Constants Linkset_Data Returns}}
Line 40: Line 40:
** Rewriting an existing value to a '''name:value''' pair returns LINKSETDATA_NOUPDATE.
** Rewriting an existing value to a '''name:value''' pair returns LINKSETDATA_NOUPDATE.
** Writing an empty string to a nonexistent name in the datastore returns LINKSETDATA_NOTFOUND.
** Writing an empty string to a nonexistent name in the datastore returns LINKSETDATA_NOTFOUND.
* Linking and unlinking prims produces predictable but potentially unwanted results:
* There is currently no way to write to or read from a linkset datastore from another linkset.
** When linking a prim to a linkset, the combined linkset datastore includes all pairs from both datastores.
* The datastore is accessible from the entire linkset but acts as a property of the root prim alone. Therefore, linking and unlinking prims produces the following results:
*** If any pairs have conflicting {{LSLP|name}}s, the combined linkset datastore will keep the original linkset's pair and silently drop the new prim's pair.
** When linking one linkset to another linkset, the combined linkset datastore includes all pairs from both datastores.
*** If the combined linkset datastore would exceed 65536 bytes, pairs from the newly linked prim will be added to the combined linkset datastore up to the limit. It is not currently clear in what order pairs are added, so there is no way to predict which will be dropped.
*** If any pairs have conflicting {{LSLP|name}}s, the combined linkset datastore will keep the pair from the original linkset and will silently drop conflicting pairs from newly added prim(s).
*** If the combined linkset datastore would exceed 131072 bytes, pairs from the newly linked prim(s) will be added to the combined linkset datastore up to the limit. It is not currently clear in what order pairs are added, so there is no way to predict which will be dropped.
** When unlinking a child prim from a linkset, the datastore remains in the original linkset and the child prim (now its own root prim) has an empty datastore.
** When unlinking a child prim from a linkset, the datastore remains in the original linkset and the child prim (now its own root prim) has an empty datastore.
*** If the datastore is too large to cache in a script to be rewritten after unlinking, you may need to devise a custom method of porting the datastore from the original linkset to the child prim, if necessary.
** When unlinking a root prim from a linkset, the datastore remains in the newly-unlinked root prim and the remaining prims in the original linkset have an empty datastore.
** When unlinking a root prim from a linkset, the datastore remains in the newly-unlinked root prim and the remaining prims in the original linkset have an empty datastore.
* There is no limit on the size of {{LSLP|value}} aside from the total datastore limit, so care should be taken when writing very large values that could crash other scripts in the linkset via [[linkset_data]].
** If a script does not define a [[linkset_data]] event, it will not load any event parameters into its memory when the datastore is written to, so the script should not crash from another script in the linkset writing to the datastore.
** If this is a possible risk, consider using '''llLinksetDataWriteProtected''', which does not send {{LSLP|value}} in [[linkset_data]], with a static {{LSLP|pass}}.
* It is possible for data to be rolled back to a previous state if the datastore is stored in an object that is restored via a simulator rollback, or in an attachment that is not properly saved back to the server on logout.
** Viewer crashes can cause attachment states to not be saved, so care should be taken when using these functions in attachments, because datastore rollbacks are likely to occur on occasion for attachments.
|examples=
|examples=
<source lang="lsl2">
<syntaxhighlight lang="lsl2">
default
default
{
{
Line 60: Line 67:
     }
     }
}
}
</source>
</syntaxhighlight>
|helpers=
|helpers=
=== Securing Against Unexpected Changes ===
=== Securing Against Tampering ===
<source lang="lsl2">
<syntaxhighlight lang="lsl2">
// While there is no "standard" for names, it's a good idea to take some steps to prevent against other scripts accidentally overwriting your data.
// While there is no "standard" for names, it's a good idea to take some steps to prevent against other scripts accidentally overwriting your data.
// Since the datastore is shared among the entire linkset, one way to avoid name conflicts is to include the prim's UUID in the data pair's name.
// Since the datastore is shared among the entire linkset, one way to avoid name conflicts is to include the prim's UUID in the data pair's name.
// This example also monitors for llLinksetDataReset and refreshes the datastore in response, which is usually good practice if you want to keep your data!
// This example also monitors for llLinksetDataReset/unlinks and refreshes the datastore in response, which is usually good practice if you want to keep your data!


key this_uuid;
key this_uuid;
Line 107: Line 114:
         }
         }
     }
     }
    changed(integer change)
    {
        if (change & CHANGED_LINK)
        {
            // Linkset has changed!
            // Generally, this can happen for a few reasons (see Caveats above), including when an avatar sits on the object. In certain circumstances, this resets the datastore.
            // However, since the linkset data functions are very quick, it is easier to just write the data on every change to be safe for this example whether or not the datastore was reset.
            llLinksetDataWrite((string)llGetKey() + "-foo", lsd_data); // Write the lsd_data string into this prim UUID's pair - in this case we can't use llLinksetDataRead because the datastore might be empty
        }
    }
}
</syntaxhighlight>
=== Simple blacklist management ===
<syntaxhighlight lang="lsl2">integer gDialogChannel;
integer gDialogHandle;
integer gManagingBlocks;
startDialog(key person)
{
    gManagingBlocks = 0;
    gDialogHandle = llListen(gDialogChannel, "", person, "");
    llDialog(person, "\nSelect action", ["List blocks", "Add block", "Remove block"], gDialogChannel);
    llSetTimerEvent(60);
}
stopDialog()
{
    llSetTimerEvent(0);
    llListenRemove(gDialogHandle);
}
}
</source>
 
default
{
 
    on_rez(integer sp)
    {
        llResetScript();
    }
 
    state_entry()
    {
        gDialogChannel = (integer)(llFrand(-10000000)-10000000);
        llListen(PUBLIC_CHANNEL, "", NULL_KEY, "");;
    }
 
    timer()
    {
        stopDialog();
    }
 
    touch_start(integer nd)
    {
        key toucherKey = llDetectedKey(0);
        if (toucherKey == llGetOwner())
        {
            startDialog(toucherKey);
        }
    }
 
    listen(integer channel, string name, key id, string message)
    {
 
        if (llGetAgentSize(id) == ZERO_VECTOR)
        {
            return;
        }
 
        if (channel == gDialogChannel)
        {
            stopDialog();
            if (gManagingBlocks)
            {
                message = llStringTrim(message, STRING_TRIM);
                if ((key)message)
                {
                    if (gManagingBlocks == 1)
                    {
                        llOwnerSay("Addition request has been sent to the blacklist storage");
                        llLinksetDataWrite("blocklist:" + message, "1");
                    }
                    else
                    {
                        llOwnerSay("Removal request has been sent to the blacklist storage.");
                        llLinksetDataDelete("blocklist:" + message);
                    }
                }
                else
                {
                    llOwnerSay("The UUID '" + message + "' appears to be invalid.");
                }
                startDialog(id);
            }
            else if (message == "List blocks")
            {
                list blocks = llLinksetDataFindKeys("^blocklist:", 0, 0);
                integer listLength = llGetListLength(blocks);
                llOwnerSay("Blacklist items: " + (string)listLength);
                integer i;
                while (i < listLength)
                {
                    string record = llGetSubString(llList2String(blocks, i), 10, -1);
                    llOwnerSay("- secondlife:///app/agent/" + record + "/about" + " - " + record);
                    ++i;
                }
                blocks = [];
                startDialog(id);
            }
            else if (message == "Add block" || message == "Remove block")
            {
                string label = "add to";
                gManagingBlocks = 1;
                if (message == "Remove block")
                {
                    gManagingBlocks = 2;
                    label = "remove from";
                }
                gDialogHandle = llListen(gDialogChannel, "", id, "");
                llTextBox(id, "\nPlease specify one single avatar UUID you'd like to " + label + " the blacklist storage.", gDialogChannel);
                llSetTimerEvent(60);
            }
            return;
        }
 
        if (llGetListLength(llLinksetDataFindKeys("blocklist:" + (string)id, 0, 1)) > 0)
        {
            llRegionSayTo(id, 0, "You're blacklisted.");
            return;
        }
 
        llRegionSayTo(id, 0, "Hello there, secondlife:///app/agent/" + (string)id + "/about - your message: " + message);
 
    }
 
    linkset_data(integer action, string name, string value)
    {
        if (action == LINKSETDATA_RESET || action == LINKSETDATA_DELETE || action == LINKSETDATA_UPDATE)
        {
            llOwnerSay("Blacklist storage modified.");
        }
    }
 
}</syntaxhighlight>
|also_functions=
|also_functions=
{{LSL DefineRow||[[llLinksetDataAvailable]]|}}
{{LSL DefineRow||[[llLinksetDataAvailable]]|}}
Line 114: Line 261:
{{LSL DefineRow||[[llLinksetDataDelete]]|}}
{{LSL DefineRow||[[llLinksetDataDelete]]|}}
{{LSL DefineRow||[[llLinksetDataDeleteProtected]]|}}
{{LSL DefineRow||[[llLinksetDataDeleteProtected]]|}}
{{LSL DefineRow||[[llLinksetDataDeleteFound]]|}}
{{LSL DefineRow||[[llLinksetDataFindKeys]]|}}
{{LSL DefineRow||[[llLinksetDataFindKeys]]|}}
{{LSL DefineRow||[[llLinksetDataListKeys]]|}}
{{LSL DefineRow||[[llLinksetDataListKeys]]|}}
Line 123: Line 271:
|also_tests
|also_tests
|also_articles
|also_articles
|notes
|notes=
* Linkset datastore operations are synchronous and are usually processed within one server frame. Therefore, it is possible to synchronize variables between multiple scripts using the datastore alone without resorting to [[llMessageLinked]], or to use the datastore directly as extended memory for specific workloads that need to work with extremely large datasets.
|cat1=Script
|cat1=Script
|cat2=LinksetData
|cat2=LinksetData

Latest revision as of 10:57, 14 August 2024

Summary

Summary: llLinksetDataWrite, llLinksetDataWriteProtected

The llLinksetDataWrite and llLinksetDataWriteProtected functions create or update a name:value pair in the linkset datastore. The linkset datastore is a semi-permanent key-value store that is retained as a prim property regardless of whether the script is deleted or reset (via manual reset, llResetScript, llResetOtherScript, or cloning the object). If value is an empty string, the pair is deleted.

This function returns 0 on success or an error code on failure.

When these functions are called, the linkset_data event is triggered in all scripts running in the linkset with an action of LINKSETDATA_UPDATE, or LINKSETDATA_DELETE if the pair is deleted.

The linkset datastore can contain up to 131072 bytes (128 KiB) of data and has no impact on script memory usage aside from the functions and events used to interact with it. Every pair written to the datastore consumes a number of bytes in the datastore equal to the length of name plus the length of value, plus an additional 32 bytes if written using llLinksetDataWriteProtected.

llLinksetDataWrite

Function: integer llLinksetDataWrite( string name, string value );

Creates or updates an unprotected name:value pair from the linkset's datastore.
Returns an integer success or failure code.

• string name The key of the name:value pair in the datastore to be updated or created.
• string value The value of the name:value pair.

llLinksetDataWriteProtected

Function: integer llLinksetDataWriteProtected( string name, string value, string pass );
0.0 Forced Delay
10.0 Energy

Creates or updates a protected name:value pair from the linkset's datastore. Further attempts to read, write or update the name:value pair must use the protected versions of those functions and must supply the same string that was used in pass.
Returns an integer success or failure code.

• string name The key of the name:value pair in the datastore to be updated or created.
• string value The value of the name:value pair.
• string pass A pass phrase used to protect the name:value pair.
Caveats
  • Protecting a name:value pair adds an additional 32 bytes to its size in the datastore regardless of the length of pass. The length of pass itself is not counted against the data storage limit.
  • When writing a protected name, the linkset_data event fires as normal, however the value parameter will be an empty string.

Constant Description
LINKSETDATA_OK 0 The name:value pair was written to the datastore.
LINKSETDATA_EMEMORY 1 A name:value pair was too large to write to the linkset datastore.
LINKSETDATA_ENOKEY 2 The name supplied to llLinksetDataWrite was empty.
LINKSETDATA_EPROTECTED 3 The name:value pair has been protected from overwrite in the linkset's datastore.
LINKSETDATA_NOTFOUND 4 The named key could not be found in the linkset's datastore when attempting to delete it.
LINKSETDATA_NOUPDATE 5 The name:value stored in the linkset was not changed by the write operation because the value stored matches the value written.

Caveats

  • Events are only fired if the linkset's datastore is changed.
    • Rewriting an existing value to a name:value pair returns LINKSETDATA_NOUPDATE.
    • Writing an empty string to a nonexistent name in the datastore returns LINKSETDATA_NOTFOUND.
  • There is currently no way to write to or read from a linkset datastore from another linkset.
  • The datastore is accessible from the entire linkset but acts as a property of the root prim alone. Therefore, linking and unlinking prims produces the following results:
    • When linking one linkset to another linkset, the combined linkset datastore includes all pairs from both datastores.
      • If any pairs have conflicting names, the combined linkset datastore will keep the pair from the original linkset and will silently drop conflicting pairs from newly added prim(s).
      • If the combined linkset datastore would exceed 131072 bytes, pairs from the newly linked prim(s) will be added to the combined linkset datastore up to the limit. It is not currently clear in what order pairs are added, so there is no way to predict which will be dropped.
    • When unlinking a child prim from a linkset, the datastore remains in the original linkset and the child prim (now its own root prim) has an empty datastore.
      • If the datastore is too large to cache in a script to be rewritten after unlinking, you may need to devise a custom method of porting the datastore from the original linkset to the child prim, if necessary.
    • When unlinking a root prim from a linkset, the datastore remains in the newly-unlinked root prim and the remaining prims in the original linkset have an empty datastore.
  • There is no limit on the size of value aside from the total datastore limit, so care should be taken when writing very large values that could crash other scripts in the linkset via linkset_data.
    • If a script does not define a linkset_data event, it will not load any event parameters into its memory when the datastore is written to, so the script should not crash from another script in the linkset writing to the datastore.
    • If this is a possible risk, consider using llLinksetDataWriteProtected, which does not send value in linkset_data, with a static pass.
  • It is possible for data to be rolled back to a previous state if the datastore is stored in an object that is restored via a simulator rollback, or in an attachment that is not properly saved back to the server on logout.
    • Viewer crashes can cause attachment states to not be saved, so care should be taken when using these functions in attachments, because datastore rollbacks are likely to occur on occasion for attachments.

Examples

default
{
    touch_start(integer num_detected)
    {
        llLinksetDataWrite("test-name", "See you on the other side!");
        llResetScript();
    }
    state_entry()
    {
        llOwnerSay(llLinksetDataRead("test-name")); // Should print "See you on the other side!" to the owner
    }
}

Useful Snippets

Securing Against Tampering

// While there is no "standard" for names, it's a good idea to take some steps to prevent against other scripts accidentally overwriting your data.
// Since the datastore is shared among the entire linkset, one way to avoid name conflicts is to include the prim's UUID in the data pair's name.
// This example also monitors for llLinksetDataReset/unlinks and refreshes the datastore in response, which is usually good practice if you want to keep your data!

key this_uuid;
string lsd_data;

default
{
    state_entry()
    {
        this_uuid = llGetKey(); // Save the current key into this_uuid for checking later
        lsd_data = "bar"; // Store the data locally so it can be rewritten if the datastore gets erased for whatever reason
        llLinksetDataWrite((string)llGetKey() + "-foo", lsd_data); // Write the data
    }
    on_rez(integer start_param)
    {
        if (llGetKey() != this_uuid)
        {
            // Prim UUID has changed!
            // This should always be the case when on_rez is called, but is included here for clarity.
            llLinksetDataWrite((string)llGetKey() + "-foo", llLinksetDataRead((string)this_uuid + "-foo")); // Read the data from the last prim UUID's pair and write it into this prim UUID's pair
            // Note that you could also just write the lsd_data string instead of calling llLinksetDataRead, but this method is theoretically a little more robust if you expect other scripts to manipulate the data.
            llLinksetDataDelete((string)this_uuid + "-foo"); // Erase the original data to free up memory
            this_uuid = llGetKey(); // Update the UUID variable to the new UUID
        }
    }
    linkset_data(integer action, string name, string value)
    {
        if (action == LINKSETDATA_UPDATE || action == LINKSETDATA_DELETE)
        {
            if (name == (string)llGetKey() + "-foo")
            {
                // Somebody else wrote to our pair - we'll just save it in case we need it later, but you could re-write the original data instead if desired.
                lsd_data = value; // Note that in the case of LINKSETDATA_DELETE, value will be an empty string (""), which may or may not be how you want to handle that case
            }
        }
        else if (action == LINKSETDATA_RESET) 
        {
            // Linkset datastore has been reset!
            llLinksetDataWrite((string)llGetKey() + "-foo", lsd_data); // Write the lsd_data string into this prim UUID's pair - in this case we can't use llLinksetDataRead because the datastore is empty
        }
    }
    changed(integer change)
    {
        if (change & CHANGED_LINK)
        {
            // Linkset has changed!
            // Generally, this can happen for a few reasons (see Caveats above), including when an avatar sits on the object. In certain circumstances, this resets the datastore.
            // However, since the linkset data functions are very quick, it is easier to just write the data on every change to be safe for this example whether or not the datastore was reset.
            llLinksetDataWrite((string)llGetKey() + "-foo", lsd_data); // Write the lsd_data string into this prim UUID's pair - in this case we can't use llLinksetDataRead because the datastore might be empty
        }
    }
}

Simple blacklist management

integer gDialogChannel;
integer gDialogHandle;
integer gManagingBlocks;

startDialog(key person)
{
    gManagingBlocks = 0;
    gDialogHandle = llListen(gDialogChannel, "", person, "");
    llDialog(person, "\nSelect action", ["List blocks", "Add block", "Remove block"], gDialogChannel);
    llSetTimerEvent(60);
}

stopDialog()
{
    llSetTimerEvent(0);
    llListenRemove(gDialogHandle);
}

default
{

    on_rez(integer sp)
    {
        llResetScript();
    }

    state_entry()
    {
        gDialogChannel = (integer)(llFrand(-10000000)-10000000);
        llListen(PUBLIC_CHANNEL, "", NULL_KEY, "");;
    }

    timer()
    {
        stopDialog();
    }

    touch_start(integer nd)
    {
        key toucherKey = llDetectedKey(0);
        if (toucherKey == llGetOwner())
        {
            startDialog(toucherKey);
        }
    }

    listen(integer channel, string name, key id, string message)
    {

        if (llGetAgentSize(id) == ZERO_VECTOR)
        {
            return;
        }

        if (channel == gDialogChannel)
        {
            stopDialog();
            if (gManagingBlocks)
            {
                message = llStringTrim(message, STRING_TRIM);
                if ((key)message)
                {
                    if (gManagingBlocks == 1)
                    {
                        llOwnerSay("Addition request has been sent to the blacklist storage");
                        llLinksetDataWrite("blocklist:" + message, "1");
                    }
                    else
                    {
                        llOwnerSay("Removal request has been sent to the blacklist storage.");
                        llLinksetDataDelete("blocklist:" + message);
                    }
                }
                else
                {
                    llOwnerSay("The UUID '" + message + "' appears to be invalid.");
                }
                startDialog(id);
            }
            else if (message == "List blocks")
            {
                list blocks = llLinksetDataFindKeys("^blocklist:", 0, 0);
                integer listLength = llGetListLength(blocks);
                llOwnerSay("Blacklist items: " + (string)listLength);
                integer i;
                while (i < listLength)
                {
                    string record = llGetSubString(llList2String(blocks, i), 10, -1);
                    llOwnerSay("- secondlife:///app/agent/" + record + "/about" + " - " + record);
                    ++i;
                }
                blocks = [];
                startDialog(id);
            }
            else if (message == "Add block" || message == "Remove block")
            {
                string label = "add to";
                gManagingBlocks = 1;
                if (message == "Remove block")
                {
                    gManagingBlocks = 2;
                    label = "remove from";
                }
                gDialogHandle = llListen(gDialogChannel, "", id, "");
                llTextBox(id, "\nPlease specify one single avatar UUID you'd like to " + label + " the blacklist storage.", gDialogChannel);
                llSetTimerEvent(60);
            }
            return;
        }

        if (llGetListLength(llLinksetDataFindKeys("blocklist:" + (string)id, 0, 1)) > 0)
        {
            llRegionSayTo(id, 0, "You're blacklisted.");
            return;
        }

        llRegionSayTo(id, 0, "Hello there, secondlife:///app/agent/" + (string)id + "/about - your message: " + message);

    }

    linkset_data(integer action, string name, string value)
    {
        if (action == LINKSETDATA_RESET || action == LINKSETDATA_DELETE || action == LINKSETDATA_UPDATE)
        {
            llOwnerSay("Blacklist storage modified.");
        }
    }

}

Notes

  • Linkset datastore operations are synchronous and are usually processed within one server frame. Therefore, it is possible to synchronize variables between multiple scripts using the datastore alone without resorting to llMessageLinked, or to use the datastore directly as extended memory for specific workloads that need to work with extremely large datasets.

Deep Notes

Signature

function integer llLinksetDataWrite( string name, string value );
function integer llLinksetDataWriteProtected( string name, string value, string pass );