Myriad Lite

// Myriad_Lite-v0.1.14-20131014.lsl
// Copyright (c) 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/
 
// CONSTANTS - DO NOT CHANGE DURING RUN
string BASENAME = "Myriad Lite"; // base name of this script without version or date
string VERSION = "0.1.14"; // Allen Kerensky's script version
string VERSIONDATE = "20131014"; // Allen Kerensky's script yyyymmdd
 
// Module to Module Messaging Constants
integer MODULE_HUD = -1;
integer LM_SENDTOATTACHMENT = 0x80000000;
integer RENDEZVOUS1 = -999; // OLD "region broadcast" channel - chat sent to ALL Myriad players in region
integer RENDEZVOUS2; // NEW "region broadcast" channel
 
// Configuration Items loaded in SETUP()
integer CHANCOMMAND; // chat sent by player to their meter? see OOMA CONFIG,CHANCOMMAND,<number> default 5
integer CHANNEL_OOCCHAT; // what channel for OOC chat? see PPMA CONFIG,CHANOOCCHAT,<number> default 22
integer CHANNEL_OOCEMOTE; // what channel for OOC emote? see PPMA CONFIG,CHANOOCEMOTE,<number> default 23
integer CHANNEL_ICCHAT; // what channel for IC chat? see PPMA CONFIG,CHANICCHAT,<number> default 44
integer CHANNEL_ICTHINK; // what channel for IC thinking out loud? see PPMA CONFIG,CHANICTHINK,<number> default 45
integer CHANNEL_ICEMOTE; // what channel for IC emotes? see PPMA CONFIG,CHANICEMOTE,<number> default 66
integer CHANNEL_NARRATE; // what channel for narrations? see PPMA CONFIG,CHANNARRATE,<number> default 88
string OOCPREFIX; // what to put before OOC messages? see PPMA CONFIG,OOCPREFIX,<string> default ((
string OOCSUFFIX; // what to put after OOC messages? see PPMA CONFIG,OOCSUFFIX,<string> default ))
// FIXME MOVE TO RANGED COMBAT
integer ALLOW_PHYS_BULLETS; // collision or Myriad Bullets? FALSE = Myriad Skill-based bullets. see PPMA CONFIG,PHYSBULLET,<true|false>
 
// RUNTIME GLOBALS - CAN CHANGE DURING RUN
integer CHANPLAYER; // dynamic channel to one player's UUID
integer CHANOBJECT; // dynamic channel to one object's UUID
integer CHANHUD; // dyname channel to HUD object UUID
integer CHANATTACH; // dynamic channel for attachments
integer CHANBAM; // dynamic channel for BAM quests
 
integer HANDRENDV1; // Myriad Rendezvous1 channel handle
integer HANDRENDV2; // Myriad Rendezvous2 channel handle
integer HANDCOMMAND; // command channel handle
integer HANDPLAYER; // player channel handle
integer HANDHUD; // HUD channel handle
integer HANDATTACH; // attachment channel handle
integer HANDBAM; // BAM channel update
 
integer HANDLE_OOCCHAT; // callback handle for llListen
integer HANDLE_OOCEMOTE; // callback handle for llListen
integer HANDLE_ICCHAT; // callback handle for llListen
integer HANDLE_ICTHINK; // callback handle for llListen
integer HANDLE_ICEMOTE; // callback handle for llListen
integer HANDLE_NARRATE; // callback handle for llListen
string TRUENAME; // name to reset item to after talker/emoter use
 
// Texture menu handling
// float bottom left X, bottom left y, top right X, top right Y, command
// coords are float 0.0 - 1.0 for X and Y - command is a string passed to PARSE() function
list TEXMENU;
 
//
// 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);
}
 
//
// PPMA GET_CONFIG
//
string GET_CONFIG(string config) {
    config = llToUpper(config);
    string out = GET_VAL("CONFIG",config);
    if ( out == KEY_NOT_FOUND ) {
        if ( config == "CHANCOMMAND" ) { out = "5"; SET_VAL("CONFIG",config,out);}
        if ( config == "CHANOOCCHAT" ) { out = "22"; SET_VAL("CONFIG",config,out);}
        if ( config == "CHANOOCEMOTE" ) { out = "23"; SET_VAL("CONFIG",config,out);}
        if ( config == "CHANICCHAT" ) { out = "44"; SET_VAL("CONFIG",config,out);}
        if ( config == "CHANICTHINK" ) { out = "45"; SET_VAL("CONFIG",config,out);}
        if ( config == "CHANICEMOTE" ) { out = "66"; SET_VAL("CONFIG",config,out);}
        if ( config == "CHANNARRATE" ) { out = "88"; SET_VAL("CONFIG",config,out);}
        if ( config == "OOCPREFIX" ) { out = "(("; SET_VAL("CONFIG",config,out);}
        if ( config == "OOCSUFFIX" ) { out = "))"; SET_VAL("CONFIG",config,out);}
        if ( config == "PHYSBULLETS" ) { out = "0"; SET_VAL("CONFIG",config,out);}
    }
    return out;
}
 
//
// PPMA GET_NAME - returns player firstname or nickname for Talker/Emoter
//
string GET_NAME() {
    string defaultname = "Myriad Player";
    string name = GET_VAL("CHARACTER","NAME");
    list tokens = llParseString2List(name,[" ",".","_"],[]);
    string firstname = llList2String(tokens,0);
    string lastname = llList2String(tokens,1);
    string nickname = GET_VAL("CHARACTER","NICKNAME");
    if ( name == KEY_NOT_FOUND && nickname == KEY_NOT_FOUND ) return defaultname;
    if ( name != KEY_NOT_FOUND && nickname == KEY_NOT_FOUND ) return name;
    if ( name == KEY_NOT_FOUND && nickname != KEY_NOT_FOUND ) return nickname;
    if ( name != KEY_NOT_FOUND && nickname != KEY_NOT_FOUND ) return firstname+" \""+nickname+"\" "+lastname;
    return defaultname;
}
 
// ABILITY TEST
// Requires ATTRIBUTE NAME, SKILL NAME
// Returns the ability test score for use by success fail, opposed rolls, etc
// See Myriad PDF page 18, Myriad Special Edition page 24
integer ABILITY_TEST(integer attribute,integer skill,integer modifiers) {
    integer highroll = 0; // clear out the highest roll
    while( attribute-- ) { // roll a dice for each point of the attribute
        integer roll = 1+(integer)llFrand(6.0); // roll this d6
        if ( roll > highroll) highroll = roll; // if this is highest roll so far, remember it
    } // finished rolling a dice for each point of the base attribute
    return highroll + skill + modifiers; // now, return the total of highest dice roll + skill value + modifiers
}
 
// CHECK AMMO
CHECKAMMO() {
    llWhisper(CHANATTACH,"CHECKAMMO");
}
 
// COMBATOFF - turn off fist fighter
COMBATOFF() {
    llMessageLinked(LINK_THIS,MODULE_HUD,"COMBATOFF",llGetOwner());
}
 
// COMBATON - turn on fist fighter
COMBATON() {
    llMessageLinked(LINK_THIS,MODULE_HUD,"COMBATON",llGetOwner());
}
 
// COMMAND - process chat and link message commands together
COMMAND(string msg,key id) {
    // break down the commands and messages into units we can work with
    list fields = llParseString2List(msg,["|"," "],[]); // break into list of fields based on "|"ider
    string command = llToLower(llStringTrim(llList2String(fields,0),STRING_TRIM)); // assume the first field is a Myriad Lite command
 
    if ( command == "debug" || command == "error" || command == "help" || command == "ownersay" || command == "rpevent" ) return; // ignore WELL commands FIXME duped from link messages - cleanup entire chat/lm message routing
 
    if ( command == "checkammo" ) { CHECKAMMO(); return;} // ATTACH_FIREARM check ammo in weapons
    if ( command == "combatoff") { COMBATOFF(); return; } // MODULE_SKILL_CLOSE turn off fist fighter
    if ( command == "combaton" ) { COMBATON(); return; } // MODULE_SKILL_CLOSE turn on the fist fighter
    if ( command == "credits" ) { CREDITS(); return;} // ALL MODULES show the credits including version number
    if ( command == "debugoff" ) { DEBUGOFF(); return; } // ALL MODULES player turn off debugging
    if ( command == "debugon" ) { DEBUGON(); return;} // ALL MODULES player turn on debugging
    if ( command == "deceive" ) { DECEIVE(msg); return;} // MODULE_SOCIAL player starts DECEIT-based social attack
    if ( command == "drawboth" ) { DRAW("both"); return; } // ATTACH_FIREARM ATTACH_MELEE draw both weapons
    if ( command == "drawleft" ) { DRAW("left"); return; } // ATTACH_FIREARM ATTACH_MELEE draw weapon in left hand
    if ( command == "drawright" ) { DRAW("right"); return; } // ATTACH_FIREARM ATTACH_MELEE draw weapon using right hand
    if ( command == "dump_records" ) { SENDTOMODULE(msg,id); return; } // MODULE_CHARSHEET
    if ( command == "format_db" ) { SENDTOMODULE(msg,id); return; } // MODULE_CHARSHEET
    if ( command == "holsterboth" ) { HOLSTER("both"); return; } // ATTACH_HOLSTER holster both weapons
    if ( command == "holsterleft" ) { HOLSTER("left"); return; } // ATTACH_HOLSTER holster weapon in left hand
    if ( command == "holsterright" ) { HOLSTER("right"); return; } // ATTACH_HOLSTER holster weapon in right hand
    if ( command == "memory" ) { GET_MEMORY(); return;} // return script memory information
    if ( command == "persuade" ) { PERSUADE(msg); return;} // MODULE_SOCIAL social attack persuasion
    if ( command == "quest" ) { QUEST(); return; } // MODULE_BAM check our current quest status
    if ( command == "reload" ) { RELOAD(); return;}  // ATTACH_FIREARM reload weapons
    if ( command == "reset" ) { RESET(); return;} // ALL MODULES reset HUD and all modules
    if ( command == "safetyoff" ) { SAFETYOFF(); return;} // ATTACH_FIREARM unsafe the weapons
    if ( command == "safetyon" ) { SAFETYON(); return;} // ATTACH_FIREARM safe the weapons
    if ( command == "setup_hud" ) { SETUP_HUD(llToLower(llStringTrim(llList2String(fields,0),STRING_TRIM)),id); return; }
    if ( command == "sheatheboth" ) { SHEATHE("both"); return; } // ATTACH_HOLSTER sheathe both weapons
    if ( command == "sheatheleft" ) { SHEATHE("left"); return; } // ATTACH_HOLSTER sheathe weapon in left hand
    if ( command == "sheatheright" ) { SHEATHE("right"); return; } // ATTACH_HOLSTER sheathe weapon in right hand
    if ( command == "socialoff" ) { SOCIALOFF(); return;} // MODULE_SOCIAL
    if ( command == "socialon" ) { SOCIALON(); return;} // MODULE_SOCIAL
    if ( command == "version" ) { GET_VERSION(); return;} // ALL MODULES show the version internals
    if ( llGetSubString(llStringTrim(llToLower(command),STRING_TRIM),0,4) == "armor" ) { SENDTOMODULE(msg,id); return; } // MODULE_ARMOR
    if ( llGetSubString(llStringTrim(llToLower(command),STRING_TRIM),0,3) == "get_" ) { SENDTOMODULE(msg,id); return; } // MODULE_CHARSHEET GET_*
    if ( llGetSubString(llStringTrim(llToLower(command),STRING_TRIM),0,4) == "list_" ) { SENDTOMODULE(msg,id); return; } // MODULE_CHARSHEET LIST_*
    if ( llGetSubString(llStringTrim(llToLower(command),STRING_TRIM),0,3) == "roll" ) { ROLL(msg); return; } // roll dice
    if ( llGetSubString(llStringTrim(llToLower(command),STRING_TRIM),0,3) == "rlv_" ) { SENDTOMODULE(msg,id); return; } // MODULE_RLV
    if ( llGetSubString(llStringTrim(llToLower(command),STRING_TRIM),0,4) == "rumor" ) { RUMOR(msg); return; } // MODULE_RUMOR
    if ( llGetSubString(llStringTrim(llToLower(command),STRING_TRIM),0,3) == "set_" ) { SENDTOMODULE(msg,id); return; } // MODULE_CHARSHEET SET_*
}
 
// CREDITS comply with Myriad RPG Creative Common-Attribution legal requirement
CREDITS() {
    OWNERSAY("The Myriad RPG System was designed, written, and illustrated by Ashok Desai.");
    OWNERSAY("RPG System licensed under the Creative Commons Attribution 2.0 UK: England and Wales.");
    OWNERSAY("Myriad Lite v"+VERSION+" "+VERSIONDATE+" Copyright (c) 2011-2014 by Allen Kerensky (OSG/SL)");
    OWNERSAY("Licensed under Creative Commons Attribution (CC-BY) 3.0 Unported and BSD 3-clause.");
    // llMessageLinked(LINK_THIS,MODULE_HUD,"VERSION",llGetOwner());
}
 
// DEBUG - show debug chat with wearer name for sorting
DEBUG(string dmessage) {
    if ( GET_FLAG("DEBUG") == TRUE ) llMessageLinked(LINK_THIS,MODULE_HUD,"DEBUG|"+dmessage,llGetOwner());
}
 
// DEBUGOFF - turn off the DEBUG flag
DEBUGOFF() {
    SET_FLAG("DEBUG",FALSE); // set debug flag to FALSE
    OWNERSAY("Debug Mode Deactivated");
}
 
// DEBUGON - turn on the DEBUG flag
DEBUGON() {
    SET_FLAG("DEBUG",TRUE); // set debug flag TRUE
    OWNERSAY("Debug Mode Activated");
}
 
// DECEIT SOCIAL COMBAT SKILL
DECEIVE(string msg) {
    string message = llDumpList2String( llList2List( llParseString2List(msg,[" "],[]) , 1, -1 ) , " " );
    if ( llStringLength( message ) > 3 && message != "deceive" ) {
        llSetObjectName(GET_NAME());
        llSay(PUBLIC_CHANNEL,"/me says, \""+message+"\"");
        llSetObjectName(TRUENAME);
    }
 
    // FIXME - if in mouselook and cursor pointing at 1 avatar, make TARGETED SOCIAL ATTACK else make AREA SOCIAL ATTACK
    // FIXME if in mouselook, focus the attack on that one person, with a bonus Gotta look them in the eye when you're lying LOL
    llMessageLinked(LINK_THIS,MODULE_HUD,"DECEIVE",llGetOwner());
}
 
// DRAW weapons
DRAW(string hand) {
    if ( hand == "left" ) { llWhisper(CHANATTACH,"DRAWLEFT"); return; } // draw left-hand weapon
    if ( hand == "right" ) { llWhisper(CHANATTACH,"DRAWRIGHT"); return; } // draw right-hand weapon
    if ( hand == "both" ) { llWhisper(CHANATTACH,"DRAWBOTH"); return; } // draw both weapons
}
 
// ERROR - show errors on debug channel with wearer name for sorting
ERROR(string emessage) {
    llMessageLinked(LINK_THIS,MODULE_HUD,"ERROR|"+emessage,llGetOwner());
}
 
// MEMORY
GET_MEMORY() {
    OWNERSAY(BASENAME+" free memory: "+(string)llGetFreeMemory()); // show this module's free memory info
    llMessageLinked(LINK_THIS,MODULE_HUD,"MEMORY",llGetOwner()); // trigger the rest of the modules too
}
 
// GETVERSION
GET_VERSION() {
    OWNERSAY(BASENAME+" v"+VERSION+"-"+VERSIONDATE); // show this module's version info
    llMessageLinked(LINK_THIS,MODULE_HUD,"VERSION",llGetOwner()); // trigger the rest of the modules too
}
 
// HELP
HELP(string help) {
    llMessageLinked(LINK_THIS,MODULE_HUD,"HELP|"+help,llGetOwner());
}
 
// HOLSTER weapons
HOLSTER(string hand) {
    if ( hand == "left" ) { llWhisper(CHANATTACH,"HOLSTERLEFT"); return; } // holster left-hand weapon
    if ( hand == "right" ) { llWhisper(CHANATTACH,"HOLSTERRIGHT"); return; } // holster right-hand weapon
    if ( hand == "both" ) { llWhisper(CHANATTACH,"HOLSTERBOTH"); return; } // holster both weapons
}
 
// METER - update a hovertext health meter or HUD bar graph
METER() {
    llMessageLinked(LINK_THIS,MODULE_HUD,"METER",llGetOwner());
}
 
// OWNERSAY - sent messages to player only
OWNERSAY(string message) {
    llMessageLinked(LINK_THIS,MODULE_HUD,"OWNERSAY|"+message,llGetOwner());
}
 
// PERSUASION SOCIAL COMBAT SKILL
PERSUADE(string msg) {
    string message = llDumpList2String( llList2List( llParseString2List(msg,[" "],[]) , 1, -1 ) , " " );
    if ( llStringLength( message ) > 3 && message != "persuade" ) {
        llSetObjectName(GET_NAME());
        llSay(PUBLIC_CHANNEL,"/me says, \""+message+"\"");
        llSetObjectName(TRUENAME);
    }
 
    // FIXME - if in mouselook and cursor pointing at 1 avatar, make TARGETED SOCIAL ATTACK else make AREA SOCIAL ATTACK
    // FIXME if in mouselook, focus the attack on that one person, with a bonus Gotta look them in the eye when you're lying LOL
    llMessageLinked(LINK_THIS,MODULE_HUD,"PERSUADE",llGetOwner());
}
 
// QUEST STATUS
QUEST() {
    llMessageLinked(LINK_THIS,MODULE_HUD,"BAMSTATUS",llGetOwner()); // send a status request to BAM Modules
}
 
// RELOAD
RELOAD() {
    llWhisper(CHANATTACH,"RELOAD");
}
 
// RESET - shut down running animations then reset the script to reload character sheet
RESET() {
    if ( GET_FLAG("DEAD") == TRUE || GET_FLAG("INCAPACITATED") == TRUE ) { // don't allow reset if already on respawn timer
        OWNERSAY("Cannot reset while incapacitated or dead. You will respawn in a few moments.");
        return;
    }
    OWNERSAY("Resetting Myriad Lite. Please wait...");
    llMessageLinked(LINK_THIS,MODULE_HUD,"RESET",llGetOwner()); // send reset to modules
    llResetScript(); // now reset this script
}
 
// ROLL DICE
ROLL(string message) {
    list input = llParseString2List(message, [" ","d"],[] );
    string command = llList2String(input,0);
    integer numberOfDice = llList2Integer(input,1);
    integer numberOfSides = llList2Integer(input,2);
    string output = "";
    integer total = 0;
    if ( command == "roll" ) {
        if ( numberOfDice >= 1 && numberOfDice <= 20 ) {
            if ( numberOfSides > 1 && numberOfSides <= 100 ) {
                integer index;
                for( index = 1; index <= numberOfDice; index++) {
                    integer roll = (integer)((llFrand(1.0) * numberOfSides)+1);
                    output += (string)roll;
                    output += ", ";
                    total += roll;                          
                }
                string ownername = llList2String(llParseString2List(llKey2Name(llGetOwner()),["@"],[]),0); // strip @where from HG names
                llSetObjectName("⚅ "+ownername);
                llWhisper(PUBLIC_CHANNEL,"/me rolls "+(string)numberOfDice+"d"+(string)numberOfSides+" resulting in "+output+" totalling "+(string)total+".");
                llSetObjectName(TRUENAME);
                return;
            }
        }
        HELP("To roll, say /5 roll #d# or /5 roll #d#. For example, saying /5 roll 1d20 in chat rolls one 20-sided dice.\"");
    }
}
 
// RPEVENT
//RPEVENT(string rpevent) {
//    llMessageLinked(LINK_THIS,MODULE_HUD,"RPEVENT|"+rpevent,llGetOwner());
//}
 
//
// RPEVENTIN - handle incoming RPevents here to prevent link message loops
//
string LAST_RP_MSG;
RPEVENTIN(string rpevent) {
    if ( rpevent == LAST_RP_MSG ) return; // ignore duplicates
    LAST_RP_MSG = rpevent; // remember this one
    string objname = llGetObjectName();
    llSetObjectName("★");    
    llOwnerSay("/me "+rpevent); // now tell the owner the rest of the RPEVENT| message
    llSetObjectName(objname);    
}
 
// RUMOR CONTROL
RUMOR(string cmdrumor) {
    DEBUG("Sending to rumor module: "+cmdrumor);
    llMessageLinked(LINK_THIS,MODULE_HUD,cmdrumor,llGetOwner()); // relay rumor commands to module
}
 
// SAFETY OFF
SAFETYOFF() {
    llWhisper(CHANATTACH, "SAFETYOFF");
}
 
// SAFETY ON
SAFETYON() {
    llWhisper(CHANATTACH, "SAFETYON");
}
 
// SENDTOATTACHMENT
SENDTOATTACHMENT(string msg) {
    DEBUG("SENDTOATTACHMENT("+msg+")");
    llWhisper(CHANATTACH,msg);
}
 
// SENDTOMODULE
SENDTOMODULE(string msg,key speaker) {
    if ( llGetSubString(llStringTrim(llToLower(msg),STRING_TRIM),0,3) == "set_" ) { // is this a privileged SET_* command?
        if ( llList2Key(llGetParcelDetails(<0,0,0>,[PARCEL_DETAILS_OWNER]),0) != speaker ) { // if so, check speaker is region owner or object owned by region owner
            ERROR("Ignoring request ["+msg+"] from unprivileged speaker ["+llKey2Name(llGetOwnerKey(speaker))+"]");
            return;
        }
    }
    // allow safer GET_* and LIST_* commands to pass through
    llMessageLinked(LINK_THIS,MODULE_HUD,msg,speaker);
}
 
// SETUP - begin bringing the HUD online
SETUP() {
    SET_FLAG("DEBUG",FALSE);
    CREDITS(); // show Myriad credits as required by the Creative Commons - Attribution license
    llSetText("",<0,0,0>,0); // clear any previous hovertext
 
    // Configuration Items
    CHANCOMMAND = (integer)GET_CONFIG("CHANCOMMAND");
    CHANNEL_OOCCHAT = (integer)GET_CONFIG("CHANOOCCHAT");
    CHANNEL_OOCEMOTE = (integer)GET_CONFIG("CHANOOCEMOTE");
    CHANNEL_ICCHAT = (integer)GET_CONFIG("CHANICCHAT");
    CHANNEL_ICTHINK = (integer)GET_CONFIG("CHANICTHINK");
    CHANNEL_ICEMOTE = (integer)GET_CONFIG("CHANICEMOTE");
    CHANNEL_NARRATE = (integer)GET_CONFIG("CHANNARRATE");    
    OOCPREFIX = GET_CONFIG("OOCPREFIX");
    OOCSUFFIX = GET_CONFIG("OOCSUFFIX");
    ALLOW_PHYS_BULLETS = (integer)GET_CONFIG("PHYSBULLETS");    
 
    // Talker/Emoter setup
    TRUENAME = BASENAME + " " + VERSION + " " + VERSIONDATE; // put together full item name for talker/emoter
    llSetObjectName(TRUENAME); // force object title back if the talker/emoter messed it up
    //OWNERSAY("Character Sheet loaded. You are now ready to roleplay.");
    if ( HANDRENDV1 != 0 ) llListenRemove(HANDRENDV1);
    HANDRENDV1 = llListen(RENDEZVOUS1,"",NULL_KEY,""); // setup listener for Myriad RP events
    if ( HANDRENDV2 != 0 ) llListenRemove(HANDRENDV2);
    list details = llGetParcelDetails(<0,0,0>,[PARCEL_DETAILS_ID]);
    string parcelid = llList2String(details,0);
    RENDEZVOUS2 = (integer)("0x"+llGetSubString(parcelid,0,7));    
    HANDRENDV2 = llListen(RENDEZVOUS2,"",NULL_KEY,"");
    if ( HANDCOMMAND != 0 ) llListenRemove(HANDCOMMAND);
    HANDCOMMAND = llListen(CHANCOMMAND,"",llGetOwner(),""); // listen to chat commands from owner
    CHANHUD = (integer)("0x"+llGetSubString((string)llGetKey(),0,6)); // calculate a player-specfic dynamic chat channel
    if ( HANDHUD != 0 ) llListenRemove(HANDHUD);
    HANDHUD = llListen(CHANHUD,"",NULL_KEY,""); // listen on the HUD object dynamic chat channel
    CHANPLAYER = (integer)("0x"+llGetSubString((string)llGetOwner(),0,6)); // calculate a player-specfic dynamic chat channel
    if ( HANDPLAYER != 0 ) llListenRemove(HANDPLAYER);
    HANDPLAYER = llListen(CHANPLAYER,"",NULL_KEY,""); // listen on the player dynamic chat channel
    CHANATTACH = (integer)("0x"+llGetSubString((string)llGetOwner(),1,7)); // attachment-specific channel
    if ( HANDATTACH != 0 ) llListenRemove(HANDATTACH);
    HANDATTACH = llListen(CHANATTACH,"",NULL_KEY,""); // listen for messages from attachments
    CHANBAM = (integer)("0x" + llGetSubString((string)llGetOwner(),-7,-1));
    if ( HANDBAM != 0 ) llListenRemove(HANDBAM);
    HANDBAM = llListen(CHANBAM,"",NULL_KEY,""); // start listener with listenremove handle
 
    // Talker/Emoter
    if ( HANDLE_OOCCHAT != 0 ) llListenRemove(HANDLE_OOCCHAT);
    HANDLE_OOCCHAT = llListen(CHANNEL_OOCCHAT,"",llGetOwner(),"");
    if ( HANDLE_OOCEMOTE != 0 ) llListenRemove(HANDLE_OOCEMOTE);
    HANDLE_OOCEMOTE = llListen(CHANNEL_OOCEMOTE,"",llGetOwner(),"");
    if ( HANDLE_ICCHAT != 0 ) llListenRemove(HANDLE_ICCHAT);
    HANDLE_ICCHAT  = llListen(CHANNEL_ICCHAT,"",llGetOwner(),"");
    if ( HANDLE_ICTHINK != 0 ) llListenRemove(HANDLE_ICTHINK);
    HANDLE_ICTHINK  = llListen(CHANNEL_ICTHINK,"",llGetOwner(),"");
    if ( HANDLE_ICEMOTE != 0 ) llListenRemove(HANDLE_ICEMOTE);
    HANDLE_ICEMOTE = llListen(CHANNEL_ICEMOTE,"",llGetOwner(),"");
    if ( HANDLE_NARRATE != 0 ) llListenRemove(HANDLE_NARRATE);
    HANDLE_NARRATE = llListen(CHANNEL_NARRATE,"",llGetOwner(),"");
 
    OWNERSAY("Registering any Myriad Lite-compatible attachments...");
    llWhisper(CHANATTACH,"REGISTERATTACHMENTS"); // ask for attachments on their dynamic channel
    METER(); // update hovertext
    QUEST(); // update the BAM Module
    OWNERSAY("Core module active.");
}
 
SETUP_HUD(string texmenu,key id) {
    if ( llList2Key(llGetParcelDetails(<0,0,0>,[PARCEL_DETAILS_OWNER]),0) != id ) { // if so, check speaker is region owner or object owned by region owner
        ERROR("Ignoring SETUP_HUD command ["+texmenu+"] from unprivileged speaker ["+llKey2Name(llGetOwnerKey(id))+"]");
        return;
    }
    list tokens = llCSV2List(texmenu); // convert CSV in fromtexturename, and coord/command lists to list
    key texid = llGetInventoryKey(llList2String(tokens,0)); // first item is texture to show
    if ( texid != NULL_KEY ) {
        llSetLinkPrimitiveParamsFast(1,[PRIM_TEXTURE,ALL_SIDES,texid,<1,1,0>,<0,0,0>,0.0]);
        // FIXME make sure the BLX,BLY,TRX,TRY,COMMAND format is held to
        TEXMENU = llList2List(tokens,1,-1); // put the rest in to menu list
    }
}
 
// SHEATHE weapons
SHEATHE(string hand) {
    if ( hand == "left" ) { llWhisper(CHANATTACH,"SHEATHELEFT"); return; } // sheathe left-hand weapon
    if ( hand == "right" ) { llWhisper(CHANATTACH,"SHEATHERIGHT"); return; } // sheathe right-hand weapon
    if ( hand == "both" ) { llWhisper(CHANATTACH,"SHEATHEBOTH"); return; } // sheathe both weapons
}
 
// SOCIALOFF - turn off social combat module
SOCIALOFF() {
    llMessageLinked(LINK_THIS,MODULE_HUD,"SOCIALOFF",llGetOwner());
}
 
// SOCIALON - turn on social combat module
SOCIALON() {
    llMessageLinked(LINK_THIS,MODULE_HUD,"SOCIALON",llGetOwner());
}
 
// FIXME MOVE TO GENERIC SKILL MODULE?
// An Unopposed Ability Test - Myriad PDF p. 19, Myriad Special Edition p. 25
// Requires TargetNumber, Stat Name, Skill Name, one or more things to do on success, one or more things to do on fail
// UNOPPOSED_TEST|<target number>|<stat name>|<skill name>|<CSV success actions>|<CSV fail actions>
// CSV success and fail actions are CSV lists action1[,action2[,...]]
// Each action is an embedded link message which uses ^ in place of the usual | used to separate fields
// Example Actions: SAY^<channel#>^<message_with_no_commas> , SHOUT^<channel#>,<message_with_no_commas>
// See the WELL module for a preset library of actions you can stack from, but any valid link message is possible
// FIXME This is a huge cheat and security hole as is without testing for valid actions by players vs. region owners
// Returns TRUE for Success and False for Fail
string valid = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.?!,^";
integer UNOPPOSED_TEST1(integer targetnum,string statname, string skillname,string onsuccess,string onfail) {
    DEBUG("CORE UNOPPOSED_TEST targetnum=["+(string)targetnum+"] statname=["+statname+"] skillname=["+skillname+"] onsuccess=["+onsuccess+"] onfail=["+onfail+"]");
    // FIXME check targetnum - replace constants with MIN_TNUM/MAX_TNUM
    if ( targetnum < 4 || targetnum > 17 ) { ERROR("Invalid target number "+(string)targetnum+" in unopposed test. Ending unopposed test."); return FALSE; }
    // FIXME check stat range - replace constants with MIN_STAT/MAX_STAT
    integer tstat = (integer)GET_VAL("STATISTIC",statname);
    if ( tstat < 0 || tstat > 10 ) { ERROR("Invalid stat amount "+(string)tstat+" in unopposed test. Ending unopposed test."); return FALSE;}
    // FIXME check skill range - replace constants with MIN_SKILL/MAX_SKILL
    integer tskill = (integer)GET_VAL("SKILL",skillname);
    if ( tskill < 0 || tskill > 7 ) { ERROR("Invalid skill amount "+(string)tskill+" in unopposed test. Ending unopposed test."); return FALSE;}
    // FIXME modifiers
    integer tmods = 0;
    integer check = ABILITY_TEST(tstat,tskill,tmods); // calculate the player's ability test value (stat, skill, modifiers
    integer result;
    string acts;
    if ( check >= targetnum ) {
        acts = onsuccess;
        result = TRUE;
    } else {  // player lost the ability test
        acts = onfail;
        result = FALSE;
    }
    // Do onsuccess or onfail actions
    // ugly string filter and conversion of ^ to | for passing to link message
    list actions = llCSV2List(acts); // split onsuccess or onfail into list of actions separated by commas
    integer count = llGetListLength(actions); // how many actions in list
    integer i; // temp variable for action by action loop
    for ( i = 0; i < count; i++ ) { // step through each action in comma list of actions
        string well = llList2String(actions,i); // get this specific action
        string out = ""; // start a new empty output string
        integer len = llStringLength(well); // get the length of this action string
        integer j; // temp variable for character by character loop
        for ( j = 0; j < len; j++) { // step through each character of current action
            string char = llGetSubString(well,j,j); // get the current character
            if ( char != "^" && llSubStringIndex(valid,char) >= 0 ) { // if the character is not a ^ and is in the list of VALID string characters
                out = out + char; // add it to the current output string
            } else { // otherwise its invalid
                out = out + " "; // and add a placeholder space instead
            } // done with valid character scan, excepting the ^
            if ( char == "^" ) out = out + "|"; // replace the ^ with a | divider used in link messages to WELL
        } // done with all actions in this list
        llMessageLinked(LINK_THIS,MODULE_HUD,out,llGetOwner()); // fire the resulting cleaned/converted action over to WELL or other hud module to act upon
        // and go on  to next action in list if any
    } // all done with actions
    return result;
}
 
UNOPPOSED_TEST2(key id,string task, string stat,string skill) {
    integer chan = (integer)("0x"+llGetSubString(id,0,6)); // calculate dynamic channel of object that triggered this test
    OWNERSAY("Unopposed Ability Test: "+task); // tell the player the task we're starting
    integer statamount = (integer)GET_VAL("STATISTIC",stat);
    if ( statamount == 0 ) return; // no such stat, can't really test or succeed can we
    if ( statamount < 0 || statamount > 10 ) return; // bogus info returned - FIXME MINSTAT/MAXSTAT
    integer skillamount = (integer)GET_VAL("SKILL",skill);
    // note skillamount = 0 is still a valid return - unskilled tests just have harder time to succeed, based on raw stat only
    if ( skillamount < 0 || skillamount > 7 ) return; // bogus info returned - FIXME MINSKILL/MAXSKILL
    integer modifiers = 0; // FIXME calculate applicable modifiers
    llRegionSay(chan,"UNOPPOSED_TEST2|"+(string)statamount+"|"+(string)skillamount+"|"+(string)modifiers);
}
 
// DEFAULT STATE - load character sheet
default {
 
    // ATTACH - logged in with meter or worn from inventory/ground while running
    attach(key id) {
        if ( id != NULL_KEY ) {
            RESET();
        }
    }
 
    // COLLISION_START -- FIXME move to Ranged COmbat module?
    collision_start(integer num_detected) {
        if ( ALLOW_PHYS_BULLETS == FALSE ) { return;} // skip phys bullet code
        if ( GET_FLAG("DEAD") == TRUE ) { return; } // can't take more damage when dead, but you can when incapacitated
        while ( num_detected-- ) { // loop through all hits this server frame
            float impactspeed = llVecMag(llDetectedVel(num_detected)); 
            if ( impactspeed > 15.0 && impactspeed <= 255.0 ) { // hit by something moving between 15.0 and 255.0 m/s
                integer impact = llRound( (impactspeed - 15.0) / 5.0 ) + 1; // calculate 1-5 range bands of damage from 0.0 to 240.0 speed
                // RANGEDCOMBAT|attackdice (1-5)|hitwho (my UUID here)|hitbywho (shooter UUID)|hitbywhat (objectname)
                string impactmsg = "RANGEDCOMBAT"+"|"+(string)impact+"|"+(string)llGetOwner()+"|"+(string)llGetOwnerKey(llDetectedKey(num_detected))+"|"+llDetectedName(num_detected);
                llMessageLinked(LINK_THIS,MODULE_HUD,impactmsg,llDetectedKey(num_detected));
            }
        }
    }
 
    // CHANGED - triggered for many changes to the avatar
    // TODO reload sim-specific settings on region change
    changed(integer changes) {
        if ( changes & CHANGED_INVENTORY ) { // inventory changed somehow?
            //OWNERSAY("Inventory changed. Reloading character sheet."); // FIXME do we still need this?
            RESET(); // saved a new character sheet? - reset and re-read it.
        }
    }
 
    // LINK MESSAGE - commands to and from other prims in HUD
    link_message(integer sender,integer sending_module,string str, key id) {
        if ( sending_module == MODULE_HUD ) return; // ignore our own link messages
 
                list fields = llParseString2List(str,["|"],[]); // break into list of fields based on "|"ider
        string command = llToLower(llStringTrim(llList2String(fields,0),STRING_TRIM)); // assume the first field is a Myriad Lite command
        if ( command == "debug" || command == "error" || command == "help" || command == "ownersay" || command == "rpevent" ) return; // ignore WELL commands - region RPevents come in through LISTEN on RENDEZVOUS1
 
        // show debug only when not ignoring other commands
        DEBUG("EVENT: link_message("+(string)sender+","+(string)sending_module+","+str+","+(string)id+")");
 
        if ( llGetSubString(command,0,4) == "armor" ) { SENDTOATTACHMENT(str); return; } // process armor messages
        if ( sending_module == LM_SENDTOATTACHMENT ) { SENDTOATTACHMENT(str); return; } // send module messages to attachments
        COMMAND(str,id); // send to shared command processor for chat and link messages
        return;
    }
 
    // LISTEN - the main Myriad Lite message processor for RP events and player commands
    listen(integer channel, string speakername, key speakerid, string message) {
        DEBUG("HUD Listen: channel=["+(string)channel+"] name=["+speakername+"] id=["+(string)speakerid+"] message=["+message+"]");
        // calculate the dynamic channel of who is speaking in case we need to return commands
        CHANOBJECT = (integer)("0x"+llGetSubString((string)speakerid,0,6));
 
        // 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 = llList2String(fields,0); // assume the first field is a Myriad Lite command
 
        // --- PLAYER COMMAND CHANNEL
        if ( channel == CHANCOMMAND ) { // handle player chat commands
            COMMAND(message,speakerid); // send to shared command processor for chat and link messages
            return;
        } // end of if channel == player commands
 
        // --- BAM CHANNEL
        if ( channel == CHANBAM ) {
            SENDTOMODULE(message,speakerid); // send BAM to Module
            return;
        } // end if channel BAMCHAN
 
        // --- Myriad Lite regionwide messages
        if ( channel == RENDEZVOUS1 || channel == RENDEZVOUS2 ) { // handle Myriad system messages    
            if ( command == "RPEVENT" ) { // Myriad Lite RPEVENT - roleplay events everyone might find interesting
                RPEVENTIN(llList2String(fields,1));
                return;
            } // end if RPEVENT
            return;
        } // end if channel == RENDEZVOUSx
 
        // --- ATTACHMENT CHANNEL
        if ( channel == CHANATTACH ) { // handle the attachment commands
            if ( GET_FLAG("DEAD") == TRUE || GET_FLAG("INCAPACITATED") == TRUE ) return; // can't mess with attachments while down
            if ( llToLower(llGetSubString(llStringTrim(command,STRING_TRIM),0,4)) == "armor" ) { SENDTOMODULE(message,llGetOwner()); return; } // process armor messages
            if ( command == "ATTACHMELEE" || command == "ATTACHRANGED" ) { // holding a weapon rather than using fists?
                llMessageLinked(LINK_THIS,MODULE_HUD,message,llGetOwner());
                return;
            }
            if ( command == "DETACHMELEE" || command == "DETACHRANGED" ) { // are we going back to fists?
                llMessageLinked(LINK_THIS,MODULE_HUD,message,llGetOwner());            
                return;
            }
            if ( command == "ATTACHMETER" || command == "DETACHMETER" ) {
                llMessageLinked(LINK_THIS,MODULE_HUD,message,llGetOwner());
                //METERWORN = TRUE; // we need to send meter events
                //METER(); // send update
                return;
            }
        }
        // --- CHANHUD - get region settings messages
        if ( channel == CHANHUD ) {
            llMessageLinked(LINK_THIS,MODULE_HUD,message,speakerid);
            return; // done with the HUD dynamic channel message processing, return            
        }
        // --- CHANPLAYER
        if ( channel == CHANPLAYER ) { // handle player dynamic commands
            if ( command == "RPEVENT" ) { // Myriad Lite RPEVENT - roleplay events everyone might find interesting
                RPEVENTIN(llList2String(fields,1));
                return;
            } // end if RPEVENT
            // incoming message from rumor server?
            if ( llGetSubString(llToLower(llStringTrim(command,STRING_TRIM)),0,4) == "rumor" ) {
                llMessageLinked(LINK_THIS,MODULE_HUD,message,speakerid); // send message and key of speaker to rumors
                return;
            }
            if ( command == "UNOPPOSED_TEST1" ) { // object in sim wants a simple skill check
                integer targetnum = llList2Integer(fields,1); // what is unopposed check target num?
                string tattrib = llList2String(fields,2); // target attribute
                string tskill = llList2String(fields,3); // target skill
                string onsuccess = llList2String(fields,4); // what to do on success
                string onfail = llList2String(fields,5); // what to do on fail
                UNOPPOSED_TEST1(targetnum,tattrib,tskill,onsuccess,onfail);
                return;
            }
            if ( command == "UNOPPOSED_TEST2" ) {
                string testtask = llList2String(fields,1); // target attribute
                string teststat = llList2String(fields,2); // target skill
                string testskill = llList2String(fields,3); // what to do on success
                UNOPPOSED_TEST2(speakerid,testtask,teststat,testskill);                
                return;
            }
            // Check social combat before physical - can still talk your way out of trouble when physically incapacitated
            if ( (command == "DECEIVE" || command == "PERSUADE") &&  GET_FLAG("DEAD") == TRUE ) return; // can't argue when dead, but you can when incapacitated
            if ( (command == "CLOSECOMBAT" || command == "RANGEDCOMBAT") && ( GET_FLAG("DEAD") == TRUE || GET_FLAG("INCAPACITATED") == TRUE ) ) return; // can't fight when dead or down
            // The last action of the PLAYER CHAN processing is to put the message on the link-message bus
            // the speakerid is sent as the uuid field in case the modules need to send a message back out to another dynamic channel object
            // SOCIAL COMBAT: ATTACKER: DECEIVE, PERSUADE - DEFENDER: DECEITATTACK, PERSUASIONATTACK
            llMessageLinked(LINK_THIS,MODULE_HUD,message,speakerid); // passthru incoming messages including Myriad Bullets
            return; // done with the player channel message processing, return
        } // end of if channel CHANPLAYER
        // Handle Out-Of-Character speaking
        if ( channel == CHANNEL_OOCCHAT ) {
            string ownername = llList2String(llParseString2List(llKey2Name(llGetOwner()),["@"],[]),0); // strip @where from HG names
            llSetObjectName(OOCPREFIX+ownername);
            llSay(PUBLIC_CHANNEL,"/me says, \""+message+"\""+OOCSUFFIX);
            llSetObjectName(TRUENAME);
            return;
        }
        // Handle Out-Of-Character emotes
        if ( channel == CHANNEL_OOCEMOTE ) {
            string ownername = llList2String(llParseString2List(llKey2Name(llGetOwner()),["@"],[]),0); // strip @where from HG names            
            llSetObjectName(OOCPREFIX+ownername);
            llSay(PUBLIC_CHANNEL,"/me "+message+OOCSUFFIX);
            llSetObjectName(TRUENAME);
            return;
        }        
        // Handle In-Character speaking
        if ( channel == CHANNEL_ICCHAT ) {
            llSetObjectName(GET_NAME());
            llSay(PUBLIC_CHANNEL,"/me says, \""+message+"\"");
            llSetObjectName(TRUENAME);
            return;
        }
        // Handle In-Character thinking
        if ( channel == CHANNEL_ICTHINK ) {
            llSetObjectName(GET_NAME());
            llSay(PUBLIC_CHANNEL,"/me thinks, \'"+message+"\'");
            llSetObjectName(TRUENAME);
            return;
        }
        // Handle In-Character emotes
        if ( channel == CHANNEL_ICEMOTE ) {
            llSetObjectName(GET_NAME());
            llSay(PUBLIC_CHANNEL,"/me "+message);
            llSetObjectName(TRUENAME);
            return;
        }
        // Handle Narration
        if ( channel == CHANNEL_NARRATE ) {
            llSetObjectName("❖");
            llSay(PUBLIC_CHANNEL,"/me "+message);
            llSetObjectName(TRUENAME);
            return;
        }
    } // end listen
 
    // ON_REZ - logged in with meter, or worn from inventory while running
    on_rez(integer param) {
        param = 0; // LSLINT
        RESET(); // a reset to reload character
    }
 
    // STATE ENTRY
    state_entry() {
        SETUP();
        SETUP_HUD("MyriadLogoWhiteTransparent512,0.0,0.0,1.0,1.0,dump_records",llGetOwner());        
    }
 
    // TOUCH_START - touch HUD for adventure update
    touch_start(integer total_number) {
        integer primnum = llDetectedLinkNumber(0); 
        string action = llGetLinkName(primnum); // get name of prim clicked in link set
        if ( llGetListLength(TEXMENU) > 0 && action == llGetObjectName() ) { // click rootprim, use texture button coords
            vector coord = llDetectedTouchST(0);
            integer i;
            for (i = 0; i < llGetListLength(TEXMENU); i+=5 ) {
                // strided list - X1,Y1,X2,Y2,Command
                float x1 = llList2Float(TEXMENU,i);
                float y1 = llList2Float(TEXMENU,i + 1 );
                float x2 = llList2Float(TEXMENU,i + 2 );
                float y2 = llList2Float(TEXMENU,i + 3 );
                string button = llList2String(TEXMENU,i + 4);
                if ( coord.x >= x1 && coord.x <= x2 && coord.y >= y1 && coord.y <= y2 ) {
                    COMMAND(button,llDetectedKey(0));
                }
            }
        }
        if ( action != "" && action != llGetObjectName() ) { // someone clicked a named button prim on this linkset
            COMMAND(action,llDetectedKey(0)); // try that prim name as a command
            return;
        }
        METER();
        QUEST();
    }
} // end state running
// END