Smooth Swing Door 2.0

Written by Kitsune

From Xugu Madison:

// $Id: smooth swing door.lsl 25 2009-08-14 21:53:09Z jrn $
// Smooth swing door, with access control. Control starts at the default state, which
// attempts to load the configuration from the notecard "Configuration" in prim inventory.
// It then passes control along to read_whitelist, then read_blacklist,
// then read_managers, which read in the access control list. After the
// access control lists are loaded, control passes to the setup_door state
// which provides final validation of the door configuration before passing
// to the closed state.
//
// There are then 6 states the door moves between:
//
// closed - indicating the door is closed and unlocked
// open - indicating the door is oepn and unlocked
// swinging - the door is swinging from closed to open
// swinging - the door is swinging from open to closed
// locked_closed - locked in the closed position
// locked_open - locked in the open position ("latched")

// Constants

list CONFIGURATION_NOTECARDS = ["Configuration", "Whitelist", "Blacklist", "Managers"];

list NOTECARD_SEPARATORS = ["="];
list NOTECARD_SPACERS = [];

// These are just defaults, and should be changed from the notecard

list g_Whitelist;
list g_Managers;
list g_Blacklist;

integer g_AllowEveryone;
integer g_AllowGroup;

float g_AutoClose; // Seconds before the door auto-closes
float g_SwingTime; // Seconds for the door to swing from closed to open or back again
integer g_SwingDegrees; // Degrees
vector g_SwingVector;

integer g_ListenChannel; // >= 0 to activate listening

float g_SoundVolume; 
integer g_ChimeActive;
string g_ChimeSoundName;
string g_ClosingSoundName;
string g_LockedSoundName;
string g_OpeningSoundName;
string g_UnlockedSoundName;

// These hold state that needs global access

// 0-4, referring to configuration, whitelist, blacklist and managerlist
// loading.
integer g_ConfigurationStage;

rotation g_ClosedRot;
rotation g_OpenRot;
vector g_UpAxis;

integer g_IsClosed = TRUE;
integer g_IsLocked = FALSE;

integer g_NotecardLine;
key g_NotecardQuery;

// Returns whether the given avatar with UIID "detectedKey" and name
// "detectedName" has permission to open/close this door. "detectedGroup"
// indicates if the avatar has the same ACTIVE group as the group this
// door belongs to (TRUE or FALSE).
integer hasAccess(key detectedKey, string detectedName, integer detectedGroup)
{
    if (detectedKey == llGetOwner())
    {
        return TRUE;
    }
    
    if (llListFindList(g_Managers, [detectedName]) >= 0)
    {
        return TRUE;
    }
    
    if (g_IsLocked)
    {
        return FALSE;
    }

    if (llListFindList(g_Whitelist, [detectedName]) >= 0)
    {
        return TRUE;
    }
            
    if (llListFindList(g_Blacklist, [detectedName]) >= 0)
    {
        return FALSE;
    }
            
    if (g_AllowEveryone)
    {
        return TRUE;
    }
            
    if (g_AllowGroup &&
        detectedGroup)
    {
        return TRUE;
    }
    
    return FALSE;
}

// Parses the configuration as it's loaded from the default state.
// Lines starting with the '#' character are assumed to be comments.
// All other lines are broken into two parts around the '=' character
// and then evaluated as a key-value pair.
parseConfigurationLine(string data)
{
    list parts;
    string name;
    string rawValue;
    string value;
    
    if (data == "" ||
        llGetSubString(data, 0, 0) == "#")
    {
        // Comment, ignore
        return;
    }
    
    parts = llParseString2List(data, NOTECARD_SEPARATORS, NOTECARD_SPACERS);
    if (llGetListLength(parts) < 2)
    {
        // Ignore value
        return;
    }
    
    name = llStringTrim(llToLower(llList2String(parts, 0)), STRING_TRIM);
    rawValue = llList2String(parts, 1);
    value = llStringTrim(llToLower(rawValue), STRING_TRIM);
    
    if (name == "access")
    {
        if (value == "owner")
        {
            g_AllowEveryone = FALSE;
            g_AllowGroup = FALSE;
        }
        else if (value == "group")
        {
            g_AllowEveryone = FALSE;
            g_AllowGroup = TRUE;
        }
        else if (value == "everyone" || value == "all")
        {
            g_AllowEveryone = TRUE;
            g_AllowGroup = TRUE;
        } else {
            llOwnerSay("Unknown access type \""
                + value + "\"; valid values are \"owner\", \"group\" or \"everyone\".");
        }
    }
    else if (name == "auto_close")
    {
        g_AutoClose = (float)value;
    }
    else if (name == "chime_name")
    {
        g_ChimeSoundName = value;
    }
    else if (name == "chime_active") 
    {
        if (value == "false" ||
            value == "no")     {
            g_ChimeActive = FALSE;
        }
        else
        {
            g_ChimeActive = TRUE;
        }
    }
    else if (name == "closing_sound_name")
    {
        if (llGetInventoryType(rawValue) != INVENTORY_SOUND)
        {
            llOwnerSay("Closing sound \""
                + rawValue + "\" specified in configuration is either missing from prim inventory, or not a sound.");
        }
        else
        {
            g_ClosingSoundName = rawValue;
        }
    }
    else if (name == "listen_channel")
    {
        g_ListenChannel = (integer)value;
    }
    else if (name == "locked_sound_name")
    {
        if (llGetInventoryType(rawValue) !=  INVENTORY_SOUND)
        {
            llOwnerSay("Locked sound \""
                + rawValue + "\" specified in configuration is either missing from prim inventory, or not a sound.");
        }
        else
        {
            g_LockedSoundName = rawValue;
        }
    }
    else if (name == "opening_sound_name")
    {
        if (llGetInventoryType(rawValue) != INVENTORY_SOUND)
        {
            llOwnerSay("Opening sound \""
                + rawValue + "\" specified in configuration is either missing from prim inventory, or not a sound.");
        }
        else
        {
            g_OpeningSoundName = rawValue;
        }
    }
    else if (name == "sound_volume")
    {
        g_SoundVolume = (float)value;
    }
    else if (name == "swing_degrees")
    {
        g_SwingDegrees = (integer)value;
        if (g_SwingDegrees < -360 ||
            g_SwingDegrees > 360)
        {
            llOwnerSay("Swing degrees must be between -360 and 360 degrees, reverting to 90 degrees.");
            g_SwingDegrees = 90;
        }
    }
    else if (name == "swing_time")
    {
        g_SwingTime = (float)value;
        if (g_SwingTime < 0.1)
        {
            llOwnerSay("Swing time must be at least 0.1 seconds, reverting to default of 1.5 seconds.");
            g_SwingTime = 1.5;
        }
    }
    else if (name == "unlocked_sound_name")
    {
        if (llGetInventoryType(rawValue) !=  INVENTORY_SOUND)
        {
            llOwnerSay("Unlocked sound \""
                + rawValue + "\" specified in configuration is either missing from prim inventory, or not a sound.");
        }
        else
        {
            g_UnlockedSoundName = rawValue;
        }
    }
    else
    {
        llOwnerSay("Unknown parameter \""
            + name + "\"; valid names are; access, auto_close, listen_channel, chime_active, chime_name, closing_sound_name, locked_sound_name, opening_sound_name, sound_volume, swing_degrees, swing_time, time_to_close and unlocked_sound_name.");
    }    
    
    return;
}

// Wipes the configuration, cancels any configuration load in progress, and
// starts loading the configuration afresh. Intended to be called only from
// the load_configuration state. Returns TRUE if configuration load was started
// successfully, FALSE otherwise.
integer reloadConfiguration()
{
    // We clear all existing configuration data before attempting to load
    // the new one.
    g_Whitelist = [];
    g_Managers = [];
    g_Blacklist = [];

    g_AllowEveryone = FALSE;
    g_AllowGroup = FALSE;

    g_AutoClose = 3.0; // Seconds before the door auto-closes
    g_SwingTime = 1.0; // Seconds
    g_SwingDegrees = 90; // Degrees
    g_SwingVector;

    g_ListenChannel = -1; // >= 0 to activate listening

    g_SoundVolume = 0.8; 
    g_ChimeActive = TRUE;
    g_ChimeSoundName = "Door bell";
    g_ClosingSoundName = "Door closing";
    g_LockedSoundName = "Door locked";
    g_OpeningSoundName = "Door opening";
    g_UnlockedSoundName = "Door unlocked";
    
    g_NotecardLine = 0;
    
    integer notecardCount = llGetListLength(CONFIGURATION_NOTECARDS);
    
    for (g_ConfigurationStage = 0; g_ConfigurationStage < notecardCount; g_ConfigurationStage++)
    {
        string notecardName = llList2String(CONFIGURATION_NOTECARDS, g_ConfigurationStage);
        
        if (startReadingNotecard(notecardName))
        {
            return TRUE;
        }
    }
    
    // No notecards to load
    return FALSE;
}

// Verifies a notecard exists in prim inventory, and warns if the script cannot
// retrieve a UUID for it. Returns TRUE if the notecard exists, FALSE otherwise.
integer startReadingNotecard(string notecardName)
{
    // Check that the notecard exists, is a notecard, and actually has an
    // asset backing it.
    if (llGetInventoryType(notecardName) != INVENTORY_NOTECARD)
    {
        return FALSE;
    }
    
    if (llGetInventoryKey(notecardName) == NULL_KEY)
    {
        llOwnerSay("I can't retrieve the key for the notecard \""
            + notecardName + "\"; this could indicate it's not full perms, or that's it's empty. If it's empty, the asset server will error when I try reading it. I just want you to know this isn't my fault.");
    }
    g_NotecardQuery = llGetNotecardLine(notecardName, g_NotecardLine++);
    
    return TRUE;
}

// Handles chiming (text & audio notification) of being clicked while in
// the closed state.
triggerChime(string avatarName)
{
    if (g_ChimeActive)
    {
        llSay(0, avatarName + " is at the door.");
        if (g_SoundVolume > 0.0 &&
        g_ChimeSoundName != "")
        {
            llTriggerSound(g_ChimeSoundName, g_SoundVolume);
        }
    }
}

// Used to update the white/black lists on the fly from chat commands.
// Accepts commands "show", "add" or "remove", with the last two
// to be followed by an avatar name.
list updateList(key id, list modifyList, string message)
{
    list parts = llParseString2List(message, [" "], []);
    integer partsLength = llGetListLength(parts);
    
    if (partsLength == 1 ||
        llList2String(parts, 1) == "list" ||
        llList2String(parts, 1) == "show")
    {
        integer listIdx;
        integer listLength = llGetListLength(modifyList);
        string message;
        
        if (listLength == 0)
        {
            llInstantMessage(id, "List is empty.");
        }
        else
        {
            llInstantMessage(id, "Avatars: "
                + llList2CSV(modifyList));
        }
        
        return modifyList;
    }
    else if (llList2String(parts, 1) == "add")
    {
        if (partsLength < 3)
        {
            llInstantMessage(id, "You must specify an avatar to add to the list.");
        }
        else
        {
            string target = llDumpList2String(llList2List(parts, 2, partsLength - 1), " ");
            
            llInstantMessage(id, "Adding \""
                + target + "\" to list.");
        
            return llListInsertList(modifyList, [target], 0);
        }
    }
    else if (llList2String(parts, 1) == "remove")
    {
        if (partsLength < 3)
        {
            llInstantMessage(id, "You must specify an avatar to remove from the list.");
        }
        else
        {
            integer targetIdx;
            string target = llDumpList2String(llList2List(parts, 2, partsLength - 1), " ");
            
            targetIdx = llListFindList(modifyList, [target]);
            if (targetIdx < 0)
            {
                llInstantMessage(id, "The avatar \""
                    + target + "\" is not in this list.");
            }
            else
            {
                llInstantMessage(id, "Removing \""
                    + target + "\" from list.");
                return llDeleteSubList(modifyList, targetIdx, targetIdx);
            }
        }
    }
    else
    {
        llInstantMessage(id, "Unrecognised command \""
            + llList2String(parts, 1) + "\"; expected \"show\", \"add\" or \"remove\".");
    }
    
    return modifyList;
}

integer validateListen(integer channel, string name, key id)
{
    key owner = llGetOwner();
        
    if (channel != g_ListenChannel)
    {
        return FALSE;
    }
        
    if (id != owner &&
        llGetOwnerKey(id) != owner &&
        // The llGetOwnerKey() is the only way I could find of checking
        // we're hearing an avatar.
        (llGetOwnerKey(id) != id ||
        llListFindList(g_Managers, [name]) >= 0))
    {
        return FALSE;
    }
    
    return TRUE;
}

// Attempts to read in the configuration, whitelist, blacklist and manager list,
// then proceeds to the closed state.
default
{
    changed(integer change)
    {
        if (change & CHANGED_INVENTORY)
        {
            if (!reloadConfiguration())
            {
                state ready;
            }
        }
    }

    dataserver(key queryID, string data)
    {
        if (queryID != g_NotecardQuery)
        {
            return;
        }
        
        if (data == EOF)
        {
            // Go back to the start of the notecard, because we're about to load
            // a new notecard.
            g_NotecardLine = 0;
            
            // And automatically proceed to the next configuration stage.
            g_ConfigurationStage++;
            
            // If we're at the end of a notecard, look for a further notecard to read, or
            // proceed to the door setup state if we've read all the notecards in.
            integer notecardCount = llGetListLength(CONFIGURATION_NOTECARDS);
    
            for (; g_ConfigurationStage < notecardCount; g_ConfigurationStage++)
            {
                string notecardName = llList2String(CONFIGURATION_NOTECARDS, g_ConfigurationStage);
        
                if (startReadingNotecard(notecardName))
                {
                    return;
                }
            }
        
            llSetText("Completing setup...", <1.0, 1.0, 1.0>, 1.0);
            if (g_SoundVolume > 0.0) {
                if (llGetInventoryType(g_ChimeSoundName) != INVENTORY_SOUND)
                {
                    g_ChimeSoundName = "";
                }
                if (llGetInventoryType(g_ClosingSoundName) != INVENTORY_SOUND)
                {
                    g_ClosingSoundName = "";
                }
                if (llGetInventoryType(g_LockedSoundName) != INVENTORY_SOUND)
                {
                    g_LockedSoundName = "";
                }
                if (llGetInventoryType(g_OpeningSoundName) != INVENTORY_SOUND)
                {
                    g_OpeningSoundName = "";
                }
                if (llGetInventoryType(g_UnlockedSoundName) != INVENTORY_SOUND)
                {
                    g_UnlockedSoundName = "";
                }
            }
        
            state ready;
        }
       
        if (g_ConfigurationStage == 0)
        {
            parseConfigurationLine(data);
        }
        else if (g_ConfigurationStage == 1)
        {
            g_Whitelist += data;
        }
        else if (g_ConfigurationStage == 2)
        {
            g_Blacklist += data;
        }
        else if (g_ConfigurationStage == 3)
        {
            g_Managers += data;
        }
        
        g_NotecardQuery = llGetNotecardLine(llList2String(CONFIGURATION_NOTECARDS, g_ConfigurationStage), g_NotecardLine++);
    }

    state_entry()
    {
        llSetText("Loading configuration...", <1.0, 1.0, 1.0>, 1.0);
        if (!reloadConfiguration())
        {
            state ready;
        }
    }
    
    state_exit()
    {
        llSetText("", <1.0, 1.0, 1.0>, 0.0);
        g_SwingVector = <0, 0, g_SwingDegrees * DEG_TO_RAD>;
    }
}

state ready
{
    changed(integer change)
    {
        if (change & CHANGED_INVENTORY)
        {
            state default;
        }
    }
    
    listen( integer channel, string name, key id, string message)
    {
        if (!validateListen(channel, name, id))
        {
            return;
        }
        
        message = llStringTrim(llToLower(message), STRING_TRIM);
        
        if (message == "lock")
        {
            // Cancel any auto-close.
            llSetTimerEvent(0.0);
            
            if (g_SoundVolume > 0.0 &&
                g_LockedSoundName != "")
            {
                llTriggerSound(g_LockedSoundName, g_SoundVolume);
            }
            llSay(0, "Locked");
            g_IsLocked = TRUE;
        }
        else if (message == "toggle")
        {
            state swinging;
        }
        else if (message == "unlock")
        {
            if (!g_IsClosed &&
                g_AutoClose > 0.0)
            {
                llSetTimerEvent(g_AutoClose);
            }
        
            if (g_SoundVolume > 0.0 &&
                g_UnlockedSoundName != "")
            {
                llTriggerSound(g_UnlockedSoundName, g_SoundVolume);
            }
            llSay(0, "Unlocked");
            g_IsLocked = FALSE;
        }
        else if (llSubStringIndex(message, "white") == 0)
        {
            g_Whitelist = updateList(id, g_Whitelist, message);
        }
        else if (llSubStringIndex(message, "black") == 0)
        {
            g_Blacklist = updateList(id, g_Blacklist, message);
        }
        else
        {
            llInstantMessage(id, "Unrecognised command \""
                + message + "\".");
        }
    }
    
    state_entry()
    {
        if (g_ListenChannel >= 0)
        {
            llListen(g_ListenChannel, "", NULL_KEY, "");
        }

        if (!g_IsClosed &&
            g_AutoClose > 0.0)
        {
            llSetTimerEvent(g_AutoClose);
        }
    }
    
    state_exit()
    {
        llSetTimerEvent(0.0);
    }
    
    timer()
    {
        state swinging;
    }
    
    touch_end(integer numTouching)
    {
        integer i;
        
        for (i = 0; i < numTouching; i++)
        {
            key detectedKey = llDetectedKey(i);
            string detectedName = llDetectedName(i);
            
            if (g_IsClosed)
            {
                triggerChime(detectedName);
            }
            
            if (hasAccess(detectedKey, detectedName, llDetectedGroup(i)))
            {
                if (g_IsLocked)
                {
                    llSay(0, "Lock overridden by priority access.");
                }
                
                state swinging;
            }
            else if (g_IsLocked)
            {
                llInstantMessage(detectedKey, "Sorry, the door is locked.");
            }
            else
            {
                llInstantMessage(detectedKey, "Sorry, you do not have permission to open this door.");
            }
        }
    }
}

state swinging
{
    state_entry()
    {
        rotation delta = llEuler2Rot(g_SwingVector);

        if (g_IsClosed)
        {
            if (g_SoundVolume > 0.0 &&
                g_OpeningSoundName != "")
            {
                llTriggerSound(g_OpeningSoundName, g_SoundVolume);
            }
        
            g_ClosedRot = llGetLocalRot();
            g_OpenRot = delta * g_ClosedRot ;
            g_UpAxis = llRot2Up(g_ClosedRot);
            
            llSetTimerEvent(g_SwingTime);
            llTargetOmega(g_UpAxis, g_SwingVector.z / g_SwingTime, 1.0);
        }
        else
        {
            if (g_SoundVolume > 0.0 &&
                g_ClosingSoundName != "")
            {
                llTriggerSound(g_ClosingSoundName, g_SoundVolume);
            }
            llSetTimerEvent(g_SwingTime);
            llTargetOmega(g_UpAxis, -(g_SwingVector.z / g_SwingTime), 1.0);
        }
    }
    
    state_exit()
    {
        llSetTimerEvent(0.0);
    }
    
    timer()
    {
        llTargetOmega(g_UpAxis, 0, 0);
        if (g_IsClosed)
        {
            llSetLocalRot(g_OpenRot);
        }
        else
        {
            llSetLocalRot(g_ClosedRot);
        }
        
        g_IsClosed = !g_IsClosed;
        
        state ready;
    }
}