From 9c9a333860debf85b403a79bd6ee93b4f6bc5ded Mon Sep 17 00:00:00 2001 From: David Walter Seikel Date: Sun, 2 Mar 2014 22:34:54 +1000 Subject: Initial import of everything. --- COPYING-PLAIN | 42 ++ LICENSE | 27 - NPC_menu.lsl | 481 +++++++++++++++ NPC_tool.lsl | 1291 ++++++++++++++++++++++++++++++++++++++++ NPC_tool_help.txt | 171 ++++++ README.md | 5 +- locator_beacon_script.lsl | 130 ++++ onefang's_utilities.lsl | 1189 ++++++++++++++++++++++++++++++++++++ onefang's_utilities_manual.txt | 228 +++++++ 9 files changed, 3533 insertions(+), 31 deletions(-) create mode 100644 COPYING-PLAIN delete mode 100644 LICENSE create mode 100644 NPC_menu.lsl create mode 100644 NPC_tool.lsl create mode 100644 NPC_tool_help.txt create mode 100644 locator_beacon_script.lsl create mode 100644 onefang's_utilities.lsl create mode 100644 onefang's_utilities_manual.txt diff --git a/COPYING-PLAIN b/COPYING-PLAIN new file mode 100644 index 0000000..dc18261 --- /dev/null +++ b/COPYING-PLAIN @@ -0,0 +1,42 @@ +Plain English Copyright Notice + +This file is not intended to be the actual License. The reason this +file exists is that we here are programmers and engineers. We aren't +lawyers. We provide licenses that we THINK say the right things, but we +have our own intentions at heart. This is a plain-english explanation +of what those intentions are, and if you follow them you will be within +the "spirit" of the license. + +The intent is for us to enjoy writing software that is useful to us (the +AUTHORS) and allow others to use it freely and also benefit from the +work we put into making it. We don't want to restrict others using it. +They should not *HAVE* to make the source code of the applications they +write that simply link to these libraries (be that statically or +dynamically), or for them to be limited as to what license they choose +to use (be it open, closed or anything else). But we would like to know +you are using these libraries. We simply would like to know that it has +been useful to someone. This is why we ask for acknowledgement of some +sort. + +You can do what you want with the source of this software - it doesn't +matter. We still have it here for ourselves and it is open and free to +use and download and play with. It can't be taken away. We don't +really mind what you do with the source to your software. We would +simply like to know that you are using it - especially if it makes it to +a commerical product. If you simply contact all the AUTHORS (see below) +telling us, and then make sure you include a paragraph or page in the +manual for the product with the copyright notice and state that you used +this software, we will be very happy. If you want to contribute back +modifications and fixes you may have made we will welcome those too with +open arms (generally). If you want help with changes needed, ports +needed or features to be added, arrangements can be easily made with +some dialogue. + +This is a Second Life script, "simply link to these libraries" means +including the script in an object and making the script no-modify, or +otherwise rendering the source code unreadable. Any use of this script +that makes the source code readable must include the License unmodified +at the top of the source code. + +David Seikel (Second Life user onefang Rejected). + diff --git a/LICENSE b/LICENSE deleted file mode 100644 index b5dfce3..0000000 --- a/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2014, David Seikel -All rights reserved. - -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 the {organization} 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER OR 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. diff --git a/NPC_menu.lsl b/NPC_menu.lsl new file mode 100644 index 0000000..0b67c55 --- /dev/null +++ b/NPC_menu.lsl @@ -0,0 +1,481 @@ +// Onefang's NPC menu version 1.0. +// Requires NPC tool, and onefang's utilities scripts. + +// Copyright (C) 2013 David Seikel (onefang rejected). +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies of the Software and its Copyright notices. In addition publicly +// documented acknowledgment must be given that this software has been used if no +// source code of this software is made available publicly. This includes +// acknowledgments in either Copyright notices, Manuals, Publicity and Marketing +// documents or any documentation provided with any product containing this +// software. This License does not apply to any software that links to the +// libraries provided by this software (statically or dynamically), but only to +// the software provided. +// +// Please see the COPYING-PLAIN for a plain-english explanation of this notice +// and it's intent. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +list chatters = []; // npc key, relay channel. +integer CHAT_KEY = 0; +integer CHAT_CHANNEL = 1; +integer CHAT_STRIDE = 2; + +list sensorRequests = []; // name to search for, type of search, range, command, user key, npc key, title for menu, agents flag, every flag +integer sensorInFlight = FALSE; + +// NPC commands. +string NPC_ANIMATE = "animate"; // NPC name, animation name +string NPC_ATTENTION= "attention"; // NPC name. +string NPC_CHANGE = "change"; // NPC name, NPC (notecard) name. +string NPC_CLONE = "clone"; // Avatar's name. +string NPC_COME = "come"; // NPC name. +string NPC_COMPLETE = "complete"; // NPC name. +string NPC_CREATE = "create"; // NPC (notecard) name, (optional) position vector. +string NPC_DELETE = "delete"; // NPC name. +string NPC_DISMISSED= "dismissed"; // NPC name. +string NPC_FOLLOW = "follow"; // NPC name, (optional) distance (float or vector). +string NPC_FLY = "fly"; // NPC name, position vector or object/agent name/key. +string NPC_GO = "go"; // NPC name, position vector or object/agent name/key. +string NPC_LAND = "land"; // NPC name, position vector or object/agent name/key. +string NPC_LINK = "link"; // Link number, number, string, (optional) key +string NPC_LISTEN = "listen"; // new command channel +string NPC_LOCATE = "locate"; // NPC name +string NPC_NUKE = "nuke"; // No arguments. +string NPC_ROTATE = "rotate"; // NPC name, Z rotation in degrees. +string NPC_SAY = "say"; // NPC name, thing to say, or relay channel. +string NPC_SCRIPT = "script"; // Script notecard name. +string NPC_SHOUT = "shout"; // NPC name, thing to shout, or relay channel. +string NPC_SIT = "sit"; // NPC name, object to sit on. +string NPC_SLEEP = "sleep"; // Seconds. +string NPC_STALK = "stalk"; // NPC name, avatar name, (optional) distance (float or vector). +string NPC_STAND = "stand"; // NPC name. +string NPC_STOP = "stop"; // NPC name. +string NPC_STOPANIM = "stopanim"; // NPC name, animation name +string NPC_TOUCH = "touch"; // NPC name, object to touch. +string NPC_WHISPER = "whisper"; // NPC name, thing to whisper, or relay channel. + +string NPC_MAIN = "Main"; // TODO - does not really matter what this is I think, so use it as the registered menu button. +string NPC_PICK = "Pick"; +string NPC_NPC = "NPC"; +string NPC_RELAY = "Relay"; + +string NPC_NPC_EXT = ".avatar"; +string NPC_SCRIPT_EXT = ".npc"; +string NPC_BACKUP_CARD = "Restore"; +string NPC_RECORD_CARD = "Recorded"; + +integer NPC_RECORD = -100; +integer NPC_RECORD_STOP = -101; +integer NPC_NEW_NPC = -102; +integer NPC_ADD_CHATTER = -103; +integer NPC_DEL_CHATTER = -104; + + +// Stuff for onefangs common utilities. +key NPCscriptKey; +key scriptKey; + +string UTILITIES_SCRIPT_NAME = "onefang's utilities"; +string LIST_SEP = "$!#"; // Used to seperate lists when sending them as strings. +integer UTILITIES_RESET = -1; +integer UTILITIES_RESET_DONE = -2; +integer UTILITIES_READ = -3; +integer UTILITIES_READ_DONE = -4; +integer UTILITIES_SUBSTITUTE = -5; +integer UTILITIES_SUBSTITUTE_DONE = -6; +integer UTILITIES_NEXT_WORD = -7; +integer UTILITIES_NEXT_WORD_DONE = -8; +integer UTILITIES_PAYMENT_MADE = -9; +integer UTILITIES_PAYMENT_MADE_DONE = -10; +integer UTILITIES_MENU = -11; +integer UTILITIES_MENU_DONE = -12; +integer UTILITIES_WARP = -13; +integer UTILITIES_WARP_DONE = -14; +integer UTILITIES_AVATAR_KEY = -15; +integer UTILITIES_AVATAR_KEY_DONE = -16; +integer UTILITIES_CHAT = -17; +integer UTILITIES_CHAT_DONE = -18; +integer UTILITIES_CHAT_FAKE = -19; +integer UTILITIES_CHAT_FAKE_DONE = -20; + + +showMenu(key id, string type, string title, list buttons, string extra) +{ + llMessageLinked(LINK_SET, UTILITIES_MENU, llDumpList2String([id, type, title] + buttons, LIST_SEP), (key) scriptKey + "|" + extra); +} + +NPCmenu(key id, key npc) +{ + string name = llKey2Name(npc); + + if ((0 == osIsUUID(npc)) || (npc == NULL_KEY) || ("" == name)) + { + showMenu(id, (string) INVENTORY_NONE, "Main NPC menu.", + [ + "clone avatar..", "create NPC..", "run script..", + "nearby NPCs..", "local NPCs..", "NPCs in sim..", + "backup NPCs", "nuke NPCs", "restore NPCs", + "start recording", "stop recording" + ], NPC_MAIN); + } + else + { + integer found = llListFindList(chatters, [npc]); + string chat = ""; + + if (0 <= found) + chat = "\nUse /" + llList2String(chatters, found + CHAT_CHANNEL) + " to relay chat to " + llKey2Name(npc); + showMenu(id, (string) INVENTORY_NONE, "Play with " + llKey2Name(npc) + " :" + chat, + [ + "change..", "chat relay..", "come here", + "go to..", "fly to..", "land at..", + "follow me", "stalk them..", "stop moving", + "sit..", NPC_STAND, + NPC_LOCATE, "take controls", "touch..", + "start animation..", "stop animation..", + NPC_DELETE + ], NPC_NPC + "|" + npc); + } +} + +startSensor(key user, key npc, string name, integer type, float range, + string command, string title, integer agents, integer everything) +{ + if ((AGENT == type) && (range > 9999.9999)) // Assumes we are only looking for NPCs. + { + integer i; + list menu = []; + list avatars = osGetAvatarList(); // Strided list, UUID, position, name. + integer length = llGetListLength(avatars); + + for (i = 0; i < length; i++) + { + key this = (key) llList2String(avatars, i * 3); + + if (osIsNpc(this)) + menu += [llKey2Name(this) + "|" + (string) this]; + } + if (llGetListLength(menu) > 0) + showMenu(user, (string) INVENTORY_NONE, "Choose NPC :", menu, NPC_PICK); + } + else + { + sensorRequests += [name, type, range, command, user, npc, title, agents, everything]; + nextSensor(); + } +} + +nextSensor() +{ + if (sensorInFlight) + return; + if (0 < llGetListLength(sensorRequests)) + { + string name = llList2String(sensorRequests, 0); + integer type = llList2Integer(sensorRequests, 1); + float range = llList2Float(sensorRequests, 2); + string command = llList2String(sensorRequests, 3); + key user = llList2String(sensorRequests, 4); + key npc = llList2String(sensorRequests, 5); + string title = llList2String(sensorRequests, 6); + integer agents = llList2Integer(sensorRequests, 7); + integer every = llList2Integer(sensorRequests, 8); + + sensorInFlight = TRUE; + llSensor(name, "", type, range, TWO_PI); + } +} + +sendCommand(key user, string command) +{ + // Work around the other script getting reset later, with a fresh key + NPCscriptKey = llGetInventoryKey("NPC tool"); + llMessageLinked(LINK_SET, UTILITIES_CHAT_FAKE, llDumpList2String([0, llKey2Name(user), user, command], LIST_SEP), NPCscriptKey); +} + +init() +{ + scriptKey = llGetInventoryKey(llGetScriptName()); + // Register our interest in touch menus. + llMessageLinked(LINK_SET, UTILITIES_MENU, llDumpList2String([NULL_KEY, INVENTORY_NONE, "NPC tool|" + llGetScriptName()], LIST_SEP), (key) scriptKey); +} + + +default +{ + state_entry() + { + init(); + } + + on_rez(integer param) + { + init(); + } + + attach(key attached) + { + init(); + } + + link_message(integer sender_num, integer num, string value, key id) + { + list keys = llParseStringKeepNulls((string) id, ["|"], []); + string extra = llList2String(keys, 1); + + id = (key) llList2String(keys, 0); + // Work around the other script getting reset later, with a fresh key + NPCscriptKey = llGetInventoryKey("NPC tool"); +//llSay(0, "id = " + (string) id + " extra = " + extra + " VALUE " + value); + if (UTILITIES_RESET_DONE == num) + init(); + else if ((NPC_NEW_NPC == num) && (NPCscriptKey == id)) + { + list args = llParseString2List(value, ["|"], []); + + NPCmenu(llList2String(args, 0), llList2String(args, 1)); + } + else if ((NPC_ADD_CHATTER == num) && (NPCscriptKey == id)) + { + list args = llParseString2List(value, ["|"], []); + key npc = llList2String(args, 0); + integer channel = llList2Integer(args, 1); + integer found = llListFindList(chatters, [npc]); + + if (0 <= found) + chatters = llDeleteSubList(chatters, found, found + CHAT_STRIDE - 1); + chatters += [npc, channel]; + } + else if ((NPC_DEL_CHATTER == num) && (NPCscriptKey == id)) + { + integer found = llListFindList(chatters, [value]); + + if (0 <= found) + chatters = llDeleteSubList(chatters, found, found + CHAT_STRIDE - 1); + } + else if ((UTILITIES_CHAT_DONE == num) && (NPCscriptKey == id)) + { + // incoming channel | incoming name | incoming key | incoming message | prefix | command | list of arguments | rest of message + list result = llParseStringKeepNulls(value, [LIST_SEP], []); + //integer inchannel = llList2Integer(result, 0); + //string inName = llList2String (result, 1); + key user = llList2Key (result, 2); + //string inMessage = llList2String (result, 3); + //string prefix = llList2String (result, 4); + string command = llList2String (result, 5); + list arguments = llList2List (result, 6, -1); // Includes "rest of message" as the last one. + + if (NPC_NUKE == command) + { + chatters = []; + sensorRequests = []; + sensorInFlight = FALSE; + } + } + else if ((UTILITIES_MENU_DONE == num) && (scriptKey == id)) // Big menu button pushed + { + list input = llParseStringKeepNulls(value, [LIST_SEP], []); + key user = (key) llList2String(input, 0); + string selection = llList2String(input, 1); + list parts = llParseString2List(selection, ["|"], []); + key uuid = (key) llList2String(parts, 1); + key npc = (key) llList2String(keys, 2); + list details = llGetObjectDetails(uuid, [OBJECT_POS]); + string menu = extra; +//llSay(0, extra + " MENU " + value + " KEYS " + llDumpList2String(keys, " ")); + + // See if it was our top level menu requested via touch registration. + if ("" == selection) + { + NPCmenu(user, NULL_KEY); + return; + } + + // Figure out what the user picked. + if ((NPC_MAIN == extra) || (NPC_NPC == extra)) + menu = selection; + // Make sure main menu items don't return to an NPC menu, by setting npc to null. + if (NPC_MAIN == extra) + npc = NULL_KEY; + + // Check if the NPC still exists. + if (NPC_NPC == extra) + { + list npcDetails = llGetObjectDetails(npc, [OBJECT_POS]); + + if (llGetListLength(npcDetails) == 0) + { + // Bail out if the NPC went AWOL. + npc = NULL_KEY; + selection = "Exit"; + } + } + + if ("Exit" == selection) + { + if (NPC_NPC == extra) npc = NULL_KEY; + if (NPC_MAIN == extra) return; + } + // Commands. + else if (NPC_ANIMATE == menu) sendCommand(user, NPC_ANIMATE + " " + npc + " " + selection); + else if (NPC_CHANGE == menu) sendCommand(user, NPC_CHANGE + " " + npc + " " + selection); + else if (NPC_CLONE == menu) sendCommand(user, NPC_CLONE + " " + uuid); + else if (NPC_FLY == menu) sendCommand(user, NPC_FLY + " " + npc + " " + llList2String(details, 0)); + else if (NPC_GO == menu) sendCommand(user, NPC_GO + " " + npc + " " + llList2String(details, 0)); + else if (NPC_LAND == menu) sendCommand(user, NPC_LAND + " " + npc + " " + llList2String(details, 0)); + else if (NPC_LOCATE == menu) sendCommand(user, NPC_LOCATE + " " + npc); + else if (NPC_RELAY == menu) sendCommand(user, NPC_SAY + " " + npc + " " + selection); + else if (NPC_SCRIPT == menu) sendCommand(user, NPC_SCRIPT + " " + selection); + else if (NPC_SIT == menu) sendCommand(user, NPC_SIT + " " + npc + " " + uuid); + else if (NPC_STALK == menu) sendCommand(user, NPC_STALK + " " + npc + " " + uuid); + else if (NPC_STAND == menu) sendCommand(user, NPC_STAND + " " + npc); + else if (NPC_STOPANIM == menu) sendCommand(user, NPC_STOPANIM + " " + npc + " " + selection); + else if (NPC_TOUCH == menu) sendCommand(user, NPC_TOUCH + " " + npc + " " + uuid); + else if ("come here" == menu) sendCommand(user, NPC_COME + " " + npc); + else if ("follow me" == menu) sendCommand(user, NPC_FOLLOW + " " + npc); + else if ("nuke NPCs" == menu) sendCommand(user, NPC_NUKE); + else if ("restore NPCs" == menu) sendCommand(user, NPC_SCRIPT + " " + NPC_BACKUP_CARD + NPC_SCRIPT_EXT); + else if ("stop moving" == menu) sendCommand(user, NPC_STOP + " " + npc); + + // Menus. + else if ("change.." == menu) showMenu(user, ((string) INVENTORY_NOTECARD) + "|.+\\" + NPC_NPC_EXT, + "Choose an NPC to change to :", [], NPC_CHANGE + "|" + npc); + else if ("chat relay.." == menu) showMenu(user, (string) INVENTORY_NONE, + "Choose a channel to relay chat from :", ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"], NPC_RELAY + "|" + npc); + else if ("create NPC.." == menu) showMenu(user, ((string) INVENTORY_NOTECARD) + "|.+\\" + NPC_NPC_EXT, + "Choose an NPC to create :", [], NPC_CREATE); + else if ("run script.." == menu) showMenu(user, ((string) INVENTORY_NOTECARD) + "|.+\\" + NPC_SCRIPT_EXT, + "Choose a script to run :", [], NPC_SCRIPT); + else if ("start animation.." == menu) showMenu(user, (string) INVENTORY_ANIMATION, + "Choose an animation to start :", [], NPC_ANIMATE + "|" + npc); + else if ("stop animation.." == menu) showMenu(user, (string) INVENTORY_ANIMATION, + "Choose an animation to stop :", [], NPC_STOPANIM + "|" + npc); + + // Sensor menus. + else if ("clone avatar.." == menu) startSensor(user, "", "", AGENT, 256.0, NPC_CLONE, "Choose person to clone :", TRUE, TRUE); + else if ("fly to.." == menu) startSensor(user, npc, "", ACTIVE | PASSIVE, 1024.0, NPC_FLY, "Choose thing to fly to :", FALSE, TRUE); + else if ("go to.." == menu) startSensor(user, npc, "", ACTIVE | PASSIVE, 1024.0, NPC_GO, "Choose thing to go to :", FALSE, TRUE); + else if ("land at.." == menu) startSensor(user, npc, "", ACTIVE | PASSIVE, 1024.0, NPC_LAND, "Choose thing to land at :", FALSE, TRUE); + else if ("local NPCs.." == menu) startSensor(user, "", "", AGENT, 256.0, NPC_PICK, "Choose NPC :", FALSE, FALSE); + else if ("nearby NPCs.." == menu) startSensor(user, "", "", AGENT, 20.0, NPC_PICK, "Choose NPC :", FALSE, FALSE); + else if ("NPCs in sim.." == menu) startSensor(user, "", "", AGENT, 16384.0, NPC_PICK, "Choose NPC :", FALSE, FALSE); + else if ("sit.." == menu) startSensor(user, npc, "", ACTIVE | SCRIPTED, 1024.0, NPC_SIT, "Choose thing to sit on :", FALSE, TRUE); + else if ("stalk them.." == menu) startSensor(user, npc, "", AGENT, 1024.0, NPC_STALK, "Choose person to stalk :", TRUE, TRUE); + else if ("touch.." == menu) startSensor(user, npc, "", ACTIVE | SCRIPTED, 1024.0, NPC_TOUCH, "Choose thing to touch :", FALSE, TRUE); + + // Misc. + else if (NPC_PICK == menu) npc = uuid; + else if ("start recording" == menu) llMessageLinked(LINK_SET, NPC_RECORD, "", NPCscriptKey); + else if ("stop recording" == menu) llMessageLinked(LINK_SET, NPC_RECORD_STOP, "", NPCscriptKey); + else if (NPC_CREATE == menu) + { + sendCommand(user, NPC_CREATE + " " + selection); + // Avoid the NPCmenu() below. An odd one out, coz the NPC wont exist yet, but we want their menu when they do exist. + return; + } + else if (NPC_DELETE == menu) + { + sendCommand(user, NPC_DELETE + " " + npc); + // An odd one out, the NPC wont exist, so return to the main menu. + npc = NULL_KEY; + } + else if ("backup NPCs" == menu) + { + list avatars = osGetAvatarList(); // Strided list, UUID, position, name. + list delete = []; + list backup = []; + integer length = llGetListLength(avatars); + integer i; + + llRemoveInventory(NPC_BACKUP_CARD + NPC_SCRIPT_EXT); + for (i = 0; i < length; i++) + { + key this = (key) llList2String(avatars, i * 3); + + if (osIsNpc(this)) + { + string aName = llKey2Name(this); + + osAgentSaveAppearance(this, aName + NPC_NPC_EXT); + delete += [NPC_DELETE + " " + aName]; + backup += [NPC_CREATE + " " + aName + " " + llList2String(avatars, (i * 3) + 1)]; + } + } + osMakeNotecard(NPC_BACKUP_CARD + NPC_SCRIPT_EXT, + ["script " + NPC_BACKUP_CARD + ".before" + NPC_SCRIPT_EXT] + + delete + backup + + ["script " + NPC_BACKUP_CARD + ".after" + NPC_SCRIPT_EXT]); + } + + // If the menu name ends in "..", then it's expected that we are waiting on another menu, so don't show one now. + if (0 == osRegexIsMatch(menu, ".+\\.\\.$")) + NPCmenu(user, npc); + } // End of menu block. + } + + no_sensor() + { + sensorInFlight = FALSE; + if (llGetListLength(sensorRequests)) + { + sensorRequests = llDeleteSubList(sensorRequests, 0, 8); + nextSensor(); + } + } + + sensor(integer numberDetected) + { + sensorInFlight = FALSE; + if (llGetListLength(sensorRequests)) + { + string name = llList2String(sensorRequests, 0); + integer type = llList2Integer(sensorRequests, 1); + float range = llList2Float(sensorRequests, 2); + string command = llList2String(sensorRequests, 3); + key user = llList2String(sensorRequests, 4); + key npc = llList2String(sensorRequests, 5); + string title = llList2String(sensorRequests, 6); + integer agents = llList2Integer(sensorRequests, 7); + integer every = llList2Integer(sensorRequests, 8); + + sensorRequests = llDeleteSubList(sensorRequests, 0, 8); + if ("" == title) + sendCommand(user, command + " " + npc + " " + llDetectedKey(0)); + else + { + integer i; + list menu = []; + + if (agents) + menu += ["you|" + (string) user]; + for (i = 0; i < numberDetected; i++) + { + key this = llDetectedKey(i); + + if (!(agents && (this == user))) + if (every || osIsNpc(this)) + menu += [llDetectedName(i) + "|" + (string) this]; + } + if (llGetListLength(menu) > 0) + showMenu(user, (string) INVENTORY_NONE, title, menu, command + "|" + npc); + } + nextSensor(); + } + } + +} \ No newline at end of file diff --git a/NPC_tool.lsl b/NPC_tool.lsl new file mode 100644 index 0000000..29dfd67 --- /dev/null +++ b/NPC_tool.lsl @@ -0,0 +1,1291 @@ +// Onefang's general purpose NPC tool version 1.0. +// Requires onefang's utilities script. + +// Copyright (C) 2013 David Seikel (onefang rejected). +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies of the Software and its Copyright notices. In addition publicly +// documented acknowledgment must be given that this software has been used if no +// source code of this software is made available publicly. This includes +// acknowledgments in either Copyright notices, Manuals, Publicity and Marketing +// documents or any documentation provided with any product containing this +// software. This License does not apply to any software that links to the +// libraries provided by this software (statically or dynamically), but only to +// the software provided. +// +// Please see the COPYING-PLAIN for a plain-english explanation of this notice +// and it's intent. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +// This is bad listening for commands on channel 0. +integer commandChannel = 0; + +list attention = []; // npc key or script number, listen handle, list of attentive NPCs +integer ATTENT_KEY = 0; +integer ATTENT_HANDLE = 1; +integer ATTENT_NPCS = 2; +integer ATTENT_STRIDE = 3; + +list chatters = []; // npc key, chat type, listen handle. In channel order. +integer CHAT_KEY = 0; +integer CHAT_TYPE = 1; +integer CHAT_HANDLE = 2; +integer CHAT_STRIDE = 3; + +list followers = []; // npc key, stalkee key, distance vector +integer FOLLOW_KEY = 0; +integer FOLLOW_STALK = 1; +integer FOLLOW_DIST = 2; +integer FOLLOW_STRIDE = 3; + +list movers = []; // npc key, destination position, least distance, timestamp +integer MOVE_KEY = 0; +integer MOVE_DEST = 1; +integer MOVE_LEAST = 2; +integer MOVE_TIME = 3; +integer MOVE_STRIDE = 4; + +float scriptTick = 0.5; +float lastTick = -1.0; +list scripts = []; // user key, script card name, flags, command list LIST_SEP separated +integer SCRIPTS_KEY = 0; +integer SCRIPTS_NAME = 1; +integer SCRIPTS_FLAGS = 2; +integer SCRIPTS_COMMANDS = 3; +integer SCRIPTS_STRIDE = 4; + +// Script flags. +integer SCRIPT_READING = 1; +integer SCRIPT_ATTENTION = 2; + +list sensorRequests = []; // name to search for, type of search, command, user key, npc key +integer sensorInFlight = FALSE; + +integer recording = FALSE; +list record = []; // recorded commands + +float restartTimer = -1.0; + +// NPC commands. +string NPC_ANIMATE = "animate"; // NPC name, animation name +string NPC_ATTENTION= "attention"; // NPC name. +string NPC_CHANGE = "change"; // NPC name, NPC (notecard) name. +string NPC_CLONE = "clone"; // Avatar's name. +string NPC_COME = "come"; // NPC name. +string NPC_COMPLETE = "complete"; // NPC name. +string NPC_CREATE = "create"; // NPC (notecard) name, (optional) position vector. +string NPC_DELETE = "delete"; // NPC name. +string NPC_DISMISSED= "dismissed"; // NPC name. +string NPC_FOLLOW = "follow"; // NPC name, (optional) distance (float or vector). +string NPC_FLY = "fly"; // NPC name, position vector or object/agent name/key. +string NPC_GO = "go"; // NPC name, position vector or object/agent name/key. +string NPC_LAND = "land"; // NPC name, position vector or object/agent name/key. +string NPC_LINK = "link"; // Link number, number, string, (optional) key +string NPC_LISTEN = "listen"; // new command channel +string NPC_LOCATE = "locate"; // NPC name +string NPC_NUKE = "nuke"; // No arguments. +string NPC_ROTATE = "rotate"; // NPC name, Z rotation in degrees. +string NPC_SAY = "say"; // NPC name, thing to say, or relay channel. +string NPC_SCRIPT = "script"; // Script notecard name. +string NPC_SHOUT = "shout"; // NPC name, thing to shout, or relay channel. +string NPC_SIT = "sit"; // NPC name, object to sit on. +string NPC_SLEEP = "sleep"; // Seconds. +string NPC_STALK = "stalk"; // NPC name, avatar name, (optional) distance (float or vector). +string NPC_STAND = "stand"; // NPC name. +string NPC_STOP = "stop"; // NPC name. +string NPC_STOPANIM = "stopanim"; // NPC name, animation name +string NPC_TOUCH = "touch"; // NPC name, object to touch. +string NPC_WHISPER = "whisper"; // NPC name, thing to whisper, or relay channel. + +string NPC_MAIN = "Main"; +string NPC_PICK = "Pick"; +string NPC_NPC = "NPC"; +string NPC_RELAY = "Relay"; + +// LSL allows setting variables at this level to the values of previously declared variables. +// But the OpenSim script engine developer had other ideas. +// Otherwise we would not need this duplication. +list OUR_COMMANDS = ["0", "", "", "" + + "animate|LA|" + + "attention|L|" + + "change|LC.avatar|" + + "clone|L|" + + "come|L|" +// "complete|L|" a script only command handled outside the usual system. + + "create|Nv|" // Yes first argument should be C, but we want an NPC name anyway. + + "delete|L|" + + "dismissed|L|" + + "follow|Ls|" // Just ask for a string second argument, sort them out when they get here. + + "fly|Ls|" // Just ask for a string second argument, sort them out when they get here. + + "go|Ls|" // Just ask for a string second argument, sort them out when they get here. + + "land|Ls|" // Just ask for a string second argument, sort them out when they get here. + + "link|IISk|" // TODO - the system can't handle that k on the end yet. + + "listen|I|" + + "locate|L|" + + "nuke||" + + "rotate|LF|" + + "say|Ls|" // Just ask for a string second argument, sort them out when they get here. + + "script|X.npc|" + + "shout|Ls|" // Just ask for a string second argument, sort them out when they get here. + + "sit|Ls|" + + "sleep|F|" + + "stalk|LLs|" // Just ask for a string second argument, sort them out when they get here. + + "stand|L|" + + "stop|L|" + + "stopanim|LA|" + + "touch|LS|" + + "whisper|Ls|" // Just ask for a string second argument, sort them out when they get here. + ]; + +// When sending multiple commands, some don't need to be expanded. +list OUR_COMMANDS_NONAMES = ["attention", "create", "link", "listen", "nuke", "script", "sleep"]; + +integer IS_NPC; +integer IS_BOTH; +integer OBJECT; + +vector STALK_DISTANCE = <-3.0, 0.0, 0.0>; +integer SIM_CHANNEL = -65767365; + +string NPC_NPC_EXT = ".avatar"; +string NPC_SCRIPT_EXT = ".npc"; +string NPC_BACKUP_CARD = "Restore"; +string NPC_RECORD_CARD = "Recorded"; +string NPC_TEMP_CARD = "Temporary"; + +integer NPC_RECORD = -100; +integer NPC_RECORD_STOP = -101; +integer NPC_NEW_NPC = -102; +integer NPC_ADD_CHATTER = -103; +integer NPC_DEL_CHATTER = -104; + + +// Stuff for onefangs common utilities. +key scriptKey; + +string UTILITIES_SCRIPT_NAME = "onefang's utilities"; +string LIST_SEP = "$!#"; // Used to seperate lists when sending them as strings. +integer UTILITIES_RESET = -1; +integer UTILITIES_RESET_DONE = -2; +integer UTILITIES_READ = -3; +integer UTILITIES_READ_DONE = -4; +integer UTILITIES_SUBSTITUTE = -5; +integer UTILITIES_SUBSTITUTE_DONE = -6; +integer UTILITIES_NEXT_WORD = -7; +integer UTILITIES_NEXT_WORD_DONE = -8; +integer UTILITIES_PAYMENT_MADE = -9; +integer UTILITIES_PAYMENT_MADE_DONE = -10; +integer UTILITIES_MENU = -11; +integer UTILITIES_MENU_DONE = -12; +integer UTILITIES_WARP = -13; +integer UTILITIES_WARP_DONE = -14; +integer UTILITIES_AVATAR_KEY = -15; +integer UTILITIES_AVATAR_KEY_DONE = -16; +integer UTILITIES_CHAT = -17; +integer UTILITIES_CHAT_DONE = -18; +integer UTILITIES_CHAT_FAKE = -19; +integer UTILITIES_CHAT_FAKE_DONE = -20; + + +integer checkChat(key npc, string message, integer type, key newNpc) +{ + // Check if it's a single word. + if (-1 == llSubStringIndex(message, " ")) + { + integer channel = (integer) message; + + // Check if it's a sane number. + if ((message == (string) channel) && (channel < 12)) + { + integer old = llListFindList(chatters, [npc]); + + if (NULL_KEY != newNpc) + chatters = llListReplaceList(chatters, [newNpc], old, old); + else + { + // We are using llList2String here, coz the above find might return -1, and only llList2String can handle that. + key oldNpc = (key) llList2String(chatters, old + CHAT_KEY); + integer oldType = (integer) llList2String(chatters, old + CHAT_TYPE); + integer handle = (integer) llList2String(chatters, old + CHAT_HANDLE); + + // No matter what, remove the old one for this NPC. + if (-1 != old) + { + llListenRemove(handle); + chatters = llListReplaceList(chatters, ["", 0, 0], old, old + CHAT_STRIDE - 1); + } + if (0 < channel) + { + handle = (integer) llList2String(chatters, (channel * CHAT_STRIDE) + CHAT_HANDLE); + if (handle) + llListenRemove(handle); + handle = llListen(channel, "", NULL_KEY, ""); + chatters = llListReplaceList(chatters, [npc, type, handle], channel * CHAT_STRIDE, (channel * CHAT_STRIDE) + CHAT_STRIDE - 1); + llMessageLinked(LINK_SET, NPC_ADD_CHATTER, npc + "|" + (string) channel, scriptKey); + } + else + llMessageLinked(LINK_SET, NPC_DEL_CHATTER, npc, scriptKey); + } + return TRUE; + } + } + + if (NULL_KEY != npc) + { + string command = "#"; + + if (0 == type) {command = "whisper"; osNpcWhisper(npc, 0, message);} + else if (1 == type) {command = "say"; osNpcSay(npc, message);} + else if (2 == type) {command = "shout"; osNpcShout(npc, 0, message);} + if (recording) + recordIt(command, [llKey2Name(npc), message]); + } + return FALSE; +} + +startSensor(key user, key npc, string name, integer type, string command) +{ + sensorRequests += [name, type, command, user, npc]; + nextSensor(); +} + +nextSensor() +{ + if (sensorInFlight) + return; + if (0 < llGetListLength(sensorRequests)) + { + string name = llList2String(sensorRequests, 0); + integer type = llList2Integer(sensorRequests, 1); + string command = llList2String(sensorRequests, 2); + key user = llList2String(sensorRequests, 3); + key npc = llList2String(sensorRequests, 4); + + sensorInFlight = TRUE; + llSensor(name, "", type, 1024.0, TWO_PI); + } +} + +integer newScript(key user, string name) +{ + integer i; + integer length = llGetListLength(scripts); + + if (NULL_KEY == llGetInventoryKey(name)) + { + llSay(0, "No such script notecard - " + name); + return -1; + } + // Scan one past the end, so that if there's no free ones, we stick it at the end. + for (i = 0; i <= length; i += SCRIPTS_STRIDE) + { + if ("" == llList2String(scripts, i + SCRIPTS_KEY)) + { + scripts = llListReplaceList(scripts, [user, name, SCRIPT_READING, ""], i, i + SCRIPTS_STRIDE - 1); + llMessageLinked(LINK_SET, UTILITIES_READ, name, scriptKey + "|" + (string) i); + // A break command is a wonderful thing LL. + return i; + } + } + // Should never get here, but just in case, try to trigger an error later on. + return -1; +} + +recordIt(string command, list arguments) +{ + integer length = llGetListLength(arguments); + integer i; + float now = llGetTime(); + + // Record pauses between commands, but not if it's less than the script tick time, no point. + if ((0.0 < lastTick) && ((now - lastTick) > scriptTick)) + record += ["sleep " + (string) (now - lastTick)]; + lastTick = now; + for (i = 0; i < length; ++i) + { + string arg = llList2String(arguments, i); + + if (osIsUUID(arg)) + arg = llKey2Name(arg); + command += " " + arg; + } + record += [command]; +} + +key string2key(string name, integer type) +{ + if (osIsUUID(name)) + return (key) name; + else if (AGENT == type) + { + list names = llParseString2List(name, [" "], []); + key uuid = osAvatarName2Key(llList2String(names, 0), llList2String(names, 1)); + + return uuid; + } + else if ((IS_BOTH == type) || (IS_NPC == type)) // osAvatarName2Key() does not work on NPCs, so we gotta scan the entire sim. + { + // osGetAvatarList() skips the owner, so we need to add it.. + list avatars = [llGetOwner(), ZERO_VECTOR, llKey2Name(llGetOwner())] + osGetAvatarList(); // Strided list, UUID, position, name. + integer length = llGetListLength(avatars); + integer i; + + for (i = 0; i < length; i +=3) + { + key this = (key) llList2String(avatars, i); + string thisName = llList2String(avatars, i + 2); + + if ((thisName == name) && (osIsNpc(this) || (IS_BOTH == type))) + return this; + } + } + else if (llGetObjectName() == name) + return llGetKey(); + + return NULL_KEY; +} + +vector string2pos(string name) +{ + key uuid = NULL_KEY; + + if (("<" == llGetSubString(name, 0, 0)) && (">" == llGetSubString(name, -1, -1))) + { + // Looks like a vector, cast it. + return (vector) name; + } + + uuid = string2key(name, IS_BOTH); + if (NULL_KEY != uuid) + return (vector) llList2String(llGetObjectDetails(uuid, [OBJECT_POS]), 0); + + return ZERO_VECTOR; +} + +integer goThere(key user, string name, string dest, string type) +{ + key npc = string2key(name, IS_NPC); + integer executed = FALSE; + + if (NULL_KEY != npc) + { + vector pos = string2pos(dest); + + if (ZERO_VECTOR == pos) + startSensor(user, npc, dest, ACTIVE | PASSIVE, type); + else + { + integer found = llListFindList(movers, [npc]); + integer method = OS_NPC_FLY | OS_NPC_LAND_AT_TARGET; + list this = [npc, pos, llVecMag((vector) llList2String(llGetObjectDetails(npc, [OBJECT_POS]), 0) - pos), llGetTime()]; + + if ((0 <= found) && ((found % MOVE_STRIDE) == 0)) + llListReplaceList(movers, this, found, found + MOVE_STRIDE - 1); + else + movers += this; + + if (NPC_GO == type) + method = OS_NPC_NO_FLY; + else if (NPC_FLY == type) + method = OS_NPC_FLY; + else if (NPC_LAND == type) + method = OS_NPC_FLY | OS_NPC_LAND_AT_TARGET; + // Telling a sitting NPC to move results in an error, so tell them to stand up, just in case. + osNpcStand(npc); + osNpcMoveToTarget(npc, pos, method); + executed = TRUE; + } + } + return executed; +} + +killNPC(key npc, key newNpc) +{ + integer found = llListFindList(followers, [npc]); + integer length = llGetListLength(attention); + + osNpcRemove(npc); + // Stop this attention whore from chatting, moving, and stalking. + checkChat(npc, "0", 0, newNpc); + + while (0 <= found) + { + if ((found % FOLLOW_STRIDE) == FOLLOW_KEY) // Change of stalker. + { + if (NULL_KEY != newNpc) + followers = llListReplaceList(followers, [newNpc], found, found); + else + followers = llDeleteSubList(followers, found, found + FOLLOW_STRIDE - 1); + } + else if ((found % FOLLOW_STRIDE) == FOLLOW_STALK) // Change of stalkee. + { + if (NULL_KEY != newNpc) + followers = llListReplaceList(followers, [newNpc], found, found); + else + followers = llDeleteSubList(followers, found - FOLLOW_STALK, found - FOLLOW_STALK + FOLLOW_STRIDE - 1); + } + found = llListFindList(followers, [npc]); + } + found = llListFindList(movers, [npc]); + if ((0 <= found) && ((found % MOVE_STRIDE) == 0)) + { + // The only user of newNpc wants even the new one gone from the movers list. + // But we do this anyway, coz it will get confused, and might need it later. + if (NULL_KEY != newNpc) + movers = llListReplaceList(movers, [newNpc], found, found); + else + movers = llDeleteSubList(movers, found, found + MOVE_STRIDE - 1); + } + // Search the attention lists to and remove/replace them from that. + for (found = 0; found < length; found += ATTENT_STRIDE) + delAttention(llList2String(attention, found + ATTENT_KEY), npc, newNpc); +} + +addAttention(key user, key npc) +{ + list npcs = []; + integer handle = 0; + integer isUser = FALSE; + integer found = llListFindList(attention, [(string) user]); + + // TODO - This should not be happening, I think, but it does. + if (NULL_KEY == npc) + return; + + if (osIsUUID(user)) + isUser = TRUE; + + if (0 != (found % ATTENT_STRIDE)) + found = -1; + + if (-1 == found) + { + if (isUser) + handle = llListen(commandChannel, llKey2Name(user), user, ""); + } + else + { + handle = llList2Integer(attention, found + ATTENT_HANDLE); + npcs = llParseString2List(llList2String(attention, found + ATTENT_NPCS), ["|"], []); + attention = llDeleteSubList(attention, found, found + ATTENT_STRIDE - 1); + found = llListFindList(npcs, [(string) npc]); + if (-1 != found) + npcs = llDeleteSubList(npcs, found, found); + } + attention += [user, handle, llDumpList2String(npcs + [npc], "|")]; + if (isUser) + llSay(0, llKey2Name(npc) + " pays attention to " + llKey2Name(user)); +} + +// Replaces instead of deletes if newNpc is not NULL. +// Deletes them all if npc is NULL. +delAttention(key user, key npc, key newNpc) +{ + list npcs = []; + integer handle; + integer isUser = FALSE; + integer found = llListFindList(attention, [(string) user]); + + if (osIsUUID(user)) + isUser = TRUE; + if (0 != (found % ATTENT_STRIDE)) + found = -1; + + if (-1 != found) + { + handle = llList2Integer(attention, found + ATTENT_HANDLE); + npcs = llParseString2List(llList2String(attention, found + ATTENT_NPCS), ["|"], []); + attention = llDeleteSubList(attention, found, found + ATTENT_STRIDE - 1); + if (NULL_KEY == npc) + npcs = []; + else + { + found = llListFindList(npcs, [(string) npc]); + if (-1 != found) + { + if (NULL_KEY != newNpc) + npcs = llListReplaceList(npcs, [newNpc], found, found); + else + npcs = llDeleteSubList(npcs, found, found); + } + } + if (0 == llGetListLength(npcs)) + llListenRemove(handle); + else + attention += [user, handle, llDumpList2String(npcs, "|")]; + if (isUser && (NULL_KEY == newNpc)) + llSay(0, llKey2Name(npc) + " ignores " + llKey2Name(user)); + } +} + +sendManyCommands(key user, string command, key index) +{ + list npcs = []; + integer handle; + integer found = llListFindList(attention, [(string) user]); + + if (user != index) + found = llListFindList(attention, [(string) index]); + if (0 != (found % ATTENT_STRIDE)) + found = -1; + + if (-1 != found) + { + integer length; + integer i; + string thisCommand; + string rest = ""; + + handle = llList2Integer(attention, found + ATTENT_HANDLE); + npcs = llParseString2List(llList2String(attention, found + ATTENT_NPCS), ["|"], []); + length = llGetListLength(npcs); + // Split the command on the first space, if there is one. + found = llSubStringIndex(command, " "); + thisCommand = llGetSubString(command, 0, found); + if (-1 != found) + { + thisCommand = llGetSubString(command, 0, found - 1); + rest = " " + llGetSubString(command, found, -1); + } + else + thisCommand = command; +// TODO - the original command will go through utilities as well, +// if it's a chat command, and generate an error. + // If it's on the no names list, then don't bother expanding the names. + if (-1 != llListFindList(OUR_COMMANDS_NONAMES, [thisCommand])) + { + // Don't bother if it's from chat, it got done already through the usual method. + if (0 == handle) + sendCommand(user, command); + } + else + { + for (i = 0; i < length; ++i) + sendCommand(user, thisCommand + " " + llList2String(npcs, i) + rest); + } + } +} + +sendCommand(key user, string command) +{ + llMessageLinked(LINK_SET, UTILITIES_CHAT_FAKE, llDumpList2String([0, llKey2Name(user), user, command], LIST_SEP), scriptKey); +} + +init() +{ + integer i; + if (llGetAttached()) + { + // We are attached to an avatar, so get it's key. + key realId = llGetOwnerKey(llGetKey()); + if (osIsNpc(realId)) + { + // TODO - Instead we should go into a "only control this NPC" mode. + llSay(0, "Deleting onefang's NPC scripts from this NPC."); + llRemoveInventory(UTILITIES_SCRIPT_NAME); + llRemoveInventory(llGetScriptName()); + } + else + { + // Only listen to the attachment wearer if attached. + OUR_COMMANDS = llListReplaceList(OUR_COMMANDS, [realId], 1, 1); + } + } + IS_NPC = ACTIVE; + IS_BOTH = SCRIPTED; + OBJECT = PASSIVE; + for (i = 0; i < 16; ++i) + chatters += ["", 0, 0]; + scriptKey = llGetInventoryKey(llGetScriptName()); + llMessageLinked(LINK_SET, UTILITIES_RESET, "reset", scriptKey); + llSetTimerEvent(scriptTick); +} + +default +{ + state_entry() + { + init(); + } + + on_rez(integer param) + { + init(); + } + + attach(key attached) + { + init(); + } + + changed(integer change) + { + // Restore NPCs if the sim restarted, after a delay to let the sim settle. + if (change & CHANGED_REGION_START) + restartTimer = llGetTime() + 60.0; + } + + link_message(integer sender_num, integer num, string value, key id) + { + list keys = llParseStringKeepNulls((string) id, ["|"], []); + string extra = llList2String(keys, 1); + + id = (key) llList2String(keys, 0); +//llSay(0, "id = " + (string) id + " extra = " + extra + " VALUE " + value); + if ((NPC_RECORD == num) && (scriptKey == id)) + { + record = []; + recording = TRUE; + } + else if ((NPC_RECORD_STOP == num) && (scriptKey == id)) + { + recording = FALSE; + lastTick = -1.0; + llRemoveInventory(NPC_RECORD_CARD + NPC_SCRIPT_EXT); + osMakeNotecard(NPC_RECORD_CARD + NPC_SCRIPT_EXT, record); + } + else if ((UTILITIES_RESET_DONE == num) && (llGetInventoryKey(UTILITIES_SCRIPT_NAME) == id)) + { + // Set our commands. + OUR_COMMANDS = llListReplaceList(OUR_COMMANDS, [commandChannel], 0, 0); + llMessageLinked(LINK_SET, UTILITIES_CHAT, llDumpList2String(OUR_COMMANDS, LIST_SEP), scriptKey); + } + else if ((UTILITIES_READ_DONE == num) && (scriptKey == id)) + { + // The script is done, remove the reading flag. + list command = llParseStringKeepNulls(value, [LIST_SEP], []); + string card = llList2String(command, 0); + // extra was pre strided when it was sent off. + integer i = ((integer) extra); + + if ("" != llList2String(scripts, i)) + { + integer flags = llList2Integer(scripts, i + SCRIPTS_FLAGS); + + scripts = llListReplaceList(scripts, [flags & (~SCRIPT_READING)], i + SCRIPTS_FLAGS, i + SCRIPTS_FLAGS); +//llSay(0, "Done reading " + (string) i + " " + card + "\n" + llDumpList2String(scripts, "~")); + } + } + else if (-1000 >= num) // SettingsReader telling us to change a setting. + { + // num is the line number : -1000 - settingsLine + // Not sure if llMessageLinked is a FIFO, but lets hope so. + // Otherwise we may have to resort to OpenSim card reading, and drop SL compatibility. + list command = llParseStringKeepNulls(value, [LIST_SEP], []); + list result = []; + string card = llList2String(command, 0); + integer length = llGetListLength(command); + // extra was pre strided when it was sent off. + integer i = (integer) extra; + integer j; + string commands = llList2String(scripts, i + SCRIPTS_COMMANDS); + + if ("" != commands) + result = [commands]; +//llSay(0, " reading " + (string) i + " " + card); +//if (0 == (num % 100)) {llSay(0, "read line " + (string) num); llSleep(0.1);} + for (j = 1; j < length; j += 2) + result += [llList2String(command, j) + LIST_SEP + llList2String(command, j + 1)]; + scripts = llListReplaceList(scripts, [llDumpList2String(result, "|")], i + SCRIPTS_COMMANDS, i + SCRIPTS_COMMANDS); + } + else if ((UTILITIES_CHAT_DONE == num) && (scriptKey == id)) + { + integer executed = FALSE; + // incoming channel | incoming name | incoming key | incoming message | prefix | command | list of arguments | rest of message + list result = llParseStringKeepNulls(value, [LIST_SEP], []); + //integer inchannel = llList2Integer(result, 0); + //string inName = llList2String (result, 1); + key user = llList2Key (result, 2); + //string inMessage = llList2String (result, 3); + //string prefix = llList2String (result, 4); + string command = llList2String (result, 5); + list arguments = llList2List (result, 6, -1); // Includes "rest of message" as the last one. +//llSay(0, "COMMAND " + value); + + // WARNING - Don't return out of this "if" chain, unless you don't want the command recorded. + // TODO - Maybe just do that instead of using the executed flag. + if (NPC_ATTENTION == command) + addAttention(user, string2key(llList2String(arguments, 0), IS_NPC)); + if (NPC_DISMISSED == command) + delAttention(user, string2key(llList2String(arguments, 0), IS_NPC), NULL_KEY); + else if (NPC_CLONE == command) + { + string name = llList2String(arguments, 0); + key uuid = string2key(name, AGENT); + + if (osIsUUID(name)) + name = llKey2Name(name); + if (NULL_KEY != uuid) + { + osAgentSaveAppearance(uuid, name + NPC_NPC_EXT); + executed = TRUE; + } + } + else if (NPC_CREATE == command) + { + string name = llList2String(arguments, 0); + list names = llParseString2List(name, [" "], []); + string last = llList2String(names, 1); + vector pos = llList2String(arguments, 1); + key npc; + integer fromMenu = FALSE; + + // Either strip off the extension, or add it. + if (llSubStringIndex(last, NPC_NPC_EXT) != -1) + { + last = llGetSubString(last, 0, -1 - llStringLength(NPC_NPC_EXT)); + // This is an evil hack, the menu system will hand us the full name of the notecard. + // Other methods are likely to not do so. But only likely. + // TODO - I think this idea fails with the new argument parsing. DOH! + fromMenu = TRUE; + } + else + name += NPC_NPC_EXT; + if (ZERO_VECTOR == pos) + pos = llGetPos() + (<1.0, 0.0, 1.5> * llGetRot()); + npc = osNpcCreate(llList2String(names, 0), last, pos, name, OS_NPC_SENSE_AS_AGENT | OS_NPC_NOT_OWNED); + executed = TRUE; + if (fromMenu) + llMessageLinked(LINK_SET, NPC_NEW_NPC, user + "|" + npc, scriptKey); + } + else if (NPC_CHANGE == command) + { + string card = llList2String(arguments, 1); + + if (llSubStringIndex(card, NPC_NPC_EXT) == -1) + card += NPC_NPC_EXT; + osNpcLoadAppearance(string2key(llList2String(arguments, 0), IS_NPC), card); + executed = TRUE; + } + else if (NPC_COME == command) + executed = goThere(user, llList2String(arguments, 0), user, NPC_GO); + else if (NPC_FOLLOW == command) + { + key npc = string2key(llList2String(arguments, 0), IS_NPC); + + if (NULL_KEY != npc) + { + integer found = llListFindList(followers, [npc]); + vector pos = (vector) llList2String(arguments, 1); + + if (ZERO_VECTOR == pos) + { + float distance = llList2Float(arguments, 1); + + if (0.0 == distance) + pos = STALK_DISTANCE; + else + pos = ; + } + if ((0 <= found) && ((found % FOLLOW_STRIDE) == 0)) + llListReplaceList(followers, [npc, user, pos], found, found + FOLLOW_STRIDE - 1); + else + followers += [npc, user, pos]; + executed = TRUE; + } + } + else if (NPC_STALK == command) + { + key npc = string2key(llList2String(arguments, 0), IS_NPC); + key avatar = string2key(llList2String(arguments, 1), IS_BOTH); + + // No stalking yourself, that's just creepy. + if ((NULL_KEY != avatar) && (NULL_KEY != npc) && (avatar != npc)) + { + integer found = llListFindList(followers, [npc]); + vector pos = (vector) llList2String(arguments, 2); + + if (ZERO_VECTOR == pos) + { + float distance = llList2Float(arguments, 2); + + if (0.0 == distance) + pos = STALK_DISTANCE; + else + pos = ; + } + if ((0 <= found) && ((found % FOLLOW_STRIDE) == 0)) + llListReplaceList(followers, [npc, avatar, pos], found, found + FOLLOW_STRIDE - 1); + else + followers += [npc, avatar, pos]; + executed = TRUE; + } + } + else if (NPC_GO == command) + executed = goThere(user, llList2String(arguments, 0), llList2String(arguments, 1), NPC_GO); + else if (NPC_FLY == command) + executed = goThere(user, llList2String(arguments, 0), llList2String(arguments, 1), NPC_FLY); + else if (NPC_LAND == command) + executed = goThere(user, llList2String(arguments, 0), llList2String(arguments, 1), NPC_LAND); + else if (NPC_LINK == command) + { + llMessageLinked(llList2Integer(arguments, 0), llList2Integer(arguments, 1), + llList2String(arguments, 2), llList2String(arguments, 3)); + executed = TRUE; + } + else if (NPC_LISTEN == command) + { + // Remove our chat commands from whatever channel they where on before. + llMessageLinked(LINK_SET, UTILITIES_CHAT, llDumpList2String(llList2List(OUR_COMMANDS, 0, 0) + ["", "", ""], LIST_SEP), scriptKey); + // Set them on the new channel. + commandChannel = llList2Integer(arguments, 0); + OUR_COMMANDS = llListReplaceList(OUR_COMMANDS, [commandChannel], 0, 0); + llMessageLinked(LINK_SET, UTILITIES_CHAT, llDumpList2String(OUR_COMMANDS, LIST_SEP), scriptKey); + executed = TRUE; + } + else if (NPC_LOCATE == command) + { + key npc = string2key(llList2String(arguments, 0), IS_NPC); + vector pos = string2pos(llList2String(arguments, 0)); + vector size = llGetAgentSize(npc); + + // This wont work, it HAS to be in a touch event. Silly LL and their hobbled thinking. + //llMapDestination(llGetRegionName(), pos, pos); + + // Offset by halfish the NPCs size, so it should end up above them. + pos.z += size.z / 1.8; + // First destroy any existing beacons. Simplifies things. + llRegionSay(SIM_CHANNEL, "nobeacon"); + llRezObject("locator beacon", llGetPos(), ZERO_VECTOR, llEuler2Rot(<180.0 * DEG_TO_RAD, 0.0, 0.0>), SIM_CHANNEL); + // Wait for it to finish starting up. A hack I know, avoids making things more complex. + // Avoids complications with object_rez(key uuid) events and having to track what we rezzed. + llSleep(1.0); + llRegionSay(SIM_CHANNEL, "beacon " + (string) pos); + } + else if (NPC_ANIMATE == command) + { + osNpcPlayAnimation(string2key(llList2String(arguments, 0), IS_NPC), llList2String(arguments, 1)); + executed = TRUE; + } + else if (NPC_STOPANIM == command) + { + osNpcStopAnimation(string2key(llList2String(arguments, 0), IS_NPC), llList2String(arguments, 1)); + executed = TRUE; + } + else if (NPC_ROTATE == command) + { + rotation rot = llEuler2Rot(<0.0, 0.0, llList2Float(arguments, 1) * DEG_TO_RAD>); + + osNpcSetRot(string2key(llList2String(arguments, 0), IS_NPC), rot); + executed = TRUE; + } + else if (NPC_SCRIPT == command) + { + newScript(user, llList2String(arguments, 0)); + // Don't actually record this, since it's commands will be recorded. + executed = FALSE; + } + else if (NPC_SAY == command) + executed = checkChat(string2key(llList2String(arguments, 0), IS_NPC), llList2String(arguments, 1), 1, NULL_KEY); + else if (NPC_SHOUT == command) + executed = checkChat(string2key(llList2String(arguments, 0), IS_NPC), llList2String(arguments, 1), 2, NULL_KEY); + else if (NPC_STOP == command) + { + key npc = string2key(llList2String(arguments, 0), IS_NPC); + integer found = llListFindList(followers, [npc]); + + if ((0 <= found) && ((found % FOLLOW_STRIDE) == 0)) + followers = llDeleteSubList(followers, found, found + FOLLOW_STRIDE - 1); + found = llListFindList(movers, [npc]); + if ((0 <= found) && ((found % MOVE_STRIDE) == 0)) + movers = llDeleteSubList(movers, found, found + MOVE_STRIDE - 1); + osNpcStopMoveToTarget(npc); + executed = TRUE; + } + else if (NPC_COMPLETE == command) // Do nothing, it's a script only command handled completely in the script. + executed = TRUE; + else if (NPC_WHISPER == command) + executed = checkChat(string2key(llList2String(arguments, 0), IS_NPC), llList2String(arguments, 1), 0, NULL_KEY); + else if (NPC_SIT == command) + { + key npc = string2key(llList2String(arguments, 0), IS_NPC); + string name = llList2String(arguments, 1); + + if (NULL_KEY != npc) + { + key that = string2key(name, OBJECT); + + if (NULL_KEY == that) + startSensor(user, npc, name, ACTIVE | SCRIPTED, NPC_SIT); + else + { + osNpcSit(npc, that, OS_NPC_SIT_NOW); + executed = TRUE; + } + } + } + else if (NPC_TOUCH == command) + { + key npc = string2key(llList2String(arguments, 0), IS_NPC); + string name = llList2String(arguments, 1); + + if (NULL_KEY != npc) + { + key that = string2key(name, OBJECT); + + if (NULL_KEY == that) + startSensor(user, npc, name, ACTIVE | SCRIPTED, NPC_TOUCH); + else + { + osNpcTouch(npc, that, LINK_ROOT); + executed = TRUE; + } + } + } + else if (NPC_STAND == command) + { + osNpcStand(string2key(llList2String(arguments, 0), IS_NPC)); + executed = TRUE; + } + else if (NPC_DELETE == command) + { + // Since this deletes the NPC, if we are recording we wont be able to do llKey2Name below. + string name = llKey2Name(string2key(llList2String(arguments, 0), IS_NPC)); + + killNPC(string2key(llList2String(arguments, 0), IS_NPC), NULL_KEY); + arguments = [name]; + executed = TRUE; + } + else if (NPC_NUKE == command) + { + list avatars = osGetAvatarList(); // Strided list, UUID, position, name. + integer length = llGetListLength(avatars); + integer i; + + for (i = 0; i < length; i++) + { + key this = (key) llList2String(avatars, i * 3); + + if (osIsNpc(this)) + osNpcRemove(this); + } + attention = []; + chatters = []; + followers = []; + movers = []; + scripts = []; + sensorRequests = []; + sensorInFlight = FALSE; + // Delete the beacons as well. + llRegionSay(SIM_CHANNEL, "nobeacon"); + executed = TRUE; + } + + // Record it, but only if it did something. + if (recording && executed) + recordIt(command, arguments); + } + } + + listen(integer channel, string name, key id, string message) + { + key npc = (key) llList2String (chatters, (channel * CHAT_STRIDE) + CHAT_KEY); + integer type = llList2Integer(chatters, (channel * CHAT_STRIDE) + CHAT_TYPE); + integer handle = llList2Integer(chatters, (channel * CHAT_STRIDE) + CHAT_HANDLE); + + // It's either in the chatters or the attention list. + // Chatters are on their own channel. + // Attentions are on the usual command channel (common for all users). + // Utilities will also be listening on the command channel. And will get a duplicate utterance, sans the name. + // So choose between them based on channel. + // Users that make a chatter channel the same as the command channel deserve what they get. + if (channel == commandChannel) + sendManyCommands(id, message, id); + if (NULL_KEY != npc) + checkChat(npc, message, type, NULL_KEY); + } + + no_sensor() + { + sensorInFlight = FALSE; + if (llGetListLength(sensorRequests)) + { + sensorRequests = llDeleteSubList(sensorRequests, 0, 4); + nextSensor(); + } + } + + sensor(integer numberDetected) + { + sensorInFlight = FALSE; + if (llGetListLength(sensorRequests)) + { + string name = llList2String(sensorRequests, 0); + integer type = llList2Integer(sensorRequests, 1); + string command = llList2String(sensorRequests, 2); + key user = llList2String(sensorRequests, 3); + key npc = llList2String(sensorRequests, 4); + + sensorRequests = llDeleteSubList(sensorRequests, 0, 4); + sendCommand(user, command + " " + npc + " " + llDetectedKey(0)); + nextSensor(); + } + } + + timer() + { + integer i; + integer length = llGetListLength(followers); + + // Check for sim restart. + if ((0.0 < restartTimer) && (llGetTime() > restartTimer)) + { + restartTimer = -1.0; + // Start with a nuke, just to clear out all the lists. + sendCommand(llGetOwnerKey(llGetKey()), NPC_NUKE); + sendCommand(llGetOwnerKey(llGetKey()), NPC_SCRIPT + " " + NPC_BACKUP_CARD + NPC_SCRIPT_EXT); + // The sim restarted, no need to take care of followers or scripts this time. + return; + } + + // First the followers. + for (i = 0; i < length; i += FOLLOW_STRIDE) + { + key npc = (key) llList2String(followers, i + FOLLOW_KEY); + key stalkee = (key) llList2String(followers, i + FOLLOW_STALK); + vector distance = (vector) llList2String(followers, i + FOLLOW_DIST); + float mag = llVecMag(distance); + list details = llGetObjectDetails(stalkee, [OBJECT_POS, OBJECT_VELOCITY, OBJECT_ROT]); + list npcDetails = llGetObjectDetails(npc, [OBJECT_POS, OBJECT_VELOCITY, OBJECT_ROT]); + vector pos = (vector) llList2String(details, 0); + vector speed = (vector) llList2String(details, 1); + rotation rot = (rotation) llList2String(details, 2); + integer npcInfo = llGetAgentInfo(npc); + integer info = llGetAgentInfo(stalkee); + integer isFlying = info & AGENT_FLYING; + integer isWalking = info & AGENT_WALKING; + integer isInAir = info & AGENT_IN_AIR; + integer isRunning = info & AGENT_ALWAYS_RUN; + integer isSitting = info & AGENT_SITTING; + vector newPos = pos + (distance * rot); + float newDist = llVecMag((vector) llList2String(npcDetails, 0) - pos); + + if (newDist > mag) + { + if (npcInfo & AGENT_SITTING) // Tell the lazy bum to stand up. + osNpcStand(npc); + if (isRunning) + osNpcMoveToTarget(npc, newPos, OS_NPC_NO_FLY | OS_NPC_RUNNING); + else if (isFlying || isInAir) + osNpcMoveToTarget(npc, newPos, OS_NPC_FLY); + else if (isWalking) + osNpcMoveToTarget(npc, newPos, OS_NPC_NO_FLY); + else + osNpcMoveToTarget(npc, newPos, OS_NPC_NO_FLY); +// osNpcMoveToTarget(npc, pos, OS_NPC_FLY | OS_NPC_LAND_AT_TARGET); + } + else + osNpcStopMoveToTarget(npc); + } + + // Then movers. + length = llGetListLength(movers); + for (i = 0; i < length; i += MOVE_STRIDE) + { + key npc = (key) llList2String (movers, i + MOVE_KEY); + vector dest = (vector) llList2String (movers, i + MOVE_DEST); + float least = llList2Float (movers, i + MOVE_LEAST); + float time = llList2Float (movers, i + MOVE_TIME); + float left = llVecMag((vector) llList2String(llGetObjectDetails(npc, [OBJECT_POS]), 0) - dest); + + if (least > left) // Progress has been made. + movers = llListReplaceList(movers, [left, llGetTime()], i + MOVE_LEAST, i + MOVE_TIME); + else if ((llGetTime() - time) > 5.0) // No progress, they are stuck. + { + key oldNpc = npc; + string name = llKey2Name(npc); + list names = llParseString2List(name, [" "], []); + list anims = llGetAnimationList(npc); + integer animLen = llGetListLength(anims); + integer invLen = llGetInventoryNumber(INVENTORY_ANIMATION); + integer j; + + llShout(0, name + " is stuck!"); +if ("" == name) +{ + llSay(0, "stuck key " + (string) i + " " + npc + " - " + oldNpc + " -- " + llDumpList2String(movers, "|")); + killNPC(oldNpc, NULL_KEY); + return; +} + osAgentSaveAppearance(npc, NPC_TEMP_CARD + NPC_NPC_EXT); + length = llGetListLength(movers); + npc = osNpcCreate(llList2String(names, 0), llList2String(names, 1), dest, NPC_TEMP_CARD + NPC_NPC_EXT, OS_NPC_SENSE_AS_AGENT | OS_NPC_NOT_OWNED); + llRemoveInventory(NPC_TEMP_CARD + NPC_NPC_EXT); + // We want to replace them into the Attention, chatters, and followers lists. + // Though no point adding them back to movers. +//llSay(0, "stuck key " + npc + " - " + oldNpc + " -- " + llDumpList2String(movers, "|")); + killNPC(oldNpc, npc); + + // Try to re instate the animations. + for (j = 0; j < animLen; ++j) + { + key anim = (key) llList2String(anims, j); + integer k; + + // For each of the anims that was playing on the old NPC, + // See if we can find a match in our inventory. + for (k = 0; k < invLen; ++k) + { + string thisName = llGetInventoryName(INVENTORY_ANIMATION, k); + + if (llGetInventoryKey(thisName) == anim) + { + osNpcPlayAnimation(npc, thisName); + k = invLen; + } + } + } + } + + if (left < 2.5) // They have arrived. + { + movers = llDeleteSubList(movers, i, i + MOVE_STRIDE - 1); + length -= MOVE_STRIDE; + osNpcStopMoveToTarget(npc); + } + } + + // Then the scripts. + length = llGetListLength(scripts); + for (i = 0; i < length; i += SCRIPTS_STRIDE) + { + string user = llList2String(scripts, i + SCRIPTS_KEY); + + if ("" != user) + { + string name = llList2String(scripts, i + SCRIPTS_NAME); + integer flags = llList2Integer(scripts, i + SCRIPTS_FLAGS); + list commands = llParseStringKeepNulls(llList2String(scripts, i + SCRIPTS_COMMANDS), ["|"], []); + list statement = llParseStringKeepNulls(llList2String(commands, 0), [LIST_SEP], []); + string command = llList2String(statement, 0); + string value = llList2Key(statement, 1); + + commands = llDeleteSubList(commands, 0, 0); + + if ("" == value) // A command with no =. Run it as a pretend chat command from the user. + { + if ("sleep " == llGetSubString(command, 0, 5)) + { + float time = (float) llGetSubString(command, 6, -1); + + commands = llListInsertList(commands, ["until " + (string)(llGetTime() + time)], 0); + } + else if ("until " == llGetSubString(command, 0, 5)) + { + float time = (float) llGetSubString(command, 6, -1); + + if (llGetTime() < time) + commands = llListInsertList(commands, [command], 0); + } + else if ("script " == llGetSubString(command, 0, 6)) + { + string new = llGetSubString(command, 7, -1); + integer index = newScript(user, new); + + if (0 == llGetListLength(commands)) + ;//llSay(0, "tail recursion detected - " + name + " -> " + new + " " + (string) i + " -> " + (string) index); + else if (-1 != index) + commands = llListInsertList(commands, ["wait " + (string) index + " " + new], 0); + } + else if ("wait " == llGetSubString(command, 0, 4)) + { + list parts = llParseStringKeepNulls(command, [" "], []); + integer index = llList2Integer(parts, 1); + string card = llList2String(parts, 2); + + // Check if the script this user started is still in the same slot we created above. + // Note, still possible to get a, hopefully rare, race condition here. + if ((llList2String(scripts, index + SCRIPTS_KEY) == user) && (llList2String(scripts, index + SCRIPTS_NAME) == card)) + commands = llListInsertList(commands, [command], 0); + } + else if ("complete" == llGetSubString(command, 0, 7) && (8 == llStringLength(command))) + { + // Deal with attention seekers, we have to wait for all of them to get there. + integer found = llListFindList(attention, [(string) i]); + + if (0 != (found % ATTENT_STRIDE)) + found = -1; + + if (-1 != found) + { + list npcs = llParseString2List(llList2String(attention, found + ATTENT_NPCS), ["|"], []); + integer nLength = llGetListLength(npcs); + integer j; + + for (j = 0; j < nLength; ++j) + { + key npc = llList2String(npcs, j); + found = llListFindList(movers, [npc]); + + // If any are still a mover, keep waiting for the move to complete. + if ((0 <= found) && ((found % MOVE_STRIDE) == 0)) + { + commands = llListInsertList(commands, [command], 0); + j = nLength; + } + } + } + } + else if ("complete " == llGetSubString(command, 0, 8)) + { + key npc = string2key(llGetSubString(command, 9, -1), IS_NPC); + integer found = llListFindList(movers, [npc]); + + // If they are still a mover, keep waiting for the move to complete. + if ((0 <= found) && ((found % MOVE_STRIDE) == 0)) + commands = llListInsertList(commands, [command], 0); + } + else if ("attention " == llGetSubString(command, 0, 9)) + { + addAttention((string) i, string2key(llGetSubString(command, 10, -1), IS_NPC)); + scripts = llListReplaceList(scripts, [flags | SCRIPT_ATTENTION], i + SCRIPTS_FLAGS, i + SCRIPTS_FLAGS); + } + else if ("dismissed " == llGetSubString(command, 0, 9)) + { + scripts = llListReplaceList(scripts, [flags & (~SCRIPT_ATTENTION)], i + SCRIPTS_FLAGS, i + SCRIPTS_FLAGS); + delAttention((string) i, string2key(llGetSubString(command, 10, -1), IS_NPC), NULL_KEY); + } + else if ("" != command) + { +//llSay(0, "DOING " + (string) (flags & SCRIPT_ATTENTION) + " " + command); + if (flags & SCRIPT_ATTENTION) + sendManyCommands(user, command, (string) i); + else + sendCommand(user, command); + } + } + else // A variable assignment. + { +// if ("DEBUG" == command) +// DEBUG = ("TRUE" == value); + } + + if ((flags & SCRIPT_READING) || (0 < llGetListLength(commands))) + scripts = llListReplaceList(scripts, [llDumpList2String(commands, "|")], i + SCRIPTS_COMMANDS, i + SCRIPTS_COMMANDS); + else + { + scripts = llListReplaceList(scripts, ["", "", 0, ""], i, i + SCRIPTS_STRIDE - 1); + // Remove all our attention seekers. + delAttention((string) i, NULL_KEY, NULL_KEY); + while ((llGetListLength(scripts) > 0) && ("" == llList2String(scripts, 0 - SCRIPTS_STRIDE))) + scripts = llDeleteSubList(scripts, 0 - SCRIPTS_STRIDE, -1); +//llSay(0, "Finished script " + (string) i + " " + llDumpList2String(commands, "^") + "\n" + llDumpList2String(scripts, "~")); + } + } + } + } + +} diff --git a/NPC_tool_help.txt b/NPC_tool_help.txt new file mode 100644 index 0000000..53eb5a3 --- /dev/null +++ b/NPC_tool_help.txt @@ -0,0 +1,171 @@ +NPC tool. +------------- + +This notecard documents onefang's "NPC tool" script. It needs another script called "onefang's utilities" to also be inside the prim. You can also include the "NPC menu" script to get the menu system. + +NOTE - this is an early release, there's things that are likely to change, but apparently lots of people are asking for it. Open source, release early, release often. + +Menu system. +-------------------- + +When you click on the object with the NPC tool script, you get it's menus. In general that just calls the below listed commands, presenting menus for picking avatars, objects, NPCs, and note cards as needed. This menu includes the "Backup NPCs", "Restore NPCs", "start recrording", and "stop recording" options that are not available as commands. + +The "nearby NPCs" option lets you select from the NPCs that are close to you, "Local NPCs" to select from any within sensor range, and "NPCs in sim" selects from all the NPCs in the sim. + +As a safety feature, if the NPC tool script is in an NPC's attachments, the script gets deleted. + + +Script and chat command system. +----------------------------------------------- + +All of this is subject to change as the tool is still under development. In particular, I plan on changing the commands to be more conversational. + +The commands can be said in local chat, or put into notecards to script NPCs. Script notecards need to have ".npc" at the end of their name. In a script notecard, lines starting with # are ignored, and so are blank lines. + +In general, each command is followed by the name of the NPC, then any other arguments. The NPC in these examples is "onefang's clone". UUIDs can be used to. + +Any objects mentioned by name have to be close enough to the prim with the NPC tool script in it for a sensor() to find it. If there are multiple objects with the same name, the closest one is chosen. + +There are three special notecard scripts for the backup and restore system. "Restore.npc" is created or over written each time you do a backup from the menus. When doing a restore, first "Restore.before.npc" is run, then "Restore.npc", and finally "Restore.after.npc". Any of those scripts that are missing are skipped. A restore is done automatically 60 seconds after the sim restarts. Or more, depends on when the script itself starts running again. + +There is also the "Recorded.npc" notecard, it holds anything you recorded with the "start recording" menu option. + +You can send these commands from some other LSL script in the same object. Add something like this to your script - + +string LIST_SEP = "$!#"; // Used to seperate lists when sending them as strings. +integer UTILITIES_CHAT_FAKE = -19; +key NPCscriptKey = llGetInventoryKey("NPC tool"); +sendCommand(key user, string command) +{ + llMessageLinked(LINK_SET, UTILITIES_CHAT_FAKE, llDumpList2String([0, llKey2Name(user), user, command], LIST_SEP), NPCscriptKey); +} + + +Creation / deletion commands. + These commands mostly deal with avatar appearance notecards, which are XML formatted notecards with their names ending in ".avatar". These cards are in the format that the OpenSim appearance functions use. + +clone onefang rejected + Creates an appearance notecard of the named avatar. In this example, that will be "onefang rejected.avatar". Yes, you can clone NPCs as well. + +create onefang's clone <1.0,2.0,3.0> + Creates an NPC from the named notecard, "onefang's clone.avatar" for example, at the given position. The new NPC will be named after the card, "onefang's clone" in this case. The position is optional, in which case the NPC is created close to the prim holding the NPC tool script. + +change onefang's clone other avatar + Causes the named NPC to change to match the appearance notecard named, "other avatar.avatar" in this example. + +delete onefang's clone + Remove the named NPC. + +nuke + Remove all NPCs in the sim. + + +Animation commands. + The animation has to be in the same prim as the NPC tool script. Multiple animations can be on an NPC at once, that's why there's start and stop commands. + +animate onefang's clone animation name + Start animating the named NPC using the named animation. + +stopanim onefang's clone animation name + Stop the named animation from animating the named NPC. + + +Chatting commands. + The named NPC say, shouts, or whispers the rest of the text in the command. If the text is just a number between 1 and 11, then that sets the number as a chat channel for that NPC to relay chat from, either said, shouted, or whispered to match. Using 0 will turn off chat relay for that NPC. The relay channel will be displayed in the NPCs menu. + +say onefang's clone Stuff to say. +shout onefang's clone Stuff to shout. +whisper onefang's clone Stuff to whisper. + + +Moving commands. + The go, fly, and land commands can have various ways of providing the destination. A vector position <1.0,2.0,3.0>, the name of an avatar or NPC, the name of an in world object, or a UUID of an avatar, in world object, or NPC. Note that's easily possible for a NPC to get stuck. They just don't understand navigating around a 3D world. If they get stuck, then they will get deleted and recreated at the destination. + +come onefang's clone + The NPC walks to the person using this command, or the person running the script via the menu system. + +complete onefang's clone + Wait for their movement to be completed. Only covers come, goto, flyto, and landat. + +go onefang's clone <1.0,2.0,3.0> + The NPC walks to the given destination. + +follow onefang's clone <-3.0, 0.0, 0.0> + Start to follow you at the (optional) distance. + +fly onefang's clone <1.0,2.0,3.0> + The NPC flies to the given destination, then remains hovering. + +land onefang's clone<1.0,2.0,3.0> + The NPC flies to the given destination, then lands. + +rotate onefang's clone 180 + The NPC rotates to the given direction in degrees. Note that there seems to be an OpenSim bug, not getting the rotations I expect. + +stalk onefang's clone onefang rejected <-3.0, 0.0, 0.0> + Start to follow the given avatar or NPC at the (optional) distance. + +stop onefang's clone + Stops the NPC from moving to a destination. + + +Scripting commands. + +link 1 2 Third thing to send. + Sends a link message "Third thing to send.", to link number 1, with num being 2. No key is sent, but that should be added later. + +listen 123 + Changes the NPC tool to listen to commands from a different channel. + +script scriptcard.npc + Runs an NPC script. If you use this inside another script, that script will wait for this new script to finish running. Circular references to scripts is not a good idea, it will eventually run out of memory. Note that it is theoretically possible for scripts running from scripts to get a little confused when running the same script multiple times, but hopefully that's rare. + +sleep 10.0 + Stops a script from running for the given number of seconds. Can be fractional seconds. Note that currently NPC tool only runs one command from the script every half a second. + + +World interaction commands. + The objects named in these commands have to be scripted. Names and UUIDs can be used for the objects. + +locate onefang's clone + Hangs a giant red beacon above the head of the NPC, so you can see where they are. Clicking on the beacon opens up the map to their position, with the TP beacon set. Honestly, just use your viewers radar. Only one beacon allowed. + +sit onefang's clone object to sit on + The NPC sits on the named in world object. + +stand onefang's clone + The NPC stands up. + +touch onefang's clone object to touch + The NPC touches the named in world object. If the object does things to avatars that touch them, it will get triggered. Things like dance balls will get automatic permission to animate the NPC. + + + +Copyright (C) 2013 David Seikel (onefang rejected). + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies of the Software and its Copyright notices. In addition publicly +documented acknowledgment must be given that this software has been used if no +source code of this software is made available publicly. This includes +acknowledgments in either Copyright notices, Manuals, Publicity and Marketing +documents or any documentation provided with any product containing this +software. This License does not apply to any software that links to the +libraries provided by this software (statically or dynamically), but only to +the software provided. + +Please see the COPYING-PLAIN for a plain-english explanation of this notice +and it's intent. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 1c4a247..1ebfa01 100644 --- a/README.md +++ b/README.md @@ -1,4 +1 @@ -NPC-tool -======== - -A tool for dealing with OpenSIm NPCs. +This is an early release of onefang's NPC tool, for controlling NPCs in OpenSim. diff --git a/locator_beacon_script.lsl b/locator_beacon_script.lsl new file mode 100644 index 0000000..4441993 --- /dev/null +++ b/locator_beacon_script.lsl @@ -0,0 +1,130 @@ +// Onefang's locator beacon script version 1.0. + +// A lot of this is just my tweaked up warpPos(), the rest is mostly trivial. +// So I'll put this into the public domain. + +integer SIM_CHANNEL = -65767365; +float offset = 128.0; +integer jumpSize = 512; + +goThere(vector destPos) +{ + float distance = llVecDist(llGetPos(), destPos); + + if (distance > 0.001) + { + integer jumps = 0; + integer time = llGetUnixTime(); + float oldDistance = 0; + //llSetPos(destPos); // This is not any faster, and only has a range of 10 meters. + //destPos += <0.0, 0.0, 2.0>; // Try to work around that damned Havok 4 bug. + + // We call the central routine several times to free the massive amount of memory used. + do + { + jumps += warpPos(distance, destPos); + distance = llVecDist(llGetPos(), destPos); + } + while (distance > 1.0 && ((llGetUnixTime() - time) < 2)); // Long jump, no limit. + + // OK, this is just being paranoid, but has been known to be needed in the past. + while ((distance > 0.001) && (0.001 < (distance - oldDistance)) // Failsafe. + && ((--jumps) > 0) && ((llGetUnixTime() - time) < 5)) // Time out. + { + llOwnerSay("Short hop from " + (string) llGetPos() + " of " + (string) distance + ". Jumps " + (string) jumps); + llSetPos(destPos); + oldDistance = distance; + distance = llVecDist(llGetPos(), destPos); + llSleep(0.5); + } + if (distance > 0.001) + { + llShout(0, "Failed to get to " + (string) destPos + ", I am @ " + (string) llGetPos()); + llInstantMessage(llGetOwner(), "Failed to get to " + (string) destPos + ", I am @ " + (string) llGetPos()); + } + } +} + +integer warpPos(float distance, vector destPos) +{ // R&D by Keknehv Psaltery, 05/25/2006 + // with a little pokeing by Strife, and a bit more + // some more munging by Talarus Luan + // Final cleanup by Keknehv Psaltery + // Extended distance by onefang Rejected. + // More optimizations by 1fang Fang. + + // Compute the number of jumps necessary. + integer jumps = (integer) (distance / 10.0) + 1; + list rules = [PRIM_POSITION, destPos]; // The start for the rules list. + integer count = 1; + + // Try and avoid stack/heap collisions. + if (jumps > jumpSize) + { + jumps = jumpSize; + llOwnerSay("Extra warp needed"); + } + while ((count = count << 1 ) < jumps) + rules += rules; + + //Changed by Eddy Ofarrel to tighten memory use some more + llSetPrimitiveParams(rules + llList2List(rules, (count - jumps) << 1, count)); +//llOwnerSay("Jumps " + (string) jumps + " free " + (string) llGetFreeMemory()); + return jumps; +} + +init() +{ + vector scale = llGetScale(); + + offset = scale.z / 2; + llListen(SIM_CHANNEL, "", NULL_KEY, ""); +} + +default +{ + state_entry() + { + init(); + } + + on_rez(integer param) + { + SIM_CHANNEL = param; + init(); + } + + attach(key attached) + { + init(); + } + + listen(integer channel, string name, key id, string message) + { + if ("beacon " == llGetSubString(message, 0, 6)) + { + vector pos = (vector) llGetSubString(message, 7, -1); + + if (ZERO_VECTOR != pos) + { + pos.z += offset; + // OpenSim bug, first one doesn't quite get there, second one needed. + // Happens with llSetPos() as well, llSetPrimitiveParams() at least does them both at once. + // Need warpPos anyway, which hides this bug. + goThere(pos); + llSetPrimitiveParams([PRIM_GLOW, ALL_SIDES, 1.0]); + llSetAlpha(1.0, ALL_SIDES); + } + } + else if ("nobeacon" == message) + llDie(); + } + + touch_start(integer num_detected) + { + vector pos = llGetPos(); + + pos.z -= offset; + llMapDestination(llGetRegionName(), pos, pos); + } +} diff --git a/onefang's_utilities.lsl b/onefang's_utilities.lsl new file mode 100644 index 0000000..e088141 --- /dev/null +++ b/onefang's_utilities.lsl @@ -0,0 +1,1189 @@ +// onefang's utilites version 3.0 +// Read a complete settings notecard and send settings to other scripts. +// Also other useful functions. + +// Copyright (C) 2007 David Seikel (onefang rejected). +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies of the Software and its Copyright notices. In addition publicly +// documented acknowledgment must be given that this software has been used if no +// source code of this software is made available publicly. This includes +// acknowledgments in either Copyright notices, Manuals, Publicity and Marketing +// documents or any documentation provided with any product containing this +// software. This License does not apply to any software that links to the +// libraries provided by this software (statically or dynamically), but only to +// the software provided. +// +// Please see the COPYING-PLAIN for a plain-english explanation of this notice +// and it's intent. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// As a special exception to the above conditions, the Second Life user known +// as Winter Ventura may ignore all permissions and conditions and provide her +// own. + +// See the notecard "onefang's utilities manual". + + +float MENU_TIMEOUT = 45.0; + +list channelHandles = []; // channel | listener +list chatChannels = []; // channel | script list +list chatOwners = []; // owner+channel | script list +list chatPrefixes = []; // channel+" "+prefix | script list (PREFIXES WITH SPACE) +list chatPrefixes2 = []; // channel+" "+prefix | script list (PREFIXES WITH NO SPACE) +list chatCommands = []; // script+channel | command list { command | argument defining stuff } +// TODO might be better to do - script+channel+" "+prefix | command list { command | argument defining stuff } +// and do away with the chatPrefix lists. Makes no space prefix checks harder I think. +// or add a prefix field to the list. + +// SettingsReaderAndUtilities constants for copying into other code. +string LIST_SEP = "$!#"; // Used to seperate lists when sending them as strings. +integer UTILITIES_RESET = -1; +integer UTILITIES_RESET_DONE = -2; +integer UTILITIES_READ = -3; +integer UTILITIES_READ_DONE = -4; +integer UTILITIES_SUBSTITUTE = -5; +integer UTILITIES_SUBSTITUTE_DONE = -6; +integer UTILITIES_NEXT_WORD = -7; +integer UTILITIES_NEXT_WORD_DONE = -8; +integer UTILITIES_PAYMENT_MADE = -9; // Not used by this script +integer UTILITIES_PAYMENT_MADE_DONE = -10; // Not used by this script +integer UTILITIES_MENU = -11; +integer UTILITIES_MENU_DONE = -12; +integer UTILITIES_WARP = -13; +integer UTILITIES_WARP_DONE = -14; +integer UTILITIES_AVATAR_KEY = -15; // Redundant in OpenSim. +integer UTILITIES_AVATAR_KEY_DONE = -16; // Redundant in OpenSim. +integer UTILITIES_CHAT = -17; +integer UTILITIES_CHAT_DONE = -18; +integer UTILITIES_CHAT_FAKE = -19; +integer UTILITIES_CHAT_FAKE_DONE = -20; // Never used anywhere. +// TODO reuse -20 for CHAT_PURGE - which removes this scriptKey from all chat lists, usually before replacing them. +// Which was done some other way if I remember. + +// internal constant and variable declarations. +integer KEYS_SCRIPT = 0; +integer KEYS_TYPE = 1; +integer KEYS_NAME = 2; +integer KEYS_EXTRA = 3; +integer KEYS_ID = 4; +integer KEYS_STRIDE = 5; + +string REQUEST_KEY = "1"; +string REQUEST_IM = "2"; +string REQUEST_ONLINE = "3"; +string REQUEST_AVATAR = "4"; + +integer MENU_USER = 0; // Key of user the big menu is for. +integer MENU_CHANNEL = 1; // Listen channel for big menu dialogs. +integer MENU_SCRIPT = 2; // ScriptKey of the script calling the menu. +integer MENU_HANDLE = 3; // Listen ID for big menu dialogs. +integer MENU_TYPE = 4; // Type of menu. +integer MENU_MESSAGE = 5; // Message for the top of big menu dialog. +integer MENU_MENU = 6; // The big menu itself. LIST_SEP separated when they come in. +integer MENU_FILTER = 7; // Regex filter for inventory menus. | separated when they come in. +integer MENU_POS = 8; // Current position within big menu. +integer MENU_MAXPOS = 9; // Maximum position within big menu. +integer MENU_NAMES = 10; // Long names to get around stupid SL menu limitations. +integer MENU_TIME = 11; // Otherwise this list is ever growing. Damn Ignore button. sigh +integer MENU_STRIDE = 12; + +list settingsCards = []; // Queue of cards to read. +string settingsName = ".settings"; // Name of a notecard in the object's inventory. +key settingsKey = NULL_KEY; // ScriptKey of the script reading the card. +integer settingsLine = 0; // Current line number. +key settingsQueryID = NULL_KEY; // ID used to identify dataserver queries. + +list menus = []; // The menus. +list registeredMenus = []; // Any scripts that have registered as needing a touch menu. +list keyRequests = []; // A list of avatar key requests. + +list ANIMATIONS = +[ + "aim_L_bow", "aim_R_bazooka", "aim_R_handgun", "aim_R_rifle", "angry_fingerwag", + "angry_tantrum", "away", "backflip", "blowkiss", "bow", "brush", "clap", + "courtbow", "cross_arms", "crouch", "crouchwalk", "curtsy", + "dance1", "dance2", "dance3", "dance4", "dance5", "dance6", "dance7", "dance8", + "dead", "drink", "express_afraid", "express_anger", "express_bored", + "express_cry", "express_embarrased", "express_laugh", "express_repulsed", + "express_sad", "express_shrug", "express_surprise", "express_wink", + "express_worry", "falldown", "female_walk", "fist_pump", "fly", "flyslow", + "hello", "hold_R_bow", "hold_R_bazooka", "hold_R_handgun", "hold_R_rifle", + "hold_throw_R", "hover", "hover_down", "hover_up", "impatient", + "jump", "jumpforjoy", "kick_roundhouse_R", "kissmybutt", "kneel_left", + "kneel_right", "land", "laugh_short", "motorcycle_sit", "musclebeach", "no_head", + "no_unhappy", "nyanya", "peace", "point_me", "point_you", + "prejump", "punch_L", "punch_onetwo", "punch_R", + "RPS_countdown", "RPS_paper", "RPS_rock", "RPS_scissors", + "run", "salute", "shoot_L_bow", "shout", "sit", "sit_female", "sit_ground", + "sit_to_stand", "sleep", "slowwalk", "smoke_idle", "smoke_inhale", + "smoke_throw_down", "snapshot", "soft_land", "stand", "standup", "stand_1", + "stand_2", "stand_3", "stand_4", "stretch", "stride", "surf", "sword_strike_R", + "talk", "throw_R", "tryon_shirt", "turnback_180", "turnleft", "turnright", + "turn_180", "type", "uphillwalk", "walk", "whisper", "whistle", "wink_hollywood", + "yell", "yes_happy", "yes_head", "yoga_float" +]; + + +list addChatScripts(list thisList, string name, string value, integer stride) +{ + integer found = llListFindList(thisList, [name]); + + if (0 <= found) + { + list values = llParseString2List(llList2String(thisList, found + 1), ["|"], []); + + if (1 == stride) + { + integer subFound = llListFindList(values, [value]); + + if (0 > subFound) + values += [value]; + } + else + { + integer length = llGetListLength(values); + integer i; + + for (i = 0; i < length; i += stride) + { + list sub = llList2List(values, i, i + stride - 1); + integer subFound = llListFindList(values, [llList2String(sub, 0)]); + + if (0 > subFound) + values += sub; + } + } + thisList = llListReplaceList(thisList, [name, llDumpList2String(values, "|")], found, found + 1); + } + else + thisList += [name, value]; + return thisList; +} + +list delChatScripts(list thisList, string name, string value, integer stride) +{ + integer found = llListFindList(thisList, [name]); + + if (0 <= found) + { + list values = llParseString2List(llList2String(thisList, found + 1), ["|"], []); + + if (1 == stride) + { + integer subFound = llListFindList(values, [value]); + + if (0 <= subFound) + values = llDeleteSubList(values, subFound, subFound); + } + else + { + integer length = llGetListLength(values); + integer i; + + for (i = 0; i < length; i += stride) + { + list sub = llList2List(values, i, i + stride - 1); + integer subFound = llListFindList(values, [llList2String(sub, 0)]); + + if (0 <= subFound) + values = llDeleteSubList(values, subFound, subFound + stride - 1); + } + } + if (llGetListLength(values)) + thisList = llListReplaceList(thisList, [name, llDumpList2String(values, "|")], found, found + 1); + else + thisList = llDeleteSubList(thisList, found, found + 1); + } + return thisList; +} + +resetPrimShit() +{ + llParticleSystem([]); +// Comment this out for now. Causes problems with updates, and is probably not needed. +// llSetRemoteScriptAccessPin(0); + llSetText("", <1, 1, 1 >, 1.0); + llSetTextureAnim(FALSE | SMOOTH | LOOP, ALL_SIDES, 1, 1, 0, 0, 0.0); + llTargetOmega(<0, 0, 0 >, 0, 0); +} + +// Get the next word. +// The last parameter is the last word returned from the previous call. +// The list results are - +// 0 = The rest of the text. +// 1 = The next word. +list nextWord(string separator, string message, string last) +{ + list result = []; + integer index; + + index = llSubStringIndex(message, separator); + if (0 <= index) + { + if (0 != index) // Check for a corner case. + last += llGetSubString(message, 0, index - 1); + if ((index + 1) < llStringLength(message)) + message = llGetSubString(message, index + 1, -1); + else + message = ""; + result += message; + result += last; + } + else + { + result += ""; + result += last + message; + } + + return (result); +} + +// Substitute params from a list. +string substitute(list values, string separator) +{ + string result = ""; + string original = llList2String(values, 0); + integer length = llGetListLength(values); + +// integer length = (values != []); // Speed hack. + integer index = 1; + + while ((0 <= index) && ("" != original)) + { + index = llSubStringIndex(original, separator); + if (0 <= index) + { + string last = separator; + integer i; + + if (0 != index) // Check for a corner case. + result += llGetSubString(original, 0, index - 1); + if ((index + 2) < llStringLength(original)) + { + last = llGetSubString(original, index + 1, index + 1); + original = llGetSubString(original, index + 2, -1); + } + else + original = ""; + + for (i = 1; i < length; ++i) + { + string pattern = llList2String(values, i); + + if (llGetSubString(pattern, 0, 0) == last) + { + last = llGetSubString(pattern, 1, -1); + i = length; // A break statement would be nice. + } + } + result += last; + } + } + + return result + original; +} + +startMenu(key id, list input) +{ + key menuUser = NULL_KEY; + integer menuChannel = -1; + key menuScript = NULL_KEY; + integer menuHandle = 0; + integer menuType = INVENTORY_NONE; + string menuMessage = ""; + list menu = []; + list menuFilter = []; + integer menuPos = 0; + integer menuMaxPos = 1; +// list menuNames = []; + integer menuIndex = llGetListLength(menus); + + menuPos = 0; + menuScript = id; + menuUser = llList2Key(input, 0); + menuFilter = llParseStringKeepNulls(llList2String(input, 1), ["|"], []); + menuType = llList2Integer(menuFilter, 0); + if (llGetListLength(menuFilter) > 1) + menuFilter = llList2List(menuFilter, 1, -1); + else + menuFilter = []; + menuMessage = llList2String(input, 2); + menu = llList2List(input, 3, -1); + if (NULL_KEY == menuUser) + { + // Only add it if it's not there. + if (-1 == llListFindList(registeredMenus, [menuMessage + "|" + menuScript])) + registeredMenus += [menuMessage + "|" + menuScript]; + registeredMenus = llListSort(registeredMenus, 1, TRUE); + return; + } + else if (INVENTORY_NONE == menuType) + menuMaxPos = llGetListLength(menu); + else if (INVENTORY_ALL == menuType) // TODO - NONE and ALL are both -1. lol + menuMaxPos = llGetListLength(ANIMATIONS); + else + { + integer i; + integer j; + integer number = llGetInventoryNumber(menuType); // NOTE this may change while we are in the menus. + integer length = llGetListLength(menuFilter); + + menuMaxPos = 0; + menu= []; + for (i = 0; i < number; ++i) + { + string name = llGetInventoryName(menuType, i); + + if (length) + { + for (j = 0; j < length; ++j) + { + if (osRegexIsMatch(name, llList2String(menuFilter, j))) + { + ++menuMaxPos; + menu += name; + } + } + } + else + { + ++menuMaxPos; + menu += name; + } + } + } + menuChannel = (integer) (llFrand(10000) + 1000); + menuHandle = llListen(menuChannel, "", menuUser, ""); + menuMaxPos /= 9; + menuMaxPos *= 9; + ++menuMaxPos; + menus += [menuUser, menuChannel, menuScript, menuHandle, menuType, menuMessage, + llDumpList2String(menu, LIST_SEP), llDumpList2String(menuFilter, "|"), + menuPos, menuMaxPos, "", llGetTime()]; + showMenu(menuIndex); +} + +showMenu(integer menuIndex) +{ + key menuUser = llList2String (menus, menuIndex + MENU_USER); + integer menuChannel = llList2Integer(menus, menuIndex + MENU_CHANNEL); + key menuScript = llList2String (menus, menuIndex + MENU_SCRIPT); + integer menuHandle = llList2Integer(menus, menuIndex + MENU_HANDLE); + integer menuType = llList2Integer(menus, menuIndex + MENU_TYPE); + string menuMessage = llList2String (menus, menuIndex + MENU_MESSAGE); + list menu = llParseString2List(llList2String (menus, menuIndex + MENU_MENU), [LIST_SEP], []); + list menuFilter = llParseString2List(llList2String (menus, menuIndex + MENU_FILTER), ["|"], []); + integer menuPos = llList2Integer(menus, menuIndex + MENU_POS); + integer menuMaxPos = llList2Integer(menus, menuIndex + MENU_MAXPOS); + list menuNames = []; + + list thisMenu = []; + integer length; + integer i; + + for (i = 0; i < 12; ++i) + { + string name; + integer index; + + if (INVENTORY_NONE == menuType) + name = llList2String(menu, menuPos + i); + else if (INVENTORY_ALL == menuType) + name = llList2String(ANIMATIONS, menuPos + i); + else + name = llList2String(menu, menuPos + i); +// name = llGetInventoryName(menuType, menuPos + i); + + index = llSubStringIndex(name, "|"); + if (index != -1) + { + list parts = llParseStringKeepNulls(name, ["|"], []); + name = llGetSubString(llList2String(parts, 0) + " ", 0, 15) + + "|" + llList2String(parts, 1)+ "|" + llList2String(parts, 2); + } + if (llStringLength(name) > 24) + { + menuNames += [name]; + name = llGetSubString(name, 0, 23); + } + // TODO - Only allow blank ones for arbitrary menus, but screws with the showMenu() code. +// if ((INVENTORY_NONE == menuType) && ("" == name)) +// name = "."; + if ("" != name) + thisMenu += [name]; + } + + length = llGetListLength(thisMenu); + if ((12 > length) && (0 == menuPos)) + { + integer j = length % 3; + if (0 == j) + thisMenu += [".", "Exit", "."]; + else if (1 == j) + { + string last = llList2String(thisMenu, -1); + thisMenu = llDeleteSubList(thisMenu, -1, -1) + [last, "Exit", "."]; + } + else if (2 == j) + { + string penultimate = llList2String(thisMenu, -2); + string last = llList2String(thisMenu, -1); + thisMenu = llDeleteSubList(thisMenu, -2, -1) + [penultimate, "Exit", last]; + } + } + else if (9 >= length) + thisMenu += ["<<", "Exit", ">>"]; + else + thisMenu = llList2List(thisMenu, 0, 8) + ["<<", "Exit", ">>"]; + + // Re order them to make LSL happy. + for (i = 0; i < length; i += 3) + thisMenu = llListInsertList(llDeleteSubList(thisMenu, -3, -1), llList2List(thisMenu, -3, -1), i); + llDialog(menuUser, menuMessage, thisMenu, menuChannel); + menus = llListReplaceList(menus, + [ + menuUser, menuChannel, menuScript, menuHandle, menuType, menuMessage, + llDumpList2String(menu, LIST_SEP), llDumpList2String(menuFilter, "|"), + menuPos, menuMaxPos, llDumpList2String(menuNames, LIST_SEP), llGetTime() + ], menuIndex, menuIndex + MENU_STRIDE - 1); +} + +myListen(integer channel, string name, key id, string message) +{ + // check if this message came from an object or avatar, + // use the objects owner if it came from an object. + // Avatars own themselves. B-) + key realId = llGetOwnerKey(id); + integer menuIndex = llListFindList(menus, [realId, channel]); + + if (0 <= menuIndex) // Menu response + { + key menuUser = llList2String (menus, menuIndex + MENU_USER); + integer menuChannel = llList2Integer(menus, menuIndex + MENU_CHANNEL); + key menuScript = llList2String (menus, menuIndex + MENU_SCRIPT); + integer menuHandle = llList2Integer(menus, menuIndex + MENU_HANDLE); + integer menuType = llList2Integer(menus, menuIndex + MENU_TYPE); + string menuMessage = llList2String (menus, menuIndex + MENU_MESSAGE); + list menu = llParseString2List(llList2String (menus, menuIndex + MENU_MENU), [LIST_SEP], []); + list menuFilter = llParseString2List(llList2String (menus, menuIndex + MENU_FILTER), ["|"], []); + integer menuPos = llList2Integer(menus, menuIndex + MENU_POS); + integer menuMaxPos = llList2Integer(menus, menuIndex + MENU_MAXPOS); + list menuNames = llParseString2List(llList2String (menus, menuIndex + MENU_NAMES), [LIST_SEP], []); + integer delete = FALSE; + + if ("<<" == message) + { + menuPos -= 9; + if (menuPos < 0) + menuPos = menuMaxPos - 1; + } + else if (">>" == message) + { + menuPos += 9; + if (menuPos > (menuMaxPos - 1)) + menuPos = 0; + } + else if ("." == message) + delete = TRUE; + else + { + delete = TRUE; + if (menuHandle) + llListenRemove(menuHandle); + if (llStringLength(message) == 24) + { + integer i; + integer length = llGetListLength(menuNames); + + for (i = 0; i < length; ++i) + { + string lName = llList2String(menuNames, i); + + if (message == llGetSubString(lName, 0, 23)) + message = lName; + } + } + + if (NULL_KEY == menuScript) + { + menuScript = (key) llList2String(llParseStringKeepNulls(message, ["|"], []), 2); + message = ""; + } + llMessageLinked(LINK_SET, UTILITIES_MENU_DONE, llDumpList2String([menuUser, message], LIST_SEP), menuScript); + } + if (delete) + menus = llDeleteSubList(menus, menuIndex, menuIndex + MENU_STRIDE - 1); + else + { + menus = llListReplaceList(menus, + [ + menuUser, menuChannel, menuScript, menuHandle, menuType, menuMessage, + llDumpList2String(menu, LIST_SEP), llDumpList2String(menuFilter, "|"), + menuPos, menuMaxPos, llDumpList2String(menuNames, LIST_SEP), llGetTime() + ], menuIndex, menuIndex + MENU_STRIDE - 1); + showMenu(menuIndex); + } + } + else // Chat command. + { + list scripts = []; + list words = []; + string prefix = ""; + string command = ""; + string idChannel = (string) realId + (string) channel; + integer thisOwner = llListFindList(chatOwners, [idChannel]); + +//llOwnerSay("->>" + (string) channel + " " + message); + +//llOwnerSay("owners " + llDumpList2String(chatOwners, "^")); +//llOwnerSay("channels " + llDumpList2String(chatChannels, "^")); +//llOwnerSay("prefixes " + llDumpList2String(chatPrefixes, "^")); +//llOwnerSay("prefixes2 " + llDumpList2String(chatPrefixes2, "^")); +//llOwnerSay("commands " + llDumpList2String(chatCommands, "^")); + if (0 <= thisOwner) + scripts = llParseString2List(llList2String(chatOwners, thisOwner + 1), ["|"], []); + else + { + integer thisChannel = llListFindList(chatChannels, [(string) channel]); + + if (0 <= thisChannel) + scripts = llParseString2List(llList2String(chatChannels, thisChannel + 1), ["|"], []); + } +//llOwnerSay(llDumpList2String(scripts, "|")); + if ([] != scripts) + { + integer thisPrefix; + string candidate; + + words = llParseString2List(message, [" "], []); + candidate = llList2String(words, 0); + thisPrefix = llListFindList(chatPrefixes, [(string) channel + " " + candidate]); +//llOwnerSay(llDumpList2String(words, "~")); +//llSay(0, candidate); +//llSay(0, (string) thisPrefix); + if (0 <= thisPrefix) + { + prefix = candidate; + scripts = llParseString2List(llList2String(chatPrefixes, thisPrefix + 1), ["|"], []); + words = llList2List(words, 1, -1); + } + else + { + integer length = llGetListLength(chatPrefixes2); + integer i; + + for (i = 0; i < length; i += 2) + { + string pName = llList2String(chatPrefixes2, i); + integer pLength = llStringLength(pName); + + if (pName == ((string) channel + " " + llGetSubString(candidate, 0, pLength))) + { +// prefix = pName; + prefix = candidate; + scripts = llParseString2List(llList2String(chatPrefixes2, i + 1), ["|"], []); +// words = [llGetSubString(candidate, pLength + 1, -1)] + llList2List(words, 1, -1); + words = llList2List(words, 1, -1); + i = length; + } + } + } + } +//llOwnerSay(llDumpList2String(scripts, "|")); +//llSay(0, prefix); + + // Finally found it, process it. + if ([] != scripts) + { + integer length = llGetListLength(scripts); + integer wordLength = llGetListLength(words); + integer i; + + // Wont put up with laggy scripts. Use a prefix if you insist on using local chat. +// if ((0 == channel) && ("" == prefix)) +// return; + + // The problem with this loop is that if a bunch of scripts are wanting the same commands, + // then it's not efficient. In the expected use cases, that should not me much of a problem. + for (i = 0; i < length; ++i) + { + string script = llList2String(scripts, i); + integer theseCommands = llListFindList(chatCommands, [script + (string) channel]); + + if (0 <= theseCommands) + { + list commands = llParseStringKeepNulls(llList2String(chatCommands, theseCommands + 1), ["|"], []); + integer thisCommand = llListFindList(commands, [llList2String(words, 0)]); +//llOwnerSay(llDumpList2String(words, "~")); +//llOwnerSay(llDumpList2String(commands, "~")); +//llSay(0, (string) thisCommand); + + if (0 != (thisCommand % 2)) + thisCommand = -1; + if (0 <= thisCommand) + { + list result = [channel, name, realId, message, prefix, llList2String(commands, thisCommand)]; + string argsTypes = llList2String(commands, thisCommand + 1); + integer argsLength = llStringLength(argsTypes); + integer required = 0; + integer optional = 0; + integer multiple = -1; +// integer oldStyle = FALSE; + + if (0 < argsLength) // Arguments expected. + { + required = (integer) llGetSubString(argsTypes, 0, 0); + optional = (integer) llGetSubString(argsTypes, 1, 1); + + { + list arguments = []; // type, required, extra + integer a; + integer argsCount = 0; + integer w = 1; + + for (a = 0; a < argsLength; a++) + { + string type = llGetSubString(argsTypes, a, a); + string TYPE = llToUpper(type); + integer isNeeded = (TYPE == type); + string extra = ""; + + if (isNeeded) + ++required; + else + ++optional; + // Sort out the extra string for those that support it. + if (("C" == TYPE) || ("X" == TYPE)) + { + string subbie = llGetSubString(argsTypes, a + 1, -1); + integer comma = llSubStringIndex(subbie, ","); + + if (-1 == comma) + { + extra = llGetSubString(argsTypes, a + 1, -1); + a = argsLength; + } + else + { + extra = llGetSubString(argsTypes, a + 1, a + 1 + comma - 1); + a += comma + 1; + } + } + arguments += [TYPE, isNeeded, extra]; + ++argsCount; + } + for (a = 0; a < argsCount; ++a) + { + string TYPE = llList2String (arguments, (a * 3) + 0); + integer isNeeded = llList2Integer(arguments, (a * 3) + 1); + string extra = llList2String (arguments, (a * 3) + 2); + string value = ""; + + // a animation, b bodypart, clothing, g gesture, m landmark, o object, sound, t texture. + // c notecard, x script, Extension is what follows up to the next comma, or end of line. + // f float, i integer, k key, r rotation, s rest of string, v vector. + // n name of avatar or NPC. Full two word variety, no other checking done. + // l local name, could be just first or last name, but they need to be in the sim. + if ("F" == TYPE) + value = (string) llList2Float(words, w); + else if ("I" == TYPE) + value = (string) llList2Integer(words, w); + else if ("K" == TYPE) + value = (key) llList2String(words, w); + else if (("R" == TYPE) || ("V" == TYPE)) + { + string next = llList2String(words, w); + + if ("<" == llGetSubString(next, 0, 0)) + { + integer l; + + for (l = 0; (w + l) < wordLength; ++l) + { + value += next; + if (">" == llGetSubString(value, -1, -1)) + { + w += l; + l = wordLength; // BREAK! + } + if (("V" == TYPE) && (3 <= l)) + l = wordLength; // BREAK! + else if (("R" == TYPE) && (4 <= l)) + l = wordLength; // BREAK! + next = llList2String(words, w + 1 + l); + } + if (">" != llGetSubString(value, -1, -1)) // Seems OpenSim at least can cast partial vectors. + value = ""; + if ("V" == TYPE) + { + vector v = (vector) value; + value = (string) v; + } + else if ("R" == TYPE) + { + rotation r = (rotation) value; + value = (string) r; + } + } + } + else if ("L" == TYPE) + { + string first = llList2String(words, w); + string last = llList2String(words, w + 1); + + if (osIsUUID(first)) + value = first; + else if ("" != last) + { + list avatars = [llGetOwner(), ZERO_VECTOR, llKey2Name(llGetOwner())] + osGetAvatarList(); // Strided list, UUID, position, name. + integer avaLength = llGetListLength(avatars); + string aName = llToLower(first + " " + last); + integer n; + + for (n = 0; n < avaLength; n +=3) + { + if (llToLower(llList2String(avatars, n + 2)) == aName) + { + value = llKey2Name(llList2String(avatars, n)); + ++w; + n = avaLength; // BREAK! + } + } + } + if (("" == value) && ("" != first)) // Try scanning the sim for a matching first or last name. + { + list candidates = []; + list avatars = [llGetOwner(), ZERO_VECTOR, llKey2Name(llGetOwner())] + osGetAvatarList(); // Strided list, UUID, position, name. + integer avaLength = llGetListLength(avatars); + integer n = llSubStringIndex(first, "'"); + + // Check if we are searching for multiples. + // There can be only one multiple, and it should be the first, + // so skip multiples checking if multiple is set already. + if ((-1 != n) && (-1 == multiple)) + { + multiple = a; + first = llGetSubString(first, 0, n - 1); + } + last = llToLower(first); + for (n = 0; n < avaLength; n +=3) + { + list names = llParseString2List(llToLower(llList2String(avatars, n + 2)), [" "], []); + + if ((llList2String(names, 0) == last) || (llList2String(names, 1) == last)) + candidates += [llList2String(avatars, n)]; + } + avaLength = llGetListLength(candidates); + if (0 == avaLength) + llSay(0, "No one matching the name " + first + " here."); + else if ((1 < avaLength) && (-1 == multiple)) + llSay(0, "More than one matching the name " + first + " here."); + else if (-1 != multiple) + value = llDumpList2String(candidates, "|"); + else + value = llKey2Name(llList2String(candidates, 0)); + } + } + else if ("N" == TYPE) + { + string first = llList2String(words, w); + string last = llList2String(words, w + 1); + + if (osIsUUID(first)) + value = first; + else if ("" != last) + { + value = first + " " + last; + ++w; + } + } + else // The rest are "rest of string". + { + if (w < wordLength) + result += [llDumpList2String(llList2List(words, w, -1), " ")]; + w = wordLength; + } + result += [value]; + ++w; + if (w > wordLength) + a = argsCount; // BREAK! + } + + // Put the rest of the words back together as "rest of message". + if (w < wordLength) + result += [llDumpList2String(llList2List(words, w, -1), " ")]; +//llSay(0, "ARGUMENTS for " + llList2String(words, 0) + " = " + llDumpList2String(arguments, "|")); +//llSay(0, "RESULTS " + llDumpList2String(result, "~")); + } + } + + if ((1 + required) > wordLength) + { + // bitch + if (id != realId) + llInstantMessage(realId, "Not enough required arguments in a command from your object " + llKey2Name(id) + " The command was - " + message); + else + llInstantMessage(realId, "Not enough required arguments in your command. The command was - " + message); + } + else + { + // RETURNS incoming channel | incoming name | incoming key | incoming message | prefix | command | list of arguments | rest of message + if (-1 != multiple) + { + list candidates = llParseString2List(llList2String(result, 6 + multiple), ["|"], []); + integer candiLength = llGetListLength(candidates); + integer c; + + for (c = 0; c < candiLength; ++c) + { + result = llListReplaceList(result, [llList2String(candidates, c)], 6 + multiple, 6 + multiple); + llMessageLinked(LINK_SET, UTILITIES_CHAT_DONE, llDumpList2String(result, LIST_SEP), (key) script); + } + } + else + llMessageLinked(LINK_SET, UTILITIES_CHAT_DONE, llDumpList2String(result, LIST_SEP), (key) script); + } + } + } + } + } + } +} + +startNextRead() +{ + if (0 < llGetListLength(settingsCards)) + { + settingsName = llList2String(settingsCards, 0); + settingsKey = llList2Key(settingsCards, 1); + settingsLine = 0; + settingsQueryID = llGetNotecardLine(settingsName, settingsLine); // request first line + settingsCards = llDeleteSubList(settingsCards, 0, 1); + } + else + { + settingsName = ".settings"; + settingsKey = NULL_KEY; + settingsQueryID = NULL_KEY; + } +} + +list readThisLine(string data) +{ + list result = []; + + data = llStringTrim(data, STRING_TRIM); + if ((0 < llStringLength(data)) && ("#" != llGetSubString(data, 0, 0))) + { + list commands = llParseStringKeepNulls(data, [";"], []); + list new = []; + string newCommand = ""; + integer length = llGetListLength(commands); + integer i; + + for (i = 0; i < length; ++i) + { + string command = llList2String(commands, i); + + // Check for line continuation. I think. lol + if ("\\" == llGetSubString(command, -1, -1)) + newCommand += llGetSubString(command, 0, -2) + ";"; + else + { + command = llStringTrim(newCommand + command, STRING_TRIM); + if (0 < llStringLength(command)) + new += [command]; + newCommand = ""; + } +//llOwnerSay("|" + newCommand + "|" + command + "|"); + } + + length = llGetListLength(new); + for (i = 0; i < length; ++i) + { + string name; + string value = llList2String(new, i); + integer equals = llSubStringIndex(value, "="); + + name = ""; + if (0 <= equals) + { + name = llStringTrim(llGetSubString(value, 0, equals - 1), STRING_TRIM_TAIL); + if ((equals + 1) < llStringLength(value)) + value = llStringTrim(llGetSubString(value, equals + 1, -1), STRING_TRIM_HEAD); + else + value = ""; + } + else + { + name = value; + value = ""; + } + result += [name, value]; + } + } + ++settingsLine; + return result; +} + +init() +{ + llMessageLinked(LINK_SET, UTILITIES_RESET_DONE, "", llGetInventoryKey(llGetScriptName())); + // Pointless in OpenSim, always reports 16384. Pffft + //llOwnerSay("Free memory " + (string) llGetFreeMemory() + " in " + llGetScriptName()); + llSetTimerEvent(MENU_TIMEOUT); +} + + +default +{ + state_entry() + { + init(); + } + + on_rez(integer param) + { + init(); + } + + attach(key attached) + { + init(); + } + + listen(integer channel, string name, key id, string message) + { + myListen(channel, name, id, message); + } + + // Handle commands from other scripts. + // Negative odd values of num are the commands, return num - 1 as the result. + link_message(integer sender_num, integer num, string value, key id) + { + list input = llParseStringKeepNulls(value, [LIST_SEP], []); + + if (UTILITIES_RESET == num) // Request stuff to be reset + { + if ("reset" == value) // Request us to be reset. + llResetScript(); + resetPrimShit(); + llMessageLinked(LINK_SET, num - 1, value, id); + } + else if (UTILITIES_READ == num) // Request a notecard to be read. + { + list result = []; + integer length = osGetNumberOfNotecardLines(value); + + settingsLine = 0; + while (settingsLine <= length) + { + result += readThisLine(osGetNotecardLine(value, settingsLine)); + if (0 == (settingsLine % 500)) // Don't send too many at once. + { + integer percent = (integer) ((((float) settingsLine) / ((float) length)) * 100.0); + + llSay(0, "Reading '" + value + "' " + (string) percent + "% done."); + // Sending a negative line to try to avoid triggering foreign scripts. + // Sending from -1000 downwards to avoid triggering our scripts. + llMessageLinked(LINK_SET, -1000 - settingsLine, llDumpList2String([value] + result, LIST_SEP), id); + result = []; + } + } + // Send the last batch. + if (0 != llGetListLength(result)) + llMessageLinked(LINK_SET, -1000 - settingsLine, llDumpList2String([value] + result, LIST_SEP), id); + llMessageLinked(LINK_SET, UTILITIES_READ_DONE, value, id); + } + else if (UTILITIES_SUBSTITUTE == num) // Request a param substitution. + { + llMessageLinked(LINK_SET, num - 1, substitute([substitute(input, "%"), "n\n", "t\t", "\\\\", "\"\""], "\\"), id); + } + else if (UTILITIES_NEXT_WORD == num) // Get the next word + { + llMessageLinked(LINK_SET, num - 1, llDumpList2String(nextWord(llList2String(input, 0), llList2String(input, 1), llList2String(input, 2)), LIST_SEP), id); + } + else if (UTILITIES_MENU == num) // Request big menu to be displayed + { + startMenu(id, input); + } + else if (UTILITIES_CHAT == num) + { + // channel list | owner list | prefix list | command list + list channels = llParseString2List(llList2String(input, 0), ["|"], []); + list owners = llParseString2List(llList2String(input, 1), ["|"], []); + list prefixes = llParseString2List(llList2String(input, 2), ["|"], []); + string commands = llList2String(input, 3); + integer chLength = llGetListLength(channels); + integer i; + + for (i = 0; i < chLength; ++i) + { + string channel = llList2String(channels, i); + integer j; + + if ("" != commands) + { +//llSay(0, "ADDING " + channel + "= " + commands); + integer oLength = llGetListLength(owners); + integer pLength = llGetListLength(prefixes); + integer found = llListFindList(channelHandles, [channel]); + + if ("0" == channel) + llOwnerSay("WARNING: Script using local chat for commands may cause lag - " + llKey2Name(id)); + chatChannels = addChatScripts(chatChannels, channel, (string) id, 1); + chatCommands = addChatScripts(chatCommands, (string) id + channel, commands, 2); + for (j = 0; j < oLength; ++j) + chatOwners = addChatScripts(chatOwners, llList2String(owners, j) + channel, (string) id, 1); + + for (j = 0; j < pLength; ++j) + { + string prefix = llList2String(prefixes, j); + + if (" " == llGetSubString(prefix, -1, -1)) + chatPrefixes = addChatScripts(chatPrefixes, channel + " " + llGetSubString(prefix, 0, -2), (string) id, 1); + else + chatPrefixes2 = addChatScripts(chatPrefixes2, channel + " " + prefix, (string) id, 1); + } + + // Do this last, so all the rest is setup already. + if (0 > found) + { +//llOwnerSay("LISTEN " + channel + " " + llList2String(owners, j)); +// TODO - Closing and reopening that channel if details change. + // NOTE - only the FIRST owner is supported. + // TODO - this is not right anyway. + // If a channel gets more than one owner from different invocations of this command, + // then it should re open the listener. + if (1 == oLength) + channelHandles += [channel, llListen((integer) channel, "", llList2Key(owners, 0), "")]; + else + channelHandles += [channel, llListen((integer) channel, "", NULL_KEY, "")]; + } + } + else // if ("" != commands) + { + integer index = llListFindList(chatCommands, [(string) id + channel]); + // Yes, I know, UUIDs are a fixed length, but there's talk of using SHA1 hashes instead. + integer keyLength = llStringLength((string) id); + integer length; + + chatChannels = delChatScripts(chatChannels, channel, (string) id, 1); + if (0 <= index) + chatCommands = llDeleteSubList(chatCommands, index, index + 1); + + length = llGetListLength(chatOwners); + for (j = 0; j < length; j += 2) + { + string this = llList2String(chatOwners, j); + + if (llGetSubString(this, keyLength, -1) == channel) + chatOwners = delChatScripts(chatOwners, this, (string) id, 1); + } + +// TODO - go through chatPrefixes/2 removing script from any on this channel + + if (0 > llListFindList(chatChannels, [channel])) + { + length = llGetListLength(channelHandles); + for (j = 0; j < length; j += 2) + { + if (llList2String(channelHandles, j) == channel) + { + llListenRemove(llList2Integer(channelHandles, j + 1)); + channelHandles = llDeleteSubList(channelHandles, j, j + 1); + length -= 2; + j -= 2; + } + } + } +//llOwnerSay("owners " + llDumpList2String(chatOwners, "^")); +//llOwnerSay("channels " + llDumpList2String(chatChannels, "^")); +//llOwnerSay("prefixes " + llDumpList2String(chatPrefixes, "^")); +//llOwnerSay("prefixes2 " + llDumpList2String(chatPrefixes2, "^")); +//llOwnerSay("commands " + llDumpList2String(chatCommands, "^")); + } // if ("" != commands) + } // for (i = 0; i < chLength; ++i) + + } + else if (UTILITIES_CHAT_FAKE == num) + { + myListen(llList2Integer(input, 0), llList2String(input, 1), llList2Key(input, 2), llList2String(input, 3)); + } + } + + touch_start(integer num) + { + integer length = llGetListLength(registeredMenus); + integer i; + + // Scan through the list, checking if those scripts still exist in inventory. + for (i = length - 1; i >= 0; --i) + { + string this = llList2String(llParseStringKeepNulls(llList2String(registeredMenus, 0), ["|"], []), 1); + + if (INVENTORY_NONE == llGetInventoryType(this)) + { + registeredMenus = llDeleteSubList(registeredMenus, i, i); + --length; + } + } + for (i = 0; i < num; ++i) + { + key id = llDetectedKey(i); + string desc = llList2String(llGetObjectDetails(llGetLinkKey(llDetectedLinkNumber(i)), [OBJECT_DESC]), 0); + + // If there's a description, then it's likely a scriptlet. + if ("" != desc) + myListen(0, llKey2Name(id), id, desc); // TODO - the problem here is that the first argument is a channel, + // and we don't know which channel to fake. + // Maybe use the debug channel as a wildcard? + else if (1 == length) // Only one registered, select it directly. + { + llMessageLinked(LINK_SET, UTILITIES_MENU_DONE, llDumpList2String([id, ""], LIST_SEP), + llList2String(llParseStringKeepNulls(llList2String(registeredMenus, 0), ["|"], []), 2)); + } + else if (0 != length) // More than one, put up a menu of them. + startMenu(NULL_KEY, [id, INVENTORY_NONE, "Choose a function :"] + registeredMenus); + // If there's zero registered menus, then do nothing. + } + } + + timer() + { + integer length = llGetListLength(menus); + float time = llGetTime(); + integer i; + + // Run through menus, removing any that timed out. + for (i = 0; i < length; i += MENU_STRIDE) + { + if (time > (llList2Float(menus, i + MENU_TIME) + MENU_TIMEOUT)) + { + integer menuHandle = llList2Integer(menus, i + MENU_HANDLE); + + llSay(0, "Menu for " + llKey2Name(llList2String(menus, i + MENU_USER)) + " timed out."); + if (menuHandle) + llListenRemove(menuHandle); + menus = llDeleteSubList(menus, i, i + MENU_STRIDE - 1); + length = llGetListLength(menus); + i -= MENU_STRIDE; + } + } + } +} diff --git a/onefang's_utilities_manual.txt b/onefang's_utilities_manual.txt new file mode 100644 index 0000000..dcb4f9a --- /dev/null +++ b/onefang's_utilities_manual.txt @@ -0,0 +1,228 @@ +A bunch of stuff copied from the script, since it's too close to the 64Kb limit. +Below the license is some rough instructions. + +// onefang's utilites version 3.0 +// Read a complete settings notecard and send settings to other scripts. +// Also other useful functions. + +// Copyright (C) 2007 David Seikel (onefang rejected). +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies of the Software and its Copyright notices. In addition publicly +// documented acknowledgment must be given that this software has been used if no +// source code of this software is made available publicly. This includes +// acknowledgments in either Copyright notices, Manuals, Publicity and Marketing +// documents or any documentation provided with any product containing this +// software. This License does not apply to any software that links to the +// libraries provided by this software (statically or dynamically), but only to +// the software provided. +// +// Please see the COPYING-PLAIN for a plain-english explanation of this notice +// and it's intent. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// As a special exception to the above conditions, the Second Life user known +// as Winter Ventura may ignore all permissions and conditions and provide her +// own. + +// All scripts in this object will get link messages with - +// num = -1000 - line number in the notecard. +// message = a list, +// settings card name, +// name of this setting, +// data for this setting (may be ""). +// id = scriptKey as passed to us by the calling script. +// Settings are in the format "setting=data" with whitespace ignored. +// # as first non space character means ignore the line. +// For best efficiency, keep notecards short, 0.1 seconds per line read. +// You can seperate settings with a ";", and include a literal ";" with "\;". +// Objects using this must be tolerant of having their settings passed +// to them at any time, even multiple times. +// Other scripts should ignore settings that are meaningless to them, +// but can complain if settings they know about are broken. +// +// Other commands are passed in link messages in the form - +// llMessageLink(LINK_SET, UTILITIES_COMMAND, llDumpList2String(argumentList, LIST_SEP), scriptKey); +// UTILITIES_COMMAND is one of the commands listed below. +// argumentList is a list of the arguments for the command. +// For those commands with one or less arguments, a dumped list is not needed. +// scriptKEy is llGetInventoryKey(llGetScriptName()) from the caller script. +// It's used by the caller to make sure it gets it's returned message and not that of other scripts. + +// llGetNotecardLine() delays for 0.1 seconds, truncates at 255 chars, no warning, 64KiB notecard limit. +// llResetOtherScript("SettingsReaderAndUtilities") to restart this from another script. + +// TODO (LSL permitting) - +// Use numbers for the returned chat commands. +// These numbers are set by the caller. +// They only should set a base number, that gets incremented for the other commands. +// Performance monitor. +// onefangs special boolean parser, put it in there somewhere. +// More complex parsing - +// No = required, just parse first word as key, rest as data. +// Does not work so well for "Avatar Name=key". +// Other comment types, embedded comments. + +// type channel owner prefix commands menu scripts users +// emoter 12+123 only none no 1 owner +// translator 1+2+3 only none no 1 owner +// online HUD 5 only fixed+ no 1 owner +// online IRC 0 +IRC fixed+ no 1 owner+IRC nick +// hug 1 only config later 1 owner+hugee +// TeddyPorter 2 owners TeddyPorter fixed+ yes 1+ owners+bookers+occupier+group+users +// collar 0+x owners *|an per script yes several owner+secowners+group+sub+everyone + +// Online IRC is TODO'd to move away from the need to listen to local chat. +// Collars are icky, but mired in historical precedence. +// TeddyPorter is the only other one with an open listener, and it uses a prefix to filter because of that. + + + +integer isKey(key thisKey) +{//by: Strife Onizuka + if (thisKey) return 2; // key is valid AND not equal NULL_KEY; the distinction is important in some cases (return value of 2 is still evaluated as unary boolean TRUE) + return (thisKey == NULL_KEY); // key is valid AND equal to NULL_KEY (return 1 or TRUE), or is not valid (return 0 or FALSE) +} + +key forceKey(key thisKey) +{//force a string or key to be a valid key assuming you want invalids to become NULL_KEY + if (thisKey) return thisKey; + return NULL_KEY; +} + +// If the above key checking turns out to be wrong, do it the hard way. +integer isKeyHard(key thisKey) +{ + integer i; + + if (llStringLength(thisKey) != 36) + return FALSE; + // Hyphenation tests: + if (llGetSubString(thisKey, 8, 8) != "-") + return FALSE; + if (llGetSubString(thisKey, 13, 13) != "-") + return FALSE; + if (llGetSubString(thisKey, 18, 18) != "-") + return FALSE; + if (llGetSubString(thisKey, 23, 23) != "-") + return FALSE; + // Hex test: + // Remove dashes (fixed, thanks Kek :-)) + thisKey = llDeleteSubString(llDeleteSubString(llDeleteSubString(llDeleteSubString((string) thisKey, 23, 23), 18, 18), 13, 13), 8, 8); + + for (i = 0; i < 32; ++i) + { + string char = llGetSubString(thisKey, i, i); + + if ((0 == ((integer) ("0x" + char))) && ("0" != char)) + return FALSE; + } + return TRUE; // Passed all tests: +} + +// Send an avatar key request to the server. +addKeyRequest(key script, string type, string name, string extra) +{ + keyRequests += [script, type, name, extra, llHTTPRequest("http://w-hat.com/name2key?terse=1&name=" + llEscapeURL(name), [], "")]; +} + + + + + // Check if anything changed. + // Including - adding inv, deleting inv, change name or desc of inv, saving notecard, recompiling script. + // Not including - script reset, no-copy inv is dragged out, inv drop by non owner. + //changed(integer change) + //{ + //if (CHANGED_INVENTORY & change) + //init(settingsName); + //} + + // Deal with each notecard line. + dataserver(key query_id, string data) + { + if (query_id == settingsQueryID) + { + if (data != EOF) + { + readThisLine(data); + settingsQueryID = llGetNotecardLine(settingsName, settingsLine); + } + else + { + llMessageLinked(LINK_SET, UTILITIES_READ_DONE, settingsName, settingsKey); + startNextRead(); + } + } + } + + http_response(key id, integer status, list meta, string body) + { + integer i; + integer length = llGetListLength(keyRequests) / KEYS_STRIDE; + + for (i = 0; i <= length; ++i) + { + integer thisRequest = i * KEYS_STRIDE; + + if ((NULL_KEY != id) && (llList2Key(keyRequests, thisRequest + KEYS_ID) == id)) + { + string script = llList2String(keyRequests, thisRequest + KEYS_SCRIPT); + string type = llList2String(keyRequests, thisRequest + KEYS_TYPE); + string name = llList2String(keyRequests, thisRequest + KEYS_NAME); + string extra = llList2String(keyRequests, thisRequest + KEYS_EXTRA); + key result = (key) body; + + keyRequests = llDeleteSubList(keyRequests, thisRequest, thisRequest + KEYS_STRIDE - 1); + i -= KEYS_STRIDE; + length -= KEYS_STRIDE; + + if (status == 499) + { + llOwnerSay("name2key request timed out for " + name + ". Trying again."); + addKeyRequest(script, type, name, extra); + } + else if (status != 200) + { + llOwnerSay("The internet exploded!! Trying again."); + addKeyRequest(script, type, name, extra); + } + else if ((!isKey(result)) || ("" == body)) + llOwnerSay("No key found for " + name); + else if (36 != llStringLength(body)) + llOwnerSay("Server broken for " + name); + else + llMessageLinked(LINK_SET, UTILITIES_AVATAR_KEY_DONE, llDumpList2String([type, name, result, extra], LIST_SEP), script); + } + } + } + + + else if (UTILITIES_AVATAR_KEY == num) + { + string type = llList2String(input, 0); + string name = llList2String(input, 1); + string extra = llList2String(input, 2); + key result = NULL_KEY; + +// TODO add a check for the key already existing in name. Should be done by caller if caller worries about speed. + + if (NULL_KEY == result) + addKeyRequest(id, type, name, extra); + else + llMessageLinked(LINK_SET, num - 1, llDumpList2String([type, name, result, extra], LIST_SEP), id); + } + -- cgit v1.1