Myriad Lite Region Settings Server

// Myriad_Lite_Region_Setting_Server-v0.0.0-20130917.lsl
// Copyright (c) 2012-2013 by Allen Kerensky (OSG/SL) All Rights Reserved.
// This work is dual-licensed under
// Creative Commons Attribution (CC BY) 3.0 Unported
// http://creativecommons.org/licenses/by/3.0/
// - or -
// Modified BSD License (3-clause)
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
// * Redistributions of source code must retain the above copyright notice, 
//   this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
//   this list of conditions and the following disclaimer in the documentation
//   and/or other materials provided with the distribution.
// * Neither the name of Myriad Lite nor the names of its contributors may be
//   used to endorse or promote products derived from this software without
//   specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY EXPRESS OR
// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
// NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// The Myriad RPG System was designed, written, and illustrated by Ashok Desai
// Myriad RPG System licensed under:
// Creative Commons Attribution (CC BY) 2.0 UK: England and Wales
// http://creativecommons.org/licenses/by/2.0/uk/
 
// FIXME
// DONE Input string sanitation
// PARTIAL Access controls for players, builders, region owners, scripters
// Strength Length check before SAVE
// Finish emulating P5 stat server, test client messaging/functions
// Convert PPMA name/desc/text to SLDB key,[fields],[values] format
// Parcel ID for Rendezvous?
// Formalize request message formats
// Formalize return value formats
// Prebuild loadable modules to populate Power, Grace, Intellect, Spirit in 1 click
// Notecard Loader?
// How to detect OSSL Notecard save option without crashing
// Use PPMA as local cache but flush to SLDB backend?
// Use PPMA as local cache but flush to Silo backend?
// BACKUP() function to write persistent prim stuff to backing store (notecard, SLDB, silo?)
// RESTORE() function to load persistent prim stuff from backing store (notecard, SLDB, silo?)
// How to put this tool on the HUD for region owners to call up
// Teach Me Tutorial wizard to make and manage stats
// Make the entire tool able to be used in mouselook (chat inputs alongside drop downs)
// HTML server to serve game data out to HTTP clients?
 
// VERSION CONTROL
string VERSION = "0.0.0"; // Allen Kerensky's script version
string VERSIONDATE = "20130917"; // Allen Kerensky's script yyyymmdd
 
string KEY_NOT_FOUND="[KEY_NOT_FOUND]";
 
// Runtimes
integer FLAG_DEBUG; // if true, send DEBUG messages while running
integer RENDEZVOUS1 = -999; // the server well known channel to listen on for client requests
integer RENDEZVOUS2; // the server well known channel to listen on for client requests
integer HANDLE1; // the server listener handle, see llListen() wiki page.
integer HANDLE2; // the server listener handle, see llListen() wiki page.
 
//============================================================================
DEBUG(string msg) {
    if ( FLAG_DEBUG == TRUE ) llSay(DEBUG_CHANNEL,"DEBUG: script=["+llGetScriptName()+"] owner=["+llKey2Name(llGetOwner())+"] message=["+msg+"]");
}
 
//============================================================================
DELETE_RECORD(string dbkey, string field) {
    DEBUG("DELETE_RECORD DBKEY=["+dbkey+"] FIELD=["+field+"] BEGINS");
    // scan index or prim names for empty, get number for update
    integer i;
    string name;
    string desc;
    for ( i = 2; i <= llGetNumberOfPrims(); i++) {
        name = llList2String(llGetLinkPrimitiveParams(i,[PRIM_NAME]),0);        
        desc = llList2String(llGetLinkPrimitiveParams(i,[PRIM_DESC]),0);
        if ( name == dbkey && desc == field ) { // record found, delete
            llSetLinkPrimitiveParamsFast(i,[PRIM_NAME,"",PRIM_DESC,"",PRIM_TEXT,"",ZERO_VECTOR,0.0,PRIM_COLOR,ALL_SIDES,<1,0,0>,1.0]); // save to database
            DEBUG("DELETE_RECORD DBKEY=["+dbkey+"] FIELD=["+field+"] DELETED");
        }
    }
    DEBUG("DELETE_RECORD DBKEY=["+dbkey+"] FIELD=["+field+"] ENDS");
}
 
//============================================================================
DUMP_RECORDS() {
    DEBUG("DUMP_RECORDS BEGINS");
    // scan index or prim names for empty, get number for update
    integer i;
    string name = "";
    string desc = "";
    string text = "";
    for ( i = 2; i <= llGetNumberOfPrims(); i++) {
        name = llList2String(llGetLinkPrimitiveParams(i,[PRIM_NAME]),0);
        desc = llList2String(llGetLinkPrimitiveParams(i,[PRIM_DESC]),0);
        if ( name != "" && desc != "" ) {
            text = llList2String(llGetLinkPrimitiveParams(i,[PRIM_TEXT]),0);
            DEBUG("RECORD=["+(string)i+"] NAME=["+name+"] DESC=["+desc+"] TEXT=["+text+"]");
        }
    }
    DEBUG("DUMP_RECORDS ENDS");
}
 
//============================================================================
DUMP_TYPE(integer chan,string type) {
    DEBUG("DUMP_TYPES CHAN=["+(string)chan+"] TYPE=["+type+"] BEGINS");
    type = llToUpper(type);
    // scan index or prim names for empty, get number for update
    integer i;
    string name = "";
    string desc = "";
    string text = "";
    for ( i = 2; i <= llGetNumberOfPrims(); i++) {
        name = llList2String(llGetLinkPrimitiveParams(i,[PRIM_NAME]),0);
        if ( name == type ) {
            desc = llList2String(llGetLinkPrimitiveParams(i,[PRIM_DESC]),0);
            text = llList2String(llGetLinkPrimitiveParams(i,[PRIM_TEXT]),0);
            if ( type == "STATISTIC" ) llSay(chan,"STATISTIC|NAME="+desc+"|"+text);
            if ( type == "SKILL" ) llSay(chan,"SKILL|NAME="+desc+"|"+text);
            if ( type == "EFFECT" ) llSay(chan,"EFFECT|NAME="+desc+"|"+text);
            if ( type == "RESILIENCE" ) llSay(chan,"RESILIENCE|NAME="+desc+"|"+text);
            if ( type == "BOON" ) llSay(chan,"BOON|NAME="+desc+"|"+text);
            if ( type == "FLAW" ) llSay(chan,"FLAW|NAME="+desc+"|"+text);
            if ( type == "CAMPAIGN" ) llSay(chan,"CAMPAIGN|NAME="+desc+"|"+text);
            if ( type == "SPECIE" ) llSay(chan,"SPECIE|NAME="+desc+"|"+text);
            if ( type == "BACKGROUND" ) llSay(chan,"BACKGROUND|NAME="+desc+"|"+text);
            if ( type == "CAREER" ) llSay(chan,"CAREER|NAME="+desc+"|"+text);        
            if ( type == "ITEM" ) llSay(chan,"ITEM|NAME="+desc+"|"+text);
        }
    }
    DEBUG("DUMP_TYPE ENDS");
}
 
//============================================================================
ERASE_TYPE(string type) {
    DEBUG("ERASE TYPE ["+type+"] BEGINS");
    // scan index or prim names for empty, get number for update
    integer i;
    string name = "";
    string desc = "";
    for ( i = 2; i <= llGetNumberOfPrims(); i++) {
        name = llList2String(llGetLinkPrimitiveParams(i,[PRIM_NAME]),0);
        if ( name == type ) {
            desc = llList2String(llGetLinkPrimitiveParams(i,[PRIM_DESC]),0);
            DELETE_RECORD(type,desc);
        }
    }
    DEBUG("ERASE_TYPE ENDS");    
}
 
//============================================================================
FORMAT_DB() {
    DEBUG("FORMATTING DATABASE BEGINS");
    llSay(PUBLIC_CHANNEL,"FORMATTING DATABASE");
    integer i;
    for ( i = 2; i <= llGetNumberOfPrims(); i++ ) {
        if ( llList2String(llGetLinkPrimitiveParams(i,[PRIM_NAME]),0) != "" ) {
            llSetLinkPrimitiveParamsFast(i,[PRIM_NAME,"",PRIM_DESC,"",PRIM_TEXT,"",ZERO_VECTOR,0.0,PRIM_COLOR,ALL_SIDES,<1,0,0>,1.0]);
        }
    }
    DEBUG("DATABASE FORMAT ENDS");
    llSay(PUBLIC_CHANNEL,"DATABASE FORMAT ENDS");
}
 
//============================================================================
string GET_VAL(string dbkey,string field) {
    DEBUG("GET_VAL DBKEY=["+dbkey+"] FIELD=["+field+"]");
    string out = KEY_NOT_FOUND;
    integer i;
    string name;
    string desc;
    for ( i = 2; i <= llGetNumberOfPrims(); i++ ) {
        name = llList2String(llGetLinkPrimitiveParams(i,[PRIM_NAME]),0);
        desc = llList2String(llGetLinkPrimitiveParams(i,[PRIM_DESC]),0);
        if ( llToLower(name) == llToLower(dbkey) && llToLower(desc) == llToLower(field) ) {
            out = llList2String(llGetLinkPrimitiveParams(i,[PRIM_TEXT]),0);
            DEBUG("GET_VAL RETURN=["+out+"]");
            return out; // bail on first match?
        }
    }
    DEBUG("GET_VAL RETURN=["+out+"]");
    return out;
}
 
//============================================================================
LIST_TYPE(integer chan,string type) {
    DEBUG("LIST_TYPE CHAN=["+(string)chan+"] TYPE=["+type+"] BEGINS");
    type = llToUpper(type);
    // scan index or prim names for empty, get number for update
    integer i;
    string name = "";
    string desc = "";
    list out = [];
    for ( i = 2; i <= llGetNumberOfPrims(); i++) {
        name = llList2String(llGetLinkPrimitiveParams(i,[PRIM_NAME]),0);
        if ( name == type ) {
            desc = llList2String(llGetLinkPrimitiveParams(i,[PRIM_DESC]),0);
            out = out + [desc];
        }
    }
    out = llListSort(out,1,TRUE);
    if ( type == "STATISTIC" ) llSay(chan,"STATISTICS|"+llList2CSV(out));
    if ( type == "SKILL" ) llSay(chan,"SKILLS|"+llList2CSV(out));
    if ( type == "EFFECT" ) llSay(chan,"EFFECTS|"+llList2CSV(out));
    if ( type == "RESILIENCE" ) llSay(chan,"RESILIENCES|"+llList2CSV(out));
    if ( type == "BOON" ) llSay(chan,"BOONS|"+llList2CSV(out));
    if ( type == "FLAW" ) llSay(chan,"FLAWS|"+llList2CSV(out));
    if ( type == "CAMPAIGN" ) llSay(chan,"CAMPAIGNS|"+llList2CSV(out));
    if ( type == "SPECIE" ) llSay(chan,"SPECIES|"+llList2CSV(out));
    if ( type == "BACKGROUND" ) llSay(chan,"BACKGROUNDS|"+llList2CSV(out));
    if ( type == "CAREER" ) llSay(chan,"CAREERS|"+llList2CSV(out));
    if ( type == "ITEM" ) llSay(chan,"ITEMS|"+llList2CSV(out));
    DEBUG("LIST_TYPE CHAN=["+(string)chan+"] TYPE=["+type+"] ENDS");
}
 
//============================================================================
SET_VAL(string dbkey, string field, string val) {
    DEBUG("SET_VAL DBKEY=["+dbkey+"] FIELD=["+field+"] VAL=["+val+"] BEGINS");
    integer i;
    string name;
    string desc;
    integer written = FALSE;
    for ( i = 2; i <= llGetNumberOfPrims(); i++ ) {
        name = llStringTrim(llList2String(llGetLinkPrimitiveParams(i,[PRIM_NAME]),0),STRING_TRIM);
        desc = llStringTrim(llList2String(llGetLinkPrimitiveParams(i,[PRIM_DESC]),0),STRING_TRIM);
        if ( ( llToLower(name) == llToLower(dbkey) && llToLower(desc) == llToLower(field) ) && written == FALSE ) {
            DEBUG("SET_VAL UPDATE RECORD=["+(string)i+"] DBKEY=["+dbkey+"] DESC=["+field+"] VAL=["+val+"]");
            llSetLinkPrimitiveParamsFast(i,[PRIM_NAME,dbkey,PRIM_DESC,field,PRIM_TEXT,val,ZERO_VECTOR,0.0,PRIM_COLOR,ALL_SIDES,<0,1,0>,1.0]);
            written = TRUE; // we did an update, remember it
        }    
    }
    if ( written == TRUE ) {
        DEBUG("SET_VAL UPDATE COMPLETE.");
        return;
    }
    // data hasn't been written, scan for free prim
    for ( i = 2; i <= llGetNumberOfPrims(); i++ ) {
        name = llStringTrim(llList2String(llGetLinkPrimitiveParams(i,[PRIM_NAME]),0),STRING_TRIM);
        desc = llStringTrim(llList2String(llGetLinkPrimitiveParams(i,[PRIM_DESC]),0),STRING_TRIM);
        if ( ( name == "" && desc == "" ) && written == FALSE ) {
            DEBUG("SET_VAL INSERT RECORD=["+(string)i+"] DBKEY=["+dbkey+"] DESC=["+field+"] VAL=["+val+"]");
            llSetLinkPrimitiveParamsFast(i,[PRIM_NAME,dbkey,PRIM_DESC,field,PRIM_TEXT,val,ZERO_VECTOR,0.0,PRIM_COLOR,ALL_SIDES,<0,1,0>,1.0]);
            written = TRUE; // we did an update, remember it
        }
    }
    if ( written == TRUE ) {
        DEBUG("SET_VAL INSERT COMPLETED.");
        return;
    }
    DEBUG("SET_VAL NO FREE RECORD FOUND TO INSERT INTO! DATA LOST! ENDS");
}
 
string STRING_FILTER(string in) {
    DEBUG("STRING_FILTER IN=["+in+"]");
    list charset = llCSV2List(" ,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z");
    string out = "";
    integer i;
    for ( i = 0; i < llStringLength(in); i++) {
        string char = llGetSubString(in,i,i);
        if ( llListFindList(charset,[char]) >= 0 ) {
            out += char;
        } else {
            out += " ";
        }
    }
    DEBUG("STRING_FILTER OUT=["+out+"]");
    return llStringTrim(out,STRING_TRIM);
}
 
string INTEGER_FILTER(string in) {
    DEBUG("INTEGER FILTER_FILTER IN=["+in+"]");
    list numbers = llCSV2List("1,2,3,4,5,6,7,8,9,0");
    string out = "";
    integer i;
    for ( i = 0; i < llStringLength(in); i++) {
        string char = llGetSubString(in,i,i);
        if ( llListFindList(numbers,[char]) >= 0 ) {
            out += char;
        } else {
            out += " ";
        }
    }
    DEBUG("INTEGER_FILTER OUT=["+out+"]");
    return llStringTrim(out,STRING_TRIM);    
}
 
//============================================================================
default {
 
    //------------------------------------------------------------------------
    state_entry() {
        FLAG_DEBUG=FALSE; // set default here to keep LSLint from complaining about the constant
        DEBUG("STATE_ENTRY BEGINS");
 
        // SETUP RENDEZVOUS1 CHANNEL (STATIC) - backward compatible for Preview 6 and earlier
        if ( HANDLE1 != 0 ) llListenRemove(HANDLE1);
        HANDLE1 = llListen(RENDEZVOUS1,"",NULL_KEY,"");
        llSay(PUBLIC_CHANNEL,"Rendezvous1 channel is "+(string)RENDEZVOUS1);
 
        // SETUP RENDEZVOUS2 CHANNEL (DYNAMIC) - new for Preview 7 and later
        list details = llGetParcelDetails(<0,0,0>,[PARCEL_DETAILS_ID]);
        string parcelid = llList2String(details,0);
        RENDEZVOUS2 = (integer)("0x"+llGetSubString(parcelid,0,7));
        if ( HANDLE2 != 0 ) llListenRemove(HANDLE2);
        HANDLE2 = llListen(RENDEZVOUS2,"",NULL_KEY,"");
        llSay(PUBLIC_CHANNEL,"Rendezvous2 channel is "+(string)RENDEZVOUS2);
 
        // Ready to serve...
        llSay(PUBLIC_CHANNEL,llGetScriptName()+" (version "+VERSION+"-"+VERSIONDATE+") ready.");
        DEBUG("STATE_ENTRY ENDS");
    }
 
    //------------------------------------------------------------------------
    listen(integer channel,string speaker,key id,string msg) {
        DEBUG("LISTEN channel=["+(string)channel+"] speaker=["+speaker+"] id=["+(string)id+"] msg=["+msg+"] BEGINS");
 
        integer replychannel = (integer)("0x"+llGetSubString(id,0,6)); // calculate dynamic channel of whoever talked to us so we can reply directly
 
        // Security Note - ALL users can *read* from database, only regeion owner can WRITE/CHANGE database
 
        //
        // DUMP FUNCTIONS to PUBLIC CHANNEL
        //
        if ( msg == "DUMP_RECORDS") { DUMP_RECORDS(); return; }
        if ( msg == "DUMP_STATISTICS" ) { DUMP_TYPE(PUBLIC_CHANNEL,"STATISTIC"); return;}
        if ( msg == "DUMP_SKILLS" ) { DUMP_TYPE(PUBLIC_CHANNEL,"SKILL"); return; }
        if ( msg == "DUMP_EFFECTS" ) { DUMP_TYPE(PUBLIC_CHANNEL,"EFFECT"); return; }
        if ( msg == "DUMP_RESILIENCES" ) { DUMP_TYPE(PUBLIC_CHANNEL,"RESILIENCE"); return; }
        if ( msg == "DUMP_BOONS" ) { DUMP_TYPE(PUBLIC_CHANNEL,"BOON"); return; }
        if ( msg == "DUMP_FLAWS" ) { DUMP_TYPE(PUBLIC_CHANNEL,"FLAW"); return; }
        if ( msg == "DUMP_CAMPAIGNS" ) { DUMP_TYPE(PUBLIC_CHANNEL,"CAMPAIGN"); return; }
        if ( msg == "DUMP_SPECIES" ) { DUMP_TYPE(PUBLIC_CHANNEL,"SPECIE"); return; }
        if ( msg == "DUMP_BACKGROUNDS" ) { DUMP_TYPE(PUBLIC_CHANNEL,"BACKGROUND"); return; }
        if ( msg == "DUMP_CAREERS" ) { DUMP_TYPE(PUBLIC_CHANNEL,"CAREER"); return; }
        if ( msg == "DUMP_ITEMS" ) { DUMP_TYPE(PUBLIC_CHANNEL,"ITEM"); return; }
 
        //
        // LIST FUNCTIONS
        //
        if ( msg == "LIST_STATISTICS" ) { LIST_TYPE(replychannel,"STATISTIC"); return;}
        if ( msg == "LIST_SKILLS" ) { LIST_TYPE(replychannel,"SKILL"); return;}
        if ( msg == "LIST_EFFECTS" ) { LIST_TYPE(replychannel,"EFFECT"); return;}
        if ( msg == "LIST_RESILIENCES" ) { LIST_TYPE(replychannel,"RESILIENCE"); return;}
        if ( msg == "LIST_BOONS" ) { LIST_TYPE(replychannel,"BOON"); return;}
        if ( msg == "LIST_FLAWS" ) { LIST_TYPE(replychannel,"FLAW"); return;}
        if ( msg == "LIST_CAMPAIGNS" ) { LIST_TYPE(replychannel,"CAMPAIGN"); return;}
        if ( msg == "LIST_SPECIES" ) { LIST_TYPE(replychannel,"SPECIE"); return;}
        if ( msg == "LIST_BACKGROUNDS" ) { LIST_TYPE(replychannel,"BACKGROUND"); return;}
        if ( msg == "LIST_CAREERS" ) { LIST_TYPE(replychannel,"CAREER"); return;}
        if ( msg == "LIST_ITEMS" ) { LIST_TYPE(replychannel,"ITEM"); return;}
 
        //
        // GET FUNCTIONS        
        //
        if ( llGetSubString(msg,0,13) == "GET_STATISTIC|" ) {
            string aname = llStringTrim(llGetSubString(msg,14,-1),STRING_TRIM);
            string stat = GET_VAL("STATISTIC",aname);
            if ( stat == KEY_NOT_FOUND ) {
                llRegionSay(replychannel,"RESPONSE=ERROR|Required statistic ["+aname+"] not found in table.");
                return;
            }
            llRegionSay(replychannel,"STATISTIC|NAME="+aname+"|"+stat);
            return;
        }
        if ( llGetSubString(msg,0,9) == "GET_SKILL|" ) {
            string aname = llStringTrim(llGetSubString(msg,10,-1),STRING_TRIM);
            string skill = GET_VAL("SKILL",aname);
            if ( skill == KEY_NOT_FOUND ) {
                llRegionSay(replychannel,"RESPONSE=ERROR|Required skill ["+aname+"] not found in table.");
                return;
            }
            llRegionSay(replychannel,"SKILL|NAME="+aname+"|"+skill);
            return;
        }
        if ( llGetSubString(msg,0,10) == "GET_EFFECT|" ) {
            string aname = llStringTrim(llGetSubString(msg,11,-1),STRING_TRIM);
            string effect = GET_VAL("EFFECT",aname);
            if ( effect == KEY_NOT_FOUND ) {
                llRegionSay(replychannel,"RESPONSE=ERROR|Required effect ["+aname+"] not found in table.");
                return;
            }
            llRegionSay(replychannel,"EFFECT|NAME="+aname+"|"+effect);
            return;
        }
        if ( llGetSubString(msg,0,14) == "GET_RESILIENCE|" ) {
            string aname = llStringTrim(llGetSubString(msg,15,-1),STRING_TRIM);
            string resilience = GET_VAL("RESILIENCE",aname);
            if ( resilience == KEY_NOT_FOUND ) {
                llRegionSay(replychannel,"RESPONSE=ERROR|Required resilience ["+aname+"] not found in table.");
                return;
            }
            llRegionSay(replychannel,"RESILIENCE|NAME="+aname+"|"+resilience);
            return;
        }
        if ( llGetSubString(msg,0,8) == "GET_BOON|" ) {
            string aname = llStringTrim(llGetSubString(msg,9,-1),STRING_TRIM);
            string boon = GET_VAL("BOON",aname);
            if ( boon == KEY_NOT_FOUND ) {
                llRegionSay(replychannel,"RESPONSE=ERROR|Required boon ["+aname+"] not found in table.");
                return;
            }
            llRegionSay(replychannel,"BOON|NAME="+aname+"|"+boon);
            return;
        }
        if ( llGetSubString(msg,0,8) == "GET_FLAW|" ) {
            string aname = llStringTrim(llGetSubString(msg,9,-1),STRING_TRIM);
            string flaw = GET_VAL("FLAW",aname);
            if ( flaw == KEY_NOT_FOUND ) {
                llRegionSay(replychannel,"RESPONSE=ERROR|Required flaw ["+aname+"] not found in table.");
                return;
            }
            llRegionSay(replychannel,"FLAW|NAME="+aname+"|"+flaw);
            return;
        }
        if ( llGetSubString(msg,0,12) == "GET_CAMPAIGN|" ) {
            string aname = llStringTrim(llGetSubString(msg,13,-1),STRING_TRIM);
            string campaign = GET_VAL("CAMPAIGN",aname);
            if ( campaign == KEY_NOT_FOUND ) {
                llRegionSay(replychannel,"RESPONSE=ERROR|Required campaign ["+aname+"] not found in table.");
                return;
            }
            llRegionSay(replychannel,"CAMPAIGN|NAME="+aname+"|"+campaign);
            return;
        }
        if ( llGetSubString(msg,0,10) == "GET_SPECIE|" ) {
            string aname = llStringTrim(llGetSubString(msg,11,-1),STRING_TRIM);
            string specie = GET_VAL("SPECIE",aname);
            if ( specie == KEY_NOT_FOUND ) {
                llRegionSay(replychannel,"RESPONSE=ERROR|Required specie ["+aname+"] not found in table.");
                return;
            }
            llRegionSay(replychannel,"SPECIE|NAME="+aname+"|"+specie);
            return;
        }
        if ( llGetSubString(msg,0,14) == "GET_BACKGROUND|" ) {
            string aname = llStringTrim(llGetSubString(msg,15,-1),STRING_TRIM);
            string background = GET_VAL("BACKGROUND",aname);
            if ( background == KEY_NOT_FOUND ) {
                llRegionSay(replychannel,"RESPONSE=ERROR|Required background ["+aname+"] not found in table.");
                return;
            }
            llRegionSay(replychannel,"BACKGROUND|NAME="+aname+"|"+background);
            return;
        }
        if ( llGetSubString(msg,0,10) == "GET_CAREER|" ) {
            string aname = llStringTrim(llGetSubString(msg,11,-1),STRING_TRIM);
            string career = GET_VAL("CAREER",aname);
            if ( career == KEY_NOT_FOUND ) {
                llRegionSay(replychannel,"RESPONSE=ERROR|Required career ["+aname+"] not found in table.");
                return;
            }
            llRegionSay(replychannel,"CAREER|NAME="+aname+"|"+career);
            return;
        }
        if ( llGetSubString(msg,0,8) == "GET_ITEM|" ) {
            string aname = llStringTrim(llGetSubString(msg,9,-1),STRING_TRIM);
            string item = GET_VAL("ITEM",aname);
            if ( item == KEY_NOT_FOUND ) {
                llRegionSay(replychannel,"RESPONSE=ERROR|Required item ["+aname+"] not found in table.");
                return;
            }
            llRegionSay(replychannel,"ITEM|NAME="+aname+"|"+item);
            return;
        }        
 
        //
        // REGION OWNER CHECK - REGION OWNER COMMANDS ONLY BELOW THIS POINT
        //
        if ( llGetOwner() != llList2Key(llGetObjectDetails(id,[OBJECT_OWNER]),0) ) {
            string err = "RESPONSE=ERROR|Region owner only function.";
            llRegionSay(replychannel,err);
            DEBUG(err);
            return;
        }
        //
        // ERASE FUNCTIONS - deletes all records of a type
        //        
        if ( msg == "FORMAT_DB" ) { FORMAT_DB(); return; }        
        if ( msg == "ERASE_STATISTICS" ) { ERASE_TYPE("STATISTIC"); return; }
        if ( msg == "ERASE_SKILLS" ) { ERASE_TYPE("SKILL"); return; }        
        if ( msg == "ERASE_EFFECTS" ) { ERASE_TYPE("EFFECT"); return; }        
        if ( msg == "ERASE_RESILIENCES" ) { ERASE_TYPE("RESILIENCE"); return; }        
        if ( msg == "ERASE_BOONS" ) { ERASE_TYPE("BOON"); return; }        
        if ( msg == "ERASE_FLAWS" ) { ERASE_TYPE("FLAW"); return; }        
        if ( msg == "ERASE_CAMPAIGNS" ) { ERASE_TYPE("CAMPAIGN"); return; }        
        if ( msg == "ERASE_SPECIES" ) { ERASE_TYPE("SPECIE"); return; }        
        if ( msg == "ERASE_BACKGROUNDS" ) { ERASE_TYPE("BACKGROUND"); return; }        
        if ( msg == "ERASE_CAREERS" ) { ERASE_TYPE("CAREER"); return; }        
        if ( msg == "ERASE_ITEMS" ) { ERASE_TYPE("ITEM"); return; }        
 
        // 
        // DELETE FUNCTIONS - delete one record of a type
        //
        if ( llGetSubString(msg,0,16) == "DELETE_STATISTIC|" ) {
            string aname = llStringTrim(llGetSubString(msg,17,-1),STRING_TRIM);
            DELETE_RECORD("STATISTIC",aname);
            LIST_TYPE(replychannel,"STATISTIC");
            return;
        }
        if ( llGetSubString(msg,0,12) == "DELETE_SKILL|" ) {
            string aname = llStringTrim(llGetSubString(msg,13,-1),STRING_TRIM);
            DELETE_RECORD("SKILL",aname);
            LIST_TYPE(replychannel,"SKILL");
            return;
        }
        if ( llGetSubString(msg,0,13) == "DELETE_EFFECT|" ) {
            string aname = llStringTrim(llGetSubString(msg,14,-1),STRING_TRIM);
            DELETE_RECORD("EFFECT",aname);
            LIST_TYPE(replychannel,"EFFECT");
            return;
        }
        if ( llGetSubString(msg,0,17) == "DELETE_RESILIENCE|" ) {
            string aname = llStringTrim(llGetSubString(msg,18,-1),STRING_TRIM);
            DELETE_RECORD("RESILIENCE",aname);
            LIST_TYPE(replychannel,"RESILIENCE");
            return;
        }
        if ( llGetSubString(msg,0,11) == "DELETE_BOON|" ) {
            string aname = llStringTrim(llGetSubString(msg,12,-1),STRING_TRIM);
            DELETE_RECORD("BOON",aname);
            LIST_TYPE(replychannel,"BOON");
            return;
        }
        if ( llGetSubString(msg,0,11) == "DELETE_FLAW|" ) {
            string aname = llStringTrim(llGetSubString(msg,12,-1),STRING_TRIM);
            DELETE_RECORD("FLAW",aname);
            LIST_TYPE(replychannel,"FLAW");
            return;
        }
        if ( llGetSubString(msg,0,15) == "DELETE_CAMPAIGN|" ) {
            string aname = llStringTrim(llGetSubString(msg,16,-1),STRING_TRIM);
            DELETE_RECORD("CAMPAIGN",aname);
            LIST_TYPE(replychannel,"CAMPAIGN");
            return;
        }
        if ( llGetSubString(msg,0,13) == "DELETE_SPECIE|" ) {
            string aname = llStringTrim(llGetSubString(msg,14,-1),STRING_TRIM);
            DELETE_RECORD("SPECIE",aname);
            LIST_TYPE(replychannel,"SPECIE");
            return;
        }
        if ( llGetSubString(msg,0,17) == "DELETE_BACKGROUND|" ) {
            string aname = llStringTrim(llGetSubString(msg,18,-1),STRING_TRIM);
            DELETE_RECORD("BACKGROUND",aname);
            LIST_TYPE(replychannel,"BACKGROUND");
            return;
        }
        if ( llGetSubString(msg,0,13) == "DELETE_CAREER|" ) {
            string aname = llStringTrim(llGetSubString(msg,14,-1),STRING_TRIM);
            DELETE_RECORD("CAREER",aname);
            LIST_TYPE(replychannel,"CAREER");
            return;
        }
        if ( llGetSubString(msg,0,11) == "DELETE_ITEM|" ) {
            string aname = llStringTrim(llGetSubString(msg,12,-1),STRING_TRIM);
            DELETE_RECORD("ITEM",aname);
            LIST_TYPE(replychannel,"ITEM");
            return;
        }
 
        //
        // CREATE FUNCTIONS
        //                
        list tokens = llParseString2List(msg,["|"],[]);
        integer tokencount = llGetListLength(tokens);                
 
        // process each attrib=value pair
        integer i;
        string request;
        string aspect;
        string name;
        string generator;
        string updater;
        string statmin;
        string statmax;
        string skillmin;
        string skillmax;
        string summary;
        string description;
        string genres;
        string type;
        string activation;
        string actionlist;
        string basestat;
        string action;
        string gpperlevel;
        string boonmax;
        string flawmax;
        string statpool;
        string skillpool;
        string perskills;
        string sfxpool;
        string healthpool;
        string gppool;
        string resources;
        string gpcost;
        string statlist;
        string boonlist;
        string flawlist;
        string skilllist;
        string sfxlist;
        string itemlist;
        string range;
        string damage;
        string rpcost;
        string period;
        string bonus;
        string rating;
        //list fields = []; // FIXME finish SLDB compatibility
        //list values = []; // FIXME finish SLDB compatibility
        for ( i = 0; i <= tokencount; i++) {
            string currenttoken = llList2String(tokens,i);
            list attribvalpair = llParseString2List(currenttoken,["="],[]);
            string attrib = llToLower(llList2String(attribvalpair,0));
            string sdata = llList2String(attribvalpair,1);
            //integer idata = llList2Integer(attribvalpair,1);
            if ( attrib == "request" ) request = sdata;
            if ( attrib == "aspect" ) aspect = sdata;
            if ( attrib == "name" ) name = sdata;
            if ( attrib == "generator" ) generator = sdata;
            if ( attrib == "updater" ) updater = sdata;
            if ( attrib == "statmin" ) statmin = sdata;
            if ( attrib == "statmax" ) statmax = sdata;
            if ( attrib == "skillmin" ) skillmin = sdata;
            if ( attrib == "skillmax" ) skillmax = sdata;
            if ( attrib == "summary" ) summary = sdata;
            if ( attrib == "description" ) description = sdata;
            if ( attrib == "genres" ) genres = sdata;
            if ( attrib == "type" ) type = sdata;
            if ( attrib == "activation" ) activation = sdata;
            if ( attrib == "actionlist" ) actionlist = sdata;
            if ( attrib == "basestat" ) basestat = sdata;
            if ( attrib == "action" ) action = sdata;
            if ( attrib == "gpperlevel" ) gpperlevel = sdata;
            if ( attrib == "boonmax" ) boonmax = sdata;
            if ( attrib == "flawmax" ) flawmax = sdata;
            if ( attrib == "statpool" ) statpool = sdata;
            if ( attrib == "skillpool" ) skillpool = sdata;
            if ( attrib == "perskills" ) perskills = sdata;
            if ( attrib == "sfxpool" ) sfxpool = sdata;
            if ( attrib == "healthpool" ) healthpool = sdata;
            if ( attrib == "gppool" ) gppool = sdata;
            if ( attrib == "resources" ) resources = sdata;
            if ( attrib == "gpcost" ) gpcost = sdata;
            if ( attrib == "statlist" ) statlist = sdata;
            if ( attrib == "boonlist" ) boonlist = sdata;
            if ( attrib == "flawlist" ) flawlist = sdata;
            if ( attrib == "skilllist" ) skilllist = sdata;
            if ( attrib == "sfxlist" ) sfxlist = sdata;
            if ( attrib == "itemlist" ) itemlist = sdata;
            if ( attrib == "range" ) range = sdata;
            if ( attrib == "damage" ) damage = sdata;
            if ( attrib == "rpcost" ) rpcost = sdata;
            if ( attrib == "period" ) period = sdata;
            if ( attrib == "bonus" ) bonus = sdata;                
            if ( attrib == "rating" ) rating = sdata;
 
            //fields = fields + [attrib]; // FIXME finish SLDB compatibility
            //values = values + [sdata]; // FIXME finish SLDB compatibility
        }
 
        // PROCESS Object now that we've broken down the message
        // REQUEST=CREATE|ASPECT=Statistic|NAME=Power|GENERATOR=Points|UPDATER=Level|SUMMARY=physical strength and resilience|DESCRIPTION=FIXME
        // CREATE,Statistic,Power,Points,Levels,physical strenghth and resilience,FIXME
        // { request: "create", aspect: "Statistic", name: "Power", generator: "Points", updater: "Level", summary: "stuff here", description: "blah blah" }
        // support alternate CREATE_<ASPECT> message format
        if ( llGetSubString(msg,0,6) == "CREATE_" ) {
            request = "CREATE";
            aspect = llGetSubString(llList2String(tokens,0),7,-1);
        }
 
        if ( request == "CREATE" ) {
            // 0. validate speaker id is region owner or object owned by region owner
            if ( llGetOwner() != llList2Key(llGetObjectDetails(id,[OBJECT_OWNER]),0) ) { // ignore create requests from IDs not owned by this server's owner
                string err = "RESPONSE=ERROR|Region owner only function.";
                llRegionSay(replychannel,err);
                DEBUG(err);
                return;
            }
            // 1. validate aspect
            list tmpaspects = ["STATISTIC","SKILL","EFFECT","RESILIENCE","BOON","FLAW","CAMPAIGN","SPECIE","BACKGROUND","CAREER","ITEM"];
            if ( llListFindList(tmpaspects,[aspect]) == -1 ) {
                string err = "RESPONSE=ERROR|Invalid aspect value ["+aspect+"]";
                llRegionSay(replychannel,err);
                DEBUG(err);
                return;
            }
            if ( llStringLength(aspect) > 63 ) { // max length 63 fits in prim name field
                string err = "RESPONSE=ERROR|ASPECT name ["+aspect+"] too long. Shorten to 63 characters or less.";
                llRegionSay(replychannel,err);
                DEBUG(err);
                return;
            }
            // 2. validate name
            name = STRING_FILTER(name);
            if ( name == "" || llStringLength(name) < 3 || llStringLength(name) > 127 ) { // max length 127 fits in prim description field
                string err = "RESPONSE=ERROR|Invalid name value ["+name+"]";
                llRegionSay(replychannel,err);
                DEBUG(err);
                return;
            }
            // 3. validate generator
            if ( aspect == "STATISTIC" || aspect == "SKILL" ) {
                if ( generator != "POINTBUY" && generator != "TEMPLATE" && generator != "RANDOM" ) {
                    string err = "RESPONSE=ERROR|Invalid generator value ["+generator+"]";
                    llRegionSay(replychannel,err);
                    DEBUG(err);
                    return;
                }
            }
            // 4. validate updater
            if ( aspect == "STATISTIC" || aspect == "SKILL" ) {
                if ( updater != "RANDOM" && updater != "GRADUAL" && updater != "LEVEL" ) {
                    string err = "RESPONSE=ERROR|Invalid updater value ["+updater+"]";
                    llRegionSay(replychannel,err);
                    DEBUG(err);
                    return;
                }
            }
            if ( aspect == "STATISTIC" ) {
                // 5. validate statmin
                statmin = INTEGER_FILTER(statmin);
                if ( (integer)statmin < 0 || (integer)statmin > 100 ) { // FIXME what is a realistic minimum?
                    string err = "RESPONSE=ERROR|Invalid statistic minimum value ["+statmin+"]";
                    llRegionSay(replychannel,err);
                    DEBUG(err);
                    return;
                }
                // 6. validate statmax
                statmax = INTEGER_FILTER(statmax);
                if ( (integer)statmax < 0 || (integer)statmax > 100 || (integer)statmin > (integer)statmax ) { // FIXME what is a realistic maximum?
                    string err = "RESPONSE=ERROR|Invalid statistic maximum value ["+statmax+"]";
                    llRegionSay(replychannel,err);
                    DEBUG(err);
                    return;
                }
            }
            if ( aspect == "SKILL" ) {
                // 7. validate skillmin
                skillmin = INTEGER_FILTER(skillmin);
                if ( (integer)skillmin < 0 || (integer)skillmin > 100 ) { // FIXME what is a realistic minimum?
                    string err = "RESPONSE=ERROR|Invalid skill minimum value ["+skillmin+"]";
                    llRegionSay(replychannel,err);
                    DEBUG(err);
                    return;
                }
                // 8. validate statmax
                skillmax = INTEGER_FILTER(skillmax);
                if ( (integer)skillmax < 0 || (integer)skillmax > 100 || (integer)skillmin > (integer)skillmax ) { // FIXME what is a realistic maximum?
                    string err = "RESPONSE=ERROR|Invalid skill maximum value ["+skillmax+"]";
                    llRegionSay(replychannel,err);
                    DEBUG(err);
                    return;
                }
            }
            // 9. validate summary
            if ( aspect == "STATISTIC" || aspect == "SKILL" ) {
                summary = STRING_FILTER(summary);
                if ( summary == "" || llStringLength(summary) < 3 || llStringLength(summary) > 63 ) { // FIXME what is a realistic max length?
                    string err = "RESPONSE=ERROR|Invalid summary value ["+summary+"]";
                    llRegionSay(replychannel,err);
                    DEBUG(err);
                    return;
                }
            }
            // 10. validate description
            if ( aspect == "STATISTIC" || aspect == "SKILL" ) {
                description = STRING_FILTER(description);
                if ( description == "" || llStringLength(description) < 3 || llStringLength(description) > 127 ) { // FIXME what is a realistic max length?
                    string err = "RESPONSE=ERROR|Invalid description value ["+description+"]";
                    llRegionSay(replychannel,err);
                    DEBUG(err);
                    return;
                }
            }
            // 7. create new or update existing
            string record ="";
            if ( aspect == "STATISTIC" )  record = "GENERATOR="+generator+"|UPDATER="+updater+"|STATMIN="+statmin+"|STATMAX="+statmax+"|SUMMARY="+summary+"|DESCRIPTION="+description; 
            if ( aspect == "SKILL" )      record = "GENERATOR="+generator+"|UPDATER="+updater+"|SKILLMIN="+skillmin+"|SKILLMAX="+skillmax+"|SUMMARY="+summary+"|GENRES="+genres+"|DESCRIPTION="+description;
            if ( aspect == "EFFECT" )     record = "TYPE="+type+"|ACTIVATION="+activation+"|ACTIONLIST="+actionlist+"|DESCRIPTION="+description;
            if ( aspect == "RESILIENCE" ) record = "BASESTAT="+basestat+"|TYPE="+type+"|ACTION="+action+"|DESCRIPTION="+description;
            if ( aspect == "BOON" )       record = "TYPE="+type+"|GPPERLEVEL="+gpperlevel+"|BOONMAX="+boonmax+"|DESCRIPTION="+description;
            if ( aspect == "FLAW" )       record = "TYPE="+type+"|GPPERLEVEL="+gpperlevel+"|FLAWMAX="+flawmax+"|DESCRIPTION="+description;
            if ( aspect == "CAMPAIGN" )   record = "STATPOOL="+statpool+"|SKILLPOOL="+skillpool+"|PERSKILLS="+perskills+"|SFXPOOL="+sfxpool+"|HEALTHPOOL="+healthpool+"|GPPOOL="+gppool+"|STATMAX="+statmax+"|SKILLMAX="+skillmax+"|RESOURCES="+resources;
            if ( aspect == "SPECIE" )     record = "GPCOST="+gpcost+"|STATLIST="+statlist+"|BOONLIST="+boonlist+"|FLAWLIST="+flawlist+"|SFXLIST="+sfxlist+"|SKILLLIST="+skilllist+"|ITEMLIST="+itemlist+"|DESCRIPTION="+description;
            if ( aspect == "BACKGROUND" ) record = "GPCOST="+gpcost+"|STATLIST="+statlist+"|BOONLIST="+boonlist+"|FLAWLIST="+flawlist+"|SFXLIST="+sfxlist+"|SKILLLIST="+skilllist+"|ITEMLIST="+itemlist+"|DESCRIPTION="+description;
            if ( aspect == "CAREER" )     record = "GPCOST="+gpcost+"|STATLIST="+statlist+"|BOONLIST="+boonlist+"|FLAWLIST="+flawlist+"|SFXLIST="+sfxlist+"|SKILLLIST="+skilllist+"|ITEMLIST="+itemlist+"|DESCRIPTION="+description;
            if ( aspect == "ITEM" )       record = "TYPE="+type+"|RANGE="+range+"|DAMAGE="+damage+"|RPCOST="+rpcost+"|PERIOD="+period+"|BONUS="+bonus+"|RATING="+rating+"|ACTIVATION="+activation+"|ACTIONLIST="+actionlist+"|DESCRIPTION="+description;
 
            if ( llStringLength(record) > 254 ) { // max length 254 fits in prim text field
                string err = "RESPONSE=ERROR|Total record length too long. Shorter summary or description may help.";
                llRegionSay(replychannel,err);
                DEBUG(err);
            }
            SET_VAL(llToUpper(aspect),name,record);
            // 8. notify region of new record and list
            if ( aspect == "STATISTIC" ) { LIST_TYPE(replychannel,"STATISTIC"); LIST_TYPE(PUBLIC_CHANNEL,"STATISTIC"); }
            if ( aspect == "SKILL" ) { LIST_TYPE(replychannel,"SKILL"); LIST_TYPE(PUBLIC_CHANNEL,"SKILL"); }
            if ( aspect == "EFFECT" ) { LIST_TYPE(replychannel,"EFFECT"); LIST_TYPE(PUBLIC_CHANNEL,"EFFECT"); }
            if ( aspect == "RESILIENCE" ) { LIST_TYPE(replychannel,"RESILIENCE"); LIST_TYPE(PUBLIC_CHANNEL,"RESILIENCE"); }
            if ( aspect == "BOON" ) { LIST_TYPE(replychannel,"BOON"); LIST_TYPE(PUBLIC_CHANNEL,"BOON"); }
            if ( aspect == "FLAW" ) { LIST_TYPE(replychannel,"FLAW"); LIST_TYPE(PUBLIC_CHANNEL,"FLAW"); }
            if ( aspect == "CAMPAIGN" ) { LIST_TYPE(replychannel,"CAMPAIGN"); LIST_TYPE(PUBLIC_CHANNEL,"CAMPAIGN"); }
            if ( aspect == "SPECIE" ) { LIST_TYPE(replychannel,"SPECIE"); LIST_TYPE(PUBLIC_CHANNEL,"SPECIE"); }
            if ( aspect == "BACKGROUND" ) { LIST_TYPE(replychannel,"BACKGROUND"); LIST_TYPE(PUBLIC_CHANNEL,"BACKGROUND"); }
            if ( aspect == "CAREER" ) { LIST_TYPE(replychannel,"CAREER"); LIST_TYPE(PUBLIC_CHANNEL,"CAREER"); }
            if ( aspect == "ITEM" ) { LIST_TYPE(replychannel,"ITEM"); LIST_TYPE(PUBLIC_CHANNEL,"ITEM"); }
            return;
        }
 
        DEBUG("LISTEN ENDS");
    }    
}