Myriad Lite Module BAM

// Myriad_Lite_Module_BAM-v0.0.6-20131026.lsl
// Copyright (c) 2013 by Baroun Tardis (OSG/SL) and 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 - needs some RPEVENTS to tell others about quester's actions?
// FIXME - needs some HELP events to tell others how to use the thing
 
// CONSTANTS - DO NOT CHANGE DURING RUN
string BASENAME = "Myriad Lite Module BAM";
string VERSION = "0.0.6"; // script version
string VERSIONDATE = "20131026"; // script date
 
// Module to Module Messaging Constants
integer MODULE_BAM = -4;
integer LM_SENDTOATTACHMENT = 0x80000000;
 
string API_OFFERADV = "OfferAdv"; // Offer an adventure to HUD wearer
string API_ACCEPTADV = "AcceptAdv"; // Offer accepted player is on an adventure
string API_INADV_QUERY = "InAdv?"; // Ask a player HUD if the player is in an adventure - InAdv?
string API_INADV_RESPONSE = "InAdv"; // In Adventure Response Yes: InAdv | String AdventureName  No: InAdv | NONE
string API_TASKIP_QUERY = "TaskIP?";
string API_TASKIP_RESPONSE = "TaskIP"; // Task In Progress Reply current task in progress: TaskIP | AdventureGoal
string API_TASKCP_QUERY = "TaskCP?"; // Task Complete Query
string API_TASKCP_RESPONSE = "TaskCP"; // Task Complete Reply
string API_DONETASK = "DoneTask"; // Task Done - player achieved current goal NPC sends: DoneTask | GoalText | TaskDone Text | PlayerUUID
string API_DONEADV = "DoneAdv"; // Done Adventure reply
string API_NONE = "NONE"; // a fixed string when not in adventure - uses variable to stay consistent everywhere
string API_ADDTASK = "AddTask"; // Add a task to the Player HUD - AddTask | TaskNumber | String Describing Task
string API_ADDHINT = "AddHint"; // Add a hint for a task to the Player HUD - AddHint | TaskNumber | String Hint
string MSG_NO_ADVENTURE = "Looking for adventure...";
string MSG_CURRENT_ADVENTURE = "Adventure: ";
string MSG_CURRENT_GOAL = "Overall Goal: ";
string MSG_CURRENT_TASK = "Current Task: ";
string MSG_CURRENT_HINT = "Current Hint: ";
 
// RUNTIME GLOBALS - CAN CHANGE DURING RUN
integer CHANOBJBAM; // channel of thing we're talking to
string  STATUSMSG; // scratch space to build status messages
 
// Adventure-Specific Configuration
// Task numbers are (AdvNum*100)+task, so they don't overlap between adventures
// string  ADVNAME="NONE"; // Adventure Name
// string  ADVTEXT; // brief description
// Current Task-specific info
// string  ADVTASKTODO; // task name of the next task handed out
// integer ADVTASKTDNUM; // task number of the next task handed out
// string  ADVTASKTODOHINT; // Hint text for the next task handed out
// Previous Task-specific info
// list    ADVTCOMP; // completed task numbers (list of numbers)
 
//
// PPMA GET_VAL
//
string KEY_NOT_FOUND = "[KEY_NOT_FOUND]";
string GET_VAL(string dbkey,string field) {
    //OWNERSAY("GET_VAL KEY=["+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?
        }
    }
    //OWNERSAY("GET_VAL RETURN=["+out+"]");
    return out;
}
 
SET_VAL(string dbkey, string field, string val) {
    //OWNERSAY("SET_VAL KEY=["+dbkey+"] FIELD=["+field+"] VAL=["+val+"]");
    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 ) {
            //OWNERSAY("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]);
            written = TRUE; // we did an update, remember it
        }    
    }
    if ( written == TRUE ) {
        //OWNERSAY("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 ) {
            //OWNERSAY("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]);
            written = TRUE; // we did an update, remember it
        }
    }
    if ( written == TRUE ) {
        //OWNERSAY("SET_VAL INSERT COMPLETED.");
        return;
    }
    //OWNERSAY("SET_VAL NO FREE RECORD FOUND TO INSERT INTO! DATA LOST");
}
 
//////////////////////////////////////////////////////////////////////////////
// FLAG FUNCTIONS
// INCAPACITATED - lost wounds
// DEAD - lost critical wounds
// DEBUG - show debug messages or not
// IDEA: INDECISION - lost resolve?
 
// LMIM: SET_FLAG|<name>=<TRUE|FALSE>
// PPMA: FLAG,<flagname>,<TRUE|FALSE>
// LMOUT: FLAG|<flagname>=<TRUE|FALSE>
// LMERR: FIXME
integer GET_FLAG(string flag) {
    string val = GET_VAL("FLAG",llToUpper(flag));
    if ( val == KEY_NOT_FOUND ) {
        ERROR("Flag ["+flag+"] does not exist.");
    }
    if ( val != "0" && val != "1") {
        ERROR("Flag: "+flag+" invalid value ["+val+"]");
    }
    integer bool = (integer)val;
    return bool;
}
 
// SET_FLAG
// LMIM: SET_FLAG|<flagname>=<TRUE|FALSE>
// PPMA: FLAG,<flagname>,<TRUE|FALSE>
// LMOUT: SET_FLAG|<flagname>=<TRUE|FALSE>
// LMERR: FIXME
SET_FLAG(string flag,integer value) {
    if ( flag == "" ) return; // FIXME add error
    if ( value != TRUE && value != FALSE ) return; // FIXME add err
    SET_VAL("FLAG",llToUpper(flag),(string)value);
}
 
// Adventure-Specific Configuration
// Adventure Name
string GET_ADVNAME() {
    string advname = GET_VAL("BAM","ADVENTURENAME");
    if ( advname == KEY_NOT_FOUND ) return API_NONE;
    return advname;
}
 
SET_ADVNAME(string name) {
    // FIXME RANGE CHECKS
    SET_VAL("BAM","ADVENTURENAME",name);
}
 
// Adventure brief description
string GET_ADVTEXT() {
    string advtext = GET_VAL("BAM","ADVENTURETEXT");
    if ( advtext == KEY_NOT_FOUND ) return "";
    return advtext;    
}
 
SET_ADVTEXT(string text) {
    // FIXME RANGE CHECKS
    SET_VAL("BAM","ADVENTURETEXT",text);
}
 
// task name of the next task handed out
string GET_ADVTASKTODO() {
    string todo = GET_VAL("BAM","ADVENTURENEXT");
    if ( todo == KEY_NOT_FOUND ) return "";
    return todo;
}
 
SET_ADVTASKTODO(string todo) {
    // FIXME RANGE CHECKS
    SET_VAL("BAM","ADVENTURETODO",todo);
}
 
// Task numbers are (AdvNum*100)+task, so they don't overlap between adventures
// task number of the next task handed out
integer GET_ADVTASKTODONUMBER() {
    string todonum = GET_VAL("BAM","ADVENTURETODONUMBER");
    if ( todonum == KEY_NOT_FOUND ) return 0;
    return (integer)todonum;
}
 
SET_ADVTASKTODONUMBER(integer todonumber) {
    // FIXME RANGE CHECKS
    if ( todonumber < 0 ) {
        ERROR("Adventure next task number less than zero - ignoring");
        return;
    }
    SET_VAL("BAM","ADVENTURENEXT",(string)todonumber);
}
 
// Hint text for the next task handed out
string GET_ADVTASKTODOHINT() {
    string hint = GET_VAL("BAM","ADVENTURETODOHINT");
    if ( hint == KEY_NOT_FOUND ) hint = "";
    return hint;
}
 
SET_ADVTASKTODOHINT(string hint) {
    // FIXME RANGE CHECKS
    SET_VAL("BAM","ADVENTURETODOHINT",hint);
}
 
// numerically sorted list of completed task numbers
list GET_ADVTASKCOMP() {
    string tasksdone = GET_VAL("BAM","ADVENTURETASKSCOMPLETE");
    if ( tasksdone == KEY_NOT_FOUND ) return [];
    return llListSort(llCSV2List(tasksdone),1,TRUE);
}
 
SET_ADVTASKCOMP(list done) {
    // FIXME RANGE CHECKS
    SET_VAL("BAM","ADVENTURETASKSCOMPLETE",llList2CSV(llListSort(done,1,TRUE)));
}
 
//============================================================================
// DEBUG - show errors on debug channel with wearer name for sorting
//============================================================================
DEBUG(string debugmsg) {
    if ( GET_FLAG("DEBUG") == TRUE ) llMessageLinked(LINK_THIS,MODULE_BAM,"DEBUG|"+debugmsg,llGetOwner());
}
 
//
// ERROR
//
ERROR(string msg) {
    llMessageLinked(LINK_THIS,MODULE_BAM,"ERROR|"+msg,llGetOwner());
}
 
// MEMORY
GET_MEMORY() {
    OWNERSAY(BASENAME+" free memory: "+(string)llGetFreeMemory()); // show this module's free memory info
}
 
// GETVERSION
GET_VERSION() {
    OWNERSAY(BASENAME+" v"+VERSION+"-"+VERSIONDATE); // show this module's version info
}
 
//
// OWNERSAY
//
OWNERSAY(string msg) {
    llMessageLinked(LINK_THIS,MODULE_BAM,"OWNERSAY|"+msg,llGetOwner());
}
 
// RESET - shut down running animations then reset the script to reload character sheet
RESET() {
    llResetScript(); // now reset
}
 
//============================================================================
// SETUP - begin bringing the HUD online
//============================================================================
SETUP() {
    llSetText("",<0,0,0>,0); // clear any previous hovertext
    if ( llGetAttached() >= 31 && llGetAttached() <= 38 ) { // are we attached to a HUD slot?
        SET_FLAG("BAMSTATUS",TRUE); // turn on HUD hovertext
    } else {
        SET_FLAG("BAMSTATUS",FALSE); // turn off body attached hovertext
    }
    STATUS();
    OWNERSAY("Baroun's Adventure Machine module active.");
}
 
//============================================================================
// STATUS() - update adventure data on HUD
//============================================================================
STATUS() {
    if ( GET_ADVNAME() == API_NONE ) { // is player in an adventure?
        STATUSMSG = MSG_NO_ADVENTURE; // nope
    } else { // yep, build the status
        STATUSMSG = MSG_CURRENT_ADVENTURE + GET_ADVNAME() + "\n" +
        MSG_CURRENT_GOAL + GET_ADVTEXT() + "\n" +
        MSG_CURRENT_TASK + GET_ADVTASKTODO() + "\n" +
        MSG_CURRENT_HINT + GET_ADVTASKTODOHINT();
    }
    OWNERSAY(STATUSMSG);
    if ( GET_FLAG("BAMSTATUS") == TRUE ) { // if attached to body, use chat output
        llSetText(STATUSMSG,<1,1,1>,1.0);
    }
}
 
//============================================================================
// DEFAULT STATE - load character sheet
//============================================================================
default {
    //------------------------------------------------------------------------
    // LINK MESSAGE - commands to and from other prims in HUD
    //------------------------------------------------------------------------
    link_message(integer sender,integer sending_module,string message, key speakerid) {
        if ( sending_module == MODULE_BAM || sending_module == LM_SENDTOATTACHMENT ) return; // ignore our own messages
 
        // break down the commands and messages into units we can work with
        list fields = llParseString2List(message,["|"],[]); // break into list of fields based on "|"ider
        string command = llToUpper(llList2String(fields,0)); // assume the first field is a Myriad Lite command
 
        if ( command == "DEBUG" || command == "ERROR" || command == "HELP" || command == "OWNERSAY" || command == "RPEVENT" ) return;
 
        if ( command == "MEMORY" ) { GET_MEMORY(); return;} // get memory info
        if ( command == "RESET" ) { RESET(); return;} // reset on command
        if ( message == "VERSION" ) { GET_VERSION(); return; } // dump the version and date on request
 
        DEBUG("EVENT link_message("+(string)sender+","+(string)sending_module+","+message+","+(string)speakerid+")");
 
        if ( message == "BAMSTATUS" ) { STATUS(); return;} // show status when specifically requested
 
        // calculate BAM dynamic channel of item/player talking to us
        CHANOBJBAM = (integer)("0x" + llGetSubString((string)speakerid,-7,-1));
 
        // process the rest of the fields for multi-field commands
        string data1   = llList2String(fields,1);
        string data2   = llList2String(fields,2);
        string data3   = llList2String(fields,3);
 
        // We're asked what adventure we're in
        if ( command == API_INADV_QUERY ) {
            llSay(CHANOBJBAM,API_INADV_RESPONSE + "|" + GET_ADVNAME()); // reply with In Adventure and Adventure Name
            STATUS();
            return; // command done, return early
        }
 
        // We're asked our current task in progress
        if ( command == API_TASKIP_QUERY ) { // what task in progress?
            // respond with task in progress
            llSay(CHANOBJBAM, API_TASKIP_RESPONSE + "|" + (string)GET_ADVTASKTODONUMBER()); // Reply with current task in progress and number
            STATUS();
            return; // command done, return early
        }
 
        // Get list of completed tasks
        if ( command == API_TASKCP_QUERY ) { // what tasks complete?
            llSay(CHANOBJBAM, API_TASKCP_RESPONSE + "|" + llList2CSV(GET_ADVTASKCOMP())); // reply with current task complete list as CSV
            STATUS();
            return; // command done, return early
        }
 
        // player is offered an adventure
        if ( command == API_OFFERADV ) { // want adventure?
            // FIXME need to add dialog box to accept/decline later
            SET_ADVNAME(data1); // name of the adventure
            SET_ADVTEXT(data2); // description of the adventure
            SET_ADVTASKCOMP([]); // clear the completed task list and start new one
            SET_ADVTASKTODO(""); // clear the next task name
            SET_ADVTASKTODONUMBER(0); // clear the next task number
            llSay(CHANOBJBAM, API_ACCEPTADV + "|" +data1); // accept the adventure
            STATUS();
            return; // command done, return early
        }
 
        // add the next task to complete
        if ( command == API_ADDTASK ) { // add a task
            SET_ADVTASKTODONUMBER((integer)data1); // next task number
            SET_ADVTASKTODO(data2); // next task name
            OWNERSAY(data2); // tell player the next task name
            STATUS();
            return; // command done, return early
        }
 
        // add a hint for the next task to complete
        if( ( command == API_ADDHINT ) && ( (integer)data1 == GET_ADVTASKTODONUMBER()) ) {
            SET_ADVTASKTODOHINT(data2); // next task hint
            OWNERSAY(data2); // tell that player the next task hint
            STATUS();
            return; // command done, return early
        }
 
        // is player done with this task of the adventure?
        if ( ( command == API_DONETASK ) && ( (integer)data1 == GET_ADVTASKTODONUMBER()) ) {
            list temp = GET_ADVTASKCOMP();
            SET_ADVTASKCOMP([(integer)data1] + temp); // add this task number to completed list
            SET_ADVTASKTODONUMBER(0); // clear out the task number we're working on since its done now
            SET_ADVTASKTODO(""); // clear out the task name
            SET_ADVTASKTODOHINT(""); // clear out the task hint
            OWNERSAY(data2); // tell player task is complete
            if ( data3 != "" ) llPlaySound(data3,0.5); // play sound if one was defined
            STATUS();
            return; // command done, return early
        }
 
        // is player done with the entire adventure?
        if ( ( command == API_DONEADV ) && ( data1 == GET_ADVNAME() ) ) {
            SET_ADVTASKCOMP([]); // clear out adventure tasks - we're done
            SET_ADVTASKTODONUMBER(0); // clear out next task number
            SET_ADVTASKTODO(""); // clear out name of next task
            SET_ADVTASKTODOHINT(""); // clear out next task hint
            SET_ADVNAME(API_NONE); // set current adventure name to none
            SET_ADVTEXT(MSG_NO_ADVENTURE); // set the current adventure name to
            OWNERSAY(data2); // tell player adventure is complete
            if ( data3 != "" ) llPlaySound(data3,0.5); // play sound if one was defined
            STATUS();
            return; // command done, return early
        }
    } // end link message
 
    //------------------------------------------------------------------------
    // STATE ENTRY - called on Reset
    //------------------------------------------------------------------------
    state_entry() {
        SETUP(); // show credits and start character sheet load
    }
 
    //------------------------------------------------------------------------
    // TOUCH_START - touch HUD for adventure update
    //------------------------------------------------------------------------
    touch_start(integer total_number) {
        total_number = 0; // LSLINT
        STATUS();
    }
} // end state running
// END