11 January 2016
Unread
to Total
.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.
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.
/////////////////////////////////////////////////////////////////////////// // 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); } }