Semaphore - main script

Code summary: 

This is the main script to put in an attachment somewhere.

// Semaphore Animation Script
// Ordinal Malaprop
// 2007-09-15

// LICENCE
// This script is licenced under a Creative Commons
// Attribution-Noncommercial-Share Alike licence - see
//      http://creativecommons.org/licenses/by-nc-sa/3.0/
// for exact details. To sum up, feel free to distribute or modify
// but it must retain my name and this licence and can't be sold,
// even for L$1.

//--------------------------------------------------------------------
// Globals and constants

// Flag positions for characters
// Every pair of digits indicates right+left arm positions, from 0 to 6
// The index of the correct pair is the index of the character in
// the LETTERS or NUMBERS list, x2
string FLAG_POS = "5512244535251552535436401341424344303132333421221403042363";

// (rest)(numerals)(cancel)ABCDEFGHIJKLMNOPQRSTUVWXYZ
// or in numeric mode
// (r)(n)(c)123456789(letters)0..(negative)
string LETTERS = " n#ABCDEFGHIJKLMNOPQRSTUVWXYZ"; // when in letter mode
string NUMBERS = " ##123456789l0"; // when in number mode
string LEGAL = " ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; // all allowed input characters

// Wait this long between signals
float gInterval = 1.0;

// Channel for command input
integer CHANNEL_INPUT = 9;
// Channel for public output
integer CHANNEL_PUBLIC = -34562421;
// Channel for local communication with attachments
integer CHANNEL_ATTACH = -89823958;

// The current message being processed
string gMessage = "";
// Next message to process
string gMessageQueue = "";
// The final, cleaned message - say this at the end
string gFinalMsg = "";
// Current mode - 0 = letter, 1 = numbers
integer gMode = 0;
// Are we translating other people's messages?
integer gTranslating = TRUE;

// Assorted constants
string ANIM_POSE = "semaphore pose";
string ANIM_ERROR = "error";
string SOUND_CLICK = "4c8c3c77-de8d-bde2-b9b8-32635e0fd4a6";
key TEXTURE_ACTIVE = "30465632-5ac5-378f-a6bf-46ab56883e7a";
key TEXTURE_DISABLED = "2ecd1b3f-4659-2bf9-fda1-a4c47d30c78f";
// Some link numbers, hardcoded, naughty
integer LINK_STATUS_DISPLAY = 2;
integer LINK_INT_BUTTON = 3;
integer LINK_INT_DISPLAY = 4;
integer LINK_ERROR = 5;

//--------------------------------------------------------------------
// Functions

// update_display
// Updates the HUD's display features
update_display()
{
        string interval = llGetSubString((string)gInterval, 0, 2);
        llMessageLinked(LINK_INT_DISPLAY, -3, interval, NULL_KEY);
        llOwnerSay("Interval between signals is " + interval + " seconds");
}

// local_msg
// Function to send messages to attachments etc
local_msg(string msg)
{
        llWhisper(CHANNEL_ATTACH, msg);
}

// public_msg
// Function to send messages publicly
public_msg(string msg)
{
        llShout(CHANNEL_PUBLIC, llKey2Name(llGetOwner()) + "|" + msg);
}

update_translate(integer translating)
{
        gTranslating = translating;
        string word = "";
        if (!gTranslating) {
                word = "not ";
        }
        llOwnerSay("Currently " + word + "translating other people's semaphore messages");
}

// preload
// Preloads all the animations in the HUD. It does this each time you
// activate it, so that anyone nearby will have them preloaded.
preload()
{
        integer n = llGetInventoryNumber(INVENTORY_ANIMATION);
        integer f = 0;
        string name = "";
        do {
                name = llGetInventoryName(INVENTORY_ANIMATION, f);
                if (name != ANIM_POSE) {
                        llStartAnimation(name);
                        llStopAnimation(name);
                }
        } while (++f < n);
        llStartAnimation(ANIM_POSE);
}

// process_next_char
// Main routine function - process the next letter or number in the current message
// and returns the message with that trimmed off.
string process_next_char(string msg)
{
        // Quit if there's nothing more to do
        if (msg == "") return msg;
        // Stop the timer while we process the next letter
        llSetTimerEvent(0.0);
        string letter = llGetSubString(msg, 0, 0);
        // Pick the correct lookup list to use
        string lookup = LETTERS;
        if (gMode) lookup = NUMBERS;
        // Find out the index of the character, and play the appropriate animations
        integer pos = llSubStringIndex(lookup, letter);
        llMessageLinked(LINK_THIS, -1, llGetSubString(FLAG_POS, pos * 2, pos * 2 + 1), NULL_KEY);
        // What is being broadcast
        string broadcast = letter;
        if (letter == "n") {
                gMode = 1;
                broadcast = "(numbers)";
        }
        else if (letter == "l") {
                gMode = 0;
                broadcast = "(letters)";
        }
        else if (letter == " ") {
                broadcast = "(rest)";
        }
        llOwnerSay(broadcast);
        public_msg(broadcast);
        // Restart the timer
        llSetTimerEvent(gInterval);
        // Was that the last character? If not, strip off the letter and return the result
        if (msg != letter) return llGetSubString(msg, 1, -1);
        // otherwise return a blank string
        else return "";
}

// clean_msg
// This function cleans the original message into a nice legal message string.
// We do this at the start to avoid unexpected delays during semaphoring.
string clean_msg(string msg)
{
        msg = llToUpper(msg);
        integer f = 0;
        integer length = llStringLength(msg);
        string newMsg = "";
        string lastChar = "";
        string letter = "";
        integer mode = 0; // 0 = letters 1 = numbers
        do {
                letter = llGetSubString(msg, f, f);
                // Do not allow:
                // - Letters not in the legal character list
                // - Multiple spaces
                if (llSubStringIndex(LEGAL, letter) != -1 && !(letter == " " && (lastChar == " " || f == 0))) {
                        if (letter == lastChar) {
                                newMsg += " "; // Insert a break between identical letters
                        }
                        if (llSubStringIndex(LETTERS, letter) == -1 && mode == 0) {
                                // We have a number, when we are in letter mode, so change mode
                                newMsg += "n";
                                mode = 1;
                        }
                        else if (llSubStringIndex(NUMBERS, letter) == -1 && mode == 1) {
                                // Letter, when we are in number mode, change
                                newMsg += "l";
                                mode = 0;
                        }
                        newMsg += letter;
                        lastChar = letter;
                }
        } while (++f < length);
        // Chop off any spaces at the start and end
        if (llGetSubString(newMsg, -1, -1) == " ") {
                newMsg = llGetSubString(newMsg, 0, -2);
        }
        if (llGetSubString(newMsg, 0, 0) == " ") {
                newMsg = llGetSubString(newMsg, 1, -1);
        }
        return newMsg;
}

error()
{
        llSetTimerEvent(0.0);
        gMessage = "";
        gMessageQueue = "";
        llStartAnimation(ANIM_ERROR);
        public_msg("(error)");
        llMessageLinked(LINK_THIS, -2, "", NULL_KEY);
        llSleep(2.0);
        llStopAnimation(ANIM_ERROR);
        llOwnerSay("All messages cancelled.");
}

help()
{
        llGiveInventory(llGetOwner(), llGetInventoryName(INVENTORY_NOTECARD, 0));
}

//--------------------------------------------------------------------
// Main program

default
{
        on_rez(integer p)
        {
                llResetScript();
        }

        state_entry()
        {
                llSetLinkAlpha(LINK_INT_BUTTON, 1.0, ALL_SIDES);
                llSetLinkAlpha(LINK_ERROR, 1.0, ALL_SIDES);
                update_display();
                update_translate(gTranslating);
                llListen(CHANNEL_PUBLIC, "", NULL_KEY, "");
                // We need animation permission
                llRequestPermissions(llGetOwner(), PERMISSION_TRIGGER_ANIMATION);
        }

        run_time_permissions(integer perms)
        {
                if (perms & PERMISSION_TRIGGER_ANIMATION) {
                        local_msg("active");
                        llSetLinkTexture(LINK_STATUS_DISPLAY, TEXTURE_ACTIVE, ALL_SIDES);
                        llListen(CHANNEL_INPUT, "", llGetOwner(), "");
                        llOwnerSay("Enter a message on channel " + (string)CHANNEL_INPUT + " to say it in semaphore, e.g. '/9 hello world'. Say '/9 !help' for full instructions.");
                        preload();
                }
                else {
                        llOwnerSay("Unfortunately you need to grant animation permissions for me to work.");
                        state disabled;
                }
        }

        listen(integer c, string name, key id, string msg)
        {
                if (c == CHANNEL_INPUT) {
                        if (llGetSubString(msg, 0, 0) == "!") {
                                // This indicates a special voice command
                                if (msg == "!translate") {
                                        update_translate(!gTranslating);
                                }
                                else if (msg == "!help") {
                                        help();
                                }
                                else if (llGetSubString(msg, 0, 8) == "!interval") {
                                        float interval = (float)llGetSubString(msg, 10, -1);
                                        if (interval > 0.0 && interval < 10.0) {
                                                gInterval = interval;
                                                update_display();
                                        }
                                        else {
                                                llOwnerSay("That was not a suitable interval parameter - try one above 0 and below 10.");
                                        }
                                }
                                else {
                                        llOwnerSay("That looked like some sort of special command, but unfortunately it wasn't one that I understood.");
                                }
                        }
                        else if (gMessage != "") {
                                gMessageQueue += " " + msg;
                                llOwnerSay("Your new message will be added to the current one.");
                        }
                        else {
                                llOwnerSay("Thinking...");
                                gMessage = "wait"; // set this to stop it adding another message
                                gFinalMsg = clean_msg(msg);
                                llOwnerSay("Beginning!");
                                gMessage = process_next_char(gFinalMsg);
                        }
                }
                else if (c == CHANNEL_PUBLIC && gTranslating) {
                        integer pos = llSubStringIndex(msg, "|");
                        string name = llGetSubString(msg, 0, pos - 1);
                        msg = llGetSubString(msg, pos + 1, -1);
                        if (llStringLength(msg) == 1 || llGetSubString(msg, 0, 0) == "(") {
                                llOwnerSay(name + " sends the signal '" + msg + "'");
                        }
                        else {
                                llOwnerSay(name + " has sent the message '" + msg + "'");
                        }
                }
        }

        timer()
        {
                if (gMessage == "") {
                        llSetTimerEvent(0.0);
                        // Stop the current animations
                        llMessageLinked(LINK_THIS, -2, "", NULL_KEY);
                        public_msg(gFinalMsg);
                        gFinalMsg = "";
                        if (gMessageQueue == "") {
                                llOwnerSay("Finished!");
                        }
                        else {
                                llOwnerSay("Thinking...");
                                string msg = gMessageQueue;
                                gMessageQueue = "";
                                gMessage = "wait";
                                gFinalMsg = clean_msg(msg);
                                llOwnerSay("Beginning!");
                                gMessage = process_next_char(gFinalMsg);
                        }
                }
                else {
                        gMessage = process_next_char(gMessage);
                }
        }

        touch_start(integer n)
        {
                if (llDetectedKey(0) != llGetOwner()) return;
                llPlaySound(SOUND_CLICK, 1.0);
                integer link = llDetectedLinkNumber(0);
                if (link == LINK_INT_BUTTON) {
                        gInterval = gInterval * 2;
                        if (gInterval > 2.0) gInterval = 0.5;
                        update_display();
                }
                else if (link == LINK_ERROR) {
                        error();
                }
                else {
                        state disabled;
                }
        }

        state_exit()
        {
                llSetTimerEvent(0.0);
                llStopAnimation(ANIM_POSE);
        }
}

state disabled
{
        state_entry()
        {
                llMessageLinked(LINK_THIS, -2, "", NULL_KEY);
                gMessage = "";
                gMessageQueue = "";
                gMode = 0;
                local_msg("disabled");
                llSetLinkTexture(LINK_STATUS_DISPLAY, TEXTURE_DISABLED, ALL_SIDES);
                llMessageLinked(LINK_INT_DISPLAY, -3, "", NULL_KEY);
                llSetLinkAlpha(LINK_INT_BUTTON, 0.5, ALL_SIDES);
                llSetLinkAlpha(LINK_ERROR, 0.5, ALL_SIDES);
                llOwnerSay("Switched off - touch me again to start");
        }

        on_rez(integer p)
        {
                llOwnerSay("Currently disabled - touch me to start semaphore");
        }

        touch_start(integer n)
        {
                if (llDetectedKey(0) != llGetOwner()) return;
                llPlaySound(SOUND_CLICK, 1.0);
                state default;
        }

}