ChangeLog

11 January 2016

  • Fixed script error when more than 12 notecards have been added (thanks Max).
  • Added settings such that notecards can be deleted automatically after being read.
  • Changed Unread to Total.

About

This script allows you to create a notecard dropbox that will reject any items except notecards and will display in an overhead text the number of "read" notecard and the total number of notecards. Once the owner touches the dropbox and selects a notecard from the menu, the script considers that notecard "read" and updates the overhead count. Similarly, if a notecard is dropped into the primitive or deleted from the primitive, the script updates the overhead text.

Setup

  • Create a primitive.
  • Create a script in that primitive and paste the script below.
  • Add or remove notecard.
  • Touch to get a dialog menu.

Developer Notes

There are some particular challenges to this script. Firstly, if we were to use the script memory to store the notecard names and their read status, after a restart that data will be gone. Secondly, LSL does not report which notecard has been added or deleted - it only triggers the changed event handler and with the INVENTORY_CHANGED flag we can only know that something in the inventory has changed.

To address the first challenge we use key-value data storage to store a CSV serialized string in the primitive's description, thus making sure that the data is still there after a simulator restart.

Secondly, to track "read" notecards, every time a notecard is sent to an agent, we generate a hash of all the characters of the notecard name, using Pedro Oval's Ord function, plus its index in the name. More precisely, for a notecard named aaac the hash stored will be 296. Whenever a notecard is read, we generate that hash and concatenate it in the primitive's description. This allows us to determine which notecard was deleted because, in case of a deletion, the string in the primitive's description will not have the same hash numbers such that we delete from that string whenever we have a mismatch.

For example, suppose that we drop a notecard named aab into the dropbox. The script will thus update the description to:

read=0

Now suppose that we read the notecard by touching the object and requesting it. After reading, the script will update the description to:

read=289

If we now add a different notecard, named baa, the dropbox script will update the description to:

read=296, 0

If we read the baa notecard, the description will now read:

read=289, 293

In case we now delete the notecard named aab, the script will start to compute the hash of the notecards in the inventory. It finds baa and computes 293. Since 293 does not equal 289, it deletes 289 such that the description reads:

read=293

As you can see, by XOR-ing with the position of the character in the name of the notecard we can avoid collisions in palindrome cases. We are also rescued by the invariant that objects cannot have the same name in the inventory which eliminates hash collisions on same names.

Code

notecard_dropbox.lsl
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2014 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
integer wasMenuIndex = 0;
list wasDialogMenu(list input, list actions, string direction) {
    integer cut = 11-wasListCountExclude(actions, [""]);
    if(direction == ">" &&  (wasMenuIndex+1)*cut+wasMenuIndex+1 < llGetListLength(input)) {
        ++wasMenuIndex;
        jump slice;
    }
    if(direction == "<" && wasMenuIndex-1 >= 0) {
        --wasMenuIndex;
        jump slice;
    }
@slice;
    integer multiple = wasMenuIndex*cut;
    input = llList2List(input, multiple+wasMenuIndex, multiple+cut+wasMenuIndex);
    input = wasListMerge(input, actions, "");
    return input;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
integer wasListCountExclude(list input, list exclude) {
    if(llGetListLength(input) == 0) return 0;
    if(llListFindList(exclude, (list)llList2String(input, 0)) == -1) 
        return 1 + wasListCountExclude(llDeleteSubList(input, 0, 0), exclude);
    return wasListCountExclude(llDeleteSubList(input, 0, 0), exclude);
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
list wasListMerge(list l, list m, string merge) {
    if(llGetListLength(l) == 0 && llGetListLength(m) == 0) return [];
    string a = llList2String(m, 0);
    if(a != merge) return [ a ] + wasListMerge(l, llDeleteSubList(m, 0, 0), merge);
    return [ llList2String(l, 0) ] + wasListMerge(llDeleteSubList(l, 0, 0), llDeleteSubList(m, 0, 0), merge);
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueGet(string k, string data) {
    if(llStringLength(data) == 0) return "";
    if(llStringLength(k) == 0) return "";
    list a = llParseStringKeepNulls(data, ["&", "="], []);
    integer i = llListFindList(llList2ListStrided(a, 0, -1, 2), [ k ]);
    if(i != -1) return llList2String(a, 2*i+1);
    return "";
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueSet(string k, string v, string data) {
    if(llStringLength(k) == 0) return "";
    if(llStringLength(v) == 0) return "";
    if(llStringLength(data) == 0) return k + "=" + v;
    integer i = llListFindList(
        llList2ListStrided(
            llParseString2List(data, ["&", "="], []), 
            0, -1, 2
        ), 
    [ k ]);
    if(i != -1) return llDumpList2String(
        llListReplaceList(
            llParseString2List(data, ["&"], []), 
            [ k + "=" + v ], 
        i, i), 
    "&");
    return data + "&" + k + "=" + v;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2011 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
integer wasAddNonEmptyStrings(list input) {
    if(input == []) return 0;
    integer a = llList2Integer(input , 0);
    if(a != 0)
        return 1 + 
            wasAddNonEmptyStrings(llDeleteSubList(input, 0, 0));
    return wasAddNonEmptyStrings(llDeleteSubList(input, 0, 0));
}
 
// Ord() function, written by Pedro Oval, 2010-05-28
// Inlined by Wizardry and Steamworks
integer Ord(string chr) {
    if (chr == "") return 0;
    string hex = llEscapeURL(chr);
    if (llGetSubString(hex, 0, 0) != "%") 
        return llBase64ToInteger("AAAA" + 
            llStringToBase64(llGetSubString(chr, 0, 0)));
    integer b = (integer)("0x" + llGetSubString(hex, 1, 2));
    if (b < 194 || b > 244) return b;
    if (b < 224) return ((b & 0x1F) << 6) | 
        (integer)("0x" + llGetSubString(hex, 4, 5)) & 0x3F;
    if (b < 240) return (b & 0x0F) << 12 + 
        ((integer)("0x" + llGetSubString(hex, 4, 5)) & 0x3F) << 6 + 
        (integer)("0x" + llGetSubString(hex, 7, 8)) & 0x3F;
    return (b & 0x07) << 18 + 
        ((integer)("0x" + llGetSubString(hex, 4, 5)) & 0x3F) << 12 + 
        ((integer)("0x" + llGetSubString(hex, 7, 8)) & 0x3F) << 6 + 
        (integer)("0x" + llGetSubString(hex, 10, 11)) & 0x3F;
}
 
integer comHandle = 0;
list menu = [];
list view = [];
string deleteCardText = "◯ DELETE";
integer deleteCard = 0;
 
default {
    state_entry() {
        // allow users to drop items in the object
        llAllowInventoryDrop(TRUE);
        // get the delete setting.
        deleteCard = (integer)wasKeyValueGet("delete", llGetObjectDesc());
        if(deleteCard) deleteCardText = "◉ DELETE";
        // generate the menu list that will be displayed when
        // the user drops a notecard into the inventory.
        //
        // only the first 23 characters are considered due to
        // limitations imposed on the size of the dialog buttons
        integer i = llGetInventoryNumber(INVENTORY_NOTECARD)-1;
        integer count = 0;
        do {
            ++count;
            menu += llGetSubString(llGetInventoryName(INVENTORY_NOTECARD, i), 0, 23);
        } while(--i>-1);
        // get the read list from the object description
        string desc = wasKeyValueGet("read", llGetObjectDesc());
        if(desc != "") view = llCSV2List(desc);
        i = count-llGetListLength(view);
        if(i <= 0) jump equal;
        do {
            view += 0;
        } while(--i>0);
@equal;
        // sum up the read list and...
        integer read = wasAddNonEmptyStrings(view);
        // set the new read list
        if(view != []) {
            llSetObjectDesc(
                wasKeyValueSet(
                    "read", 
                    llList2CSV(view), 
                    llGetObjectDesc()
                )
            );
        }
        // set the text
        llSetText(
            "Notes (Read / Total): " + 
            (string)read + 
            "/" + 
            (string)llGetInventoryNumber(INVENTORY_NOTECARD), 
        <1,1,1>, 1.0);
    }
 
    changed(integer change) {
        // if it is not an inventory change, ignore
        if(change & CHANGED_INVENTORY == 0) return;
        // delete any inventory item that is not 
        // the current script and is not a notecard
        integer i = llGetInventoryNumber(INVENTORY_ALL)-1;
        integer cards = 0;
        do {
            string item = llGetInventoryName(INVENTORY_ALL, i);
            if(item == llGetScriptName()) jump continue;
            if(llGetInventoryType(item) == INVENTORY_NOTECARD) {
                ++cards;
                jump continue;
            }
            llRemoveInventory(item);
@continue;
        } while(--i>-1);
        // get the read list from the object description
        view = llCSV2List(
            wasKeyValueGet("read", llGetObjectDesc())
        );
        // if a card was added, just reset the script
        if(cards > llGetListLength(view)) llResetScript();
        // if a card was removed step through the list
        // of notecards and check the sequence of view
        i = 0;
        do {
            string name = llGetInventoryName(INVENTORY_NOTECARD, i);
            // compute ord-hash and index of the notecard name
            integer j = llStringLength(name)-1;
            integer o = 0;
            do {
                o += Ord(llGetSubString(name, j, j)) ^ j;
            } while(--j>-1);
            // check if the hash matches the one stored in view
            if(o != llList2Integer(view, i))
                view = llDeleteSubList(view, i, i);
        } while(++i<cards);
        // set the new read list
        llSetObjectDesc(
            wasKeyValueSet(
                "read", 
                llList2CSV(view), 
                llGetObjectDesc()
            )
        );
        // finally, reset the script
        llResetScript();
    }
 
    touch_start(integer total_number) {
        // if the agent that touched the primitive is not the owner, abort.
        if(llDetectedKey(0) != llGetOwner()) return;
        // if we do not have any notecards, ignore
        if(!llGetInventoryNumber(INVENTORY_NOTECARD)) return;
        // generate a channel and allow the user to select a notecard
        integer comChannel = (integer)("0x8" + llGetSubString(llGetKey(), 0, 6));
        comHandle = llListen(comChannel, "", llDetectedKey(0), "");
        llDialog(llDetectedKey(0), "Please browse the available notecards:\n", wasDialogMenu(menu, ["⟵ Back", "⌠ Settings ⌡", "Next ⟶"], ""), comChannel);
    }
    listen(integer channel, string name, key id, string message) {
        // show back and forward menus
        if(message == "⟵ Back") {
            llDialog(id, "Please browse the available items:\n", wasDialogMenu(menu, ["⟵ Back", "⌠ Settings ⌡", "Next ⟶"], "<"), channel);
            return;
        }
        if(message == "Next ⟶") {
            llDialog(id, "Please browse the available items:\n", wasDialogMenu(menu, ["⟵ Back", "⌠ Settings ⌡", "Next ⟶"], ">"), channel);
            return;
        }
        if(message == "⌠ Settings ⌡") {
            llDialog(id, "Please chose from the options below:\n\n-The delete option, when toggled, will delete the notecards after reading.\n", ["⏏ Exit", deleteCardText ], channel);
            return;
        }
        if(message == "◯ DELETE") {
            deleteCardText = "◉ DELETE";
            deleteCard = 1;
            llSetObjectDesc(
                wasKeyValueSet(
                    "delete", 
                    "1", 
                    llGetObjectDesc()
                )
            );
            llDialog(id, "Please browse the available notecards:\n", wasDialogMenu(menu, ["⟵ Back", "⌠ Settings ⌡", "Next ⟶"], ""), channel);
            return;
        }
        if(message == "◉ DELETE") {
            deleteCardText = "◯ DELETE";
            deleteCard = 0;
            llSetObjectDesc(
                wasKeyValueSet(
                    "delete", 
                    "0", 
                    llGetObjectDesc()
                )
            );
            llDialog(id, "Please browse the available notecards:\n", wasDialogMenu(menu, ["⟵ Back", "⌠ Settings ⌡", "Next ⟶"], ""), channel);
            return;
        }
        if(message == "⏏ Exit") {
            llDialog(id, "Please browse the available notecards:\n", wasDialogMenu(menu, ["⟵ Back", "⌠ Settings ⌡", "Next ⟶"], ""), channel);
            return;
        }
        // go through the inventory and match the first 23 characters of the 
        // menu selection to the name of the notecard and send the notecard
        integer i = llGetInventoryNumber(INVENTORY_NOTECARD)-1;
        do {
            string card = llGetInventoryName(INVENTORY_NOTECARD, i);
            if(llGetSubString(message, 0, 23) != 
                llGetSubString(card, 0, 23)) jump continue;
 
            // hand out the notecard
            llGiveInventory(id, card);
            // delete the card in case it was requested
            if(deleteCard) {
                integer x = llListFindList(menu, (list)llGetSubString(card, 0, 23));
                menu = llDeleteSubList(menu, x, x);
                llRemoveInventory(card);
            }
            // get the read list from store
            view = llCSV2List(
                wasKeyValueGet("read", llGetObjectDesc())
            );
            // replace an ord-hash and index of the notecard name
            integer j = llStringLength(card)-1;
            integer o = 0;
            do {
                o += Ord(llGetSubString(card, j, j)) ^ j;
            } while(--j>-1);
            view = llListReplaceList(view, (list)o, i, i);
            // set the new read list
            llSetObjectDesc(
                wasKeyValueSet(
                    "read", 
                    llList2CSV(view), 
                    llGetObjectDesc()
                )
            );
            // add-up the read notecards...
            integer read = wasAddNonEmptyStrings(view);
            // and set the text
            llSetText(
                "Notes (Read / Total): " + 
                (string)read + 
                "/" + 
                (string)llGetInventoryNumber(INVENTORY_NOTECARD), 
            <1,1,1>, 1.0);
            // finally, close the channel and abort
            llListenRemove(comHandle);
            return;
@continue;
        } while(--i>-1);
    }
 
}

secondlife/notecard_dropbox.txt · Last modified: 2022/11/24 07:46 by 127.0.0.1

Access website using Tor Access website using i2p Wizardry and Steamworks PGP Key


For the contact, copyright, license, warranty and privacy terms for the usage of this website please see the contact, license, privacy, copyright.