03-06-2005 05:11
edit 17 April 2005: for SL 1.6 compatibility and 1 minor bug fix

1 - Introduction

I've been told that this script is the best thing since the plywood
cube; which may be an exaggeration, but I've been pretty pleased. As
you all are well aware, Linden Labs has not yet given us writable
notecards. Well it turns out that we still have the ability to store
persistent data (and it's not through using an external XML-RPC
server). You just have to think a little differently :)

This inspiration came after one of those Why-haven't-I-logged-off-yet?
nights that end somewhere around sunrise, when I got totally sick of
re-entering a bunch of dynamically derived positions for an object
animation. In short, we just let a separate script hold onto our data
storage. As long as the storage script never gets reset, the data will
persist. You can make it really persistent if you take the object
back into your inventory, too.

What I'm providing here is effectively freeware; this software is like
the cat flap: bloody obvious once you see it. Nevertheless, the new
economy being what it is any donations to my cause will be accepted
with great delight. My stipend just doesn't go as far as it used to,
you know, and given the way it looks like the Lindens are taking the
economy, I'd rather have cash than rates.

There are two scripts attached here. One implements a command-line API
which is suitable for testing, and the other implements the Script
File Store itself. As it stands, the system has the ability to store
~14.5K of key+data per Script File Store "bank" (a bank is a single
SFS server script). I have a number of improvements planned for better
management and scalability of storage, but those aren't going to come
for free :)

2 - Architecture

The Script File Store is implemented with a client/server architecture
using link_messages as the networking transport. And if you think that
is an overblown description, you try documenting something while
jet-lagged in Frankfurt Airport :)

2.1 - Server Architecture

The server maintains a strided list (a-list to all you Lisp-heads) of
the [address, data] pairs. All communications with the server script
are via the link_message() event. Communications are validated against
the sender's link-number and the integer param to
llMessageLinked(). That integer parameter is referred to as a
communications channel.

There are five channels used by the server, and the API provided in
the console application numbers them sequentially from a base integer,
which is also called the bank number (each script being the equivalent
of an old-style memory bank). When read or written, all data is passed
in the string param and addresses are passed in the key param of the
llMessageLinked call.

The control channel works a little differently. It receives commands
in the string parameter of llMessageLinked(). The same principle used
in the read and write functions applies to data returned from control
messages, although the data is returned on a different channel
(primarily so a dump of the data can be distinguished from read
responses). The current implementation only supports two control
commands: "free" and "dump".

The "free" command returns the amount of free memory left in the
bank. Be warned, my tests have show that invoking the
llGetFreeMemory() call actually leaks script memory! If running out of
memory is a real possibility for your application, you should
proactively manage it, just like when you use llEmail().

The "dump" command returns all of the data in the bank. This is useful
for many things, e.g. searching for a particular entry in a
database. In particular it was implemented to allow the contents of a
bank to get dumped out the the chat history, where they could be
cut&pasted into a notecard :)

2.2 - Client Architecture

Well given the server interface description above, your client can be
anything you bloody well want it to be :) However the client I have
provided in the console applicationmaintains two lists of data
channels where it expects to get information back from the server. The
API channels are numbered consecutively starting with the read
channel. While the sequential allocation of these channels is
hard-coded into the API, the assignment of the five channels is
completely configurable on the server side. See section 3 (Usage) for
details on how to configure the server's communications parameters if
you choose to use a different API.

3 - Usage

Step 1: if your object will be running under SL 1.6 delete the
definition of llListReplaceList. Please don't feed the WOMBAT!

Step 2: adjust the communications parameters. There are three channels
which send data from the client to the server, and two which
return data to the client. The server is written to only
listen to a single prim, so your client script must reside in
the prim to which the server is listening. If you cut&paste
the API code from the console application script you will also
need to adjust those parameters.

Server params:

parent - the link number of the prim with the client
read - the read channel, client->server. this is also
referred to as the bank number for historical
write - the write channel, client->server
data - the data channel, server->client
ctl - control commands, client->server
meta - data returned from control commands,

Client Params:

fs_data_channels - list of channels where we can expect
to see data retrieval responses.
fs_meta_channels - list of channels where we can expect
to see control message responses.

Step 3: drop the script into your object which needs data storage

Step 4: write (or reuse) your client code

4 - Summary

Given that this documentation is now nearly as long as the code
itself, I leave to you the code. And a final thought, courtesy of Glod
Glodsson from Terry Pratchett's _Soul Music_:

"In my experience what every artist wants,
really wants,
is to be paid"

-- C
Discussion should take place over in Scripting Tips

CONSOLE Application Code


// -*- c++ -*-

// Script File Store API code

list fs_data_channels = [2, 7]; // the list of all link params used for data
list fs_meta_channels = [4, 9]; // the list of all link params used for metadata

integer fs_data(integer n) {
return llListFindList(fs_data_channels, [n]) != -1; }

integer fs_meta(integer n) {
return llListFindList(fs_meta_channels, [n]) != -1; }

fs_dump(integer b) {
llMessageLinked(0, b+3, "dump", NULL_KEY); }

fs_free(integer b) {
llMessageLinked(0, b+3, "free", NULL_KEY); }

fs_read(integer b, string a) {
llMessageLinked(0, b, "", (key)a); }

fs_save(integer b) {
llMessageLinked(0, b+3, "save", NULL_KEY); }

fs_write(integer b, string a, string d) {
llMessageLinked(0, b+1, d, (key)a); }

link_message(integer s, integer n, string d, key a) {
if(fs_data(n) || fs_meta(n)) {
llSay(0, "at: " + (string)a + " data: " + d); }}

// everything frome here and down is part of the
// SFS console application
state_entry() {
llSay(0, "In the beginning was the command line");
llListen(0, "", llGetOwner(), ""); }

listen(integer c, string n, key id, string m) {
if(llSubStringIndex(m, "#fs ") == 0) {
list words = llParseString2List(m, [" "], []);
string cmd = llList2String(words, 1);

if(cmd == "dump") {
integer bank = (integer)llList2String(words, 2);
fs_dump(bank); }

if(cmd == "free") {
integer bank = (integer)llList2String(words, 2);
fs_free(bank); }

if(cmd == "read") {
integer bank = (integer)llList2String(words, 2);
string address = llList2String(words, 3);
fs_read(bank, address); }

if(cmd == "write") {
integer bank = (integer)llList2String(words, 2);
string address = llList2String(words, 3);
string data = llDumpList2String(llDeleteSubList(words, 0, 3), " ");
fs_write(bank, address, data); }


Script File Server Code


// -*- c++ -*-

// globals
// The Script File Store is designed to work in a multi-prim,
// multi-scripted environment. As such, it is necessary to enforce a
// fairly strict filter on the link-messages it will process. There
// are more elegant ways to parameterize the link-message addressing
// (and keep it in sync with the API code), but they will all cost us
// precious bytes of storage.

integer parent = 0; // which prim (link number) to listen to

integer read = 0; // set this for this scripts read address
integer write = 1; // set this for this scripts write address
integer data = 2; // set this for scripts data responses
integer ctl = 3; // set this for store control messages
integer meta = 4; // set this for metadata message address

// The Script File Store
list store = [];

integer have_op(integer n) {
// returns true if this is an operation to which we need to
// respond
return n == read
|| n == write
|| n == ctl; }

link_message(integer s, integer n, string d, key id) {
if(s == parent && have_op(n)) {
if(n == read || n == write) {
string address = (string)id;
integer index = llListFindList(store, [address]);

if(n == write) {
if(index == -1)
// don't have this address yet
store = [address, d] + store;

else if(llStringLength(d) == 0)
// delete this address
// edit: to fix bug here
store = llDeleteSubList(store, index, index+1);

// overwriting this data
store = llListReplaceList(store,
[address, d],
index+1); }

else if(n == read) {
if(index == -1)
// unused addresses contain the empty string
llMessageLinked(s, data, "", id);

// retrieve the data
llList2String(store, index + 1),
id); }}

else if(n == ctl) {
// meta commands
if(d == "free")
llMessageLinked(s, meta, (string)llGetFreeMemory(), NULL_KEY);

else if(d == "dump") {
integer i = 0;
llMessageLinked(s, meta,
(string)(llGetListLength(store) / 2),
for(; i < llGetListLength(store); i += 2)
llMessageLinked(s, meta,
llList2String(store, i+1),
(key)llList2String(store, i)); }}}}