aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorDavid Walter Seikel2014-03-02 22:34:54 +1000
committerDavid Walter Seikel2014-03-02 22:34:54 +1000
commit9c9a333860debf85b403a79bd6ee93b4f6bc5ded (patch)
treea04fe89e2467782129dc6aba7b82d7e8cf57e97a
parentInitial commit (diff)
downloadNPC-tool-9c9a333860debf85b403a79bd6ee93b4f6bc5ded.zip
NPC-tool-9c9a333860debf85b403a79bd6ee93b4f6bc5ded.tar.gz
NPC-tool-9c9a333860debf85b403a79bd6ee93b4f6bc5ded.tar.bz2
NPC-tool-9c9a333860debf85b403a79bd6ee93b4f6bc5ded.tar.xz
Initial import of everything.
-rw-r--r--COPYING-PLAIN42
-rw-r--r--LICENSE27
-rw-r--r--NPC_menu.lsl481
-rw-r--r--NPC_tool.lsl1291
-rw-r--r--NPC_tool_help.txt171
-rw-r--r--README.md5
-rw-r--r--locator_beacon_script.lsl130
-rw-r--r--onefang's_utilities.lsl1189
-rw-r--r--onefang's_utilities_manual.txt228
9 files changed, 3533 insertions, 31 deletions
diff --git a/COPYING-PLAIN b/COPYING-PLAIN
new file mode 100644
index 0000000..dc18261
--- /dev/null
+++ b/COPYING-PLAIN
@@ -0,0 +1,42 @@
1Plain English Copyright Notice
2
3This file is not intended to be the actual License. The reason this
4file exists is that we here are programmers and engineers. We aren't
5lawyers. We provide licenses that we THINK say the right things, but we
6have our own intentions at heart. This is a plain-english explanation
7of what those intentions are, and if you follow them you will be within
8the "spirit" of the license.
9
10The intent is for us to enjoy writing software that is useful to us (the
11AUTHORS) and allow others to use it freely and also benefit from the
12work we put into making it. We don't want to restrict others using it.
13They should not *HAVE* to make the source code of the applications they
14write that simply link to these libraries (be that statically or
15dynamically), or for them to be limited as to what license they choose
16to use (be it open, closed or anything else). But we would like to know
17you are using these libraries. We simply would like to know that it has
18been useful to someone. This is why we ask for acknowledgement of some
19sort.
20
21You can do what you want with the source of this software - it doesn't
22matter. We still have it here for ourselves and it is open and free to
23use and download and play with. It can't be taken away. We don't
24really mind what you do with the source to your software. We would
25simply like to know that you are using it - especially if it makes it to
26a commerical product. If you simply contact all the AUTHORS (see below)
27telling us, and then make sure you include a paragraph or page in the
28manual for the product with the copyright notice and state that you used
29this software, we will be very happy. If you want to contribute back
30modifications and fixes you may have made we will welcome those too with
31open arms (generally). If you want help with changes needed, ports
32needed or features to be added, arrangements can be easily made with
33some dialogue.
34
35This is a Second Life script, "simply link to these libraries" means
36including the script in an object and making the script no-modify, or
37otherwise rendering the source code unreadable. Any use of this script
38that makes the source code readable must include the License unmodified
39at the top of the source code.
40
41David Seikel (Second Life user onefang Rejected).
42
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index b5dfce3..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,27 +0,0 @@
1Copyright (c) 2014, David Seikel
2All rights reserved.
3
4Redistribution and use in source and binary forms, with or without modification,
5are permitted provided that the following conditions are met:
6
7* Redistributions of source code must retain the above copyright notice, this
8 list of conditions and the following disclaimer.
9
10* Redistributions in binary form must reproduce the above copyright notice, this
11 list of conditions and the following disclaimer in the documentation and/or
12 other materials provided with the distribution.
13
14* Neither the name of the {organization} nor the names of its
15 contributors may be used to endorse or promote products derived from
16 this software without specific prior written permission.
17
18THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27SOFTWARE, 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 @@
1// Onefang's NPC menu version 1.0.
2// Requires NPC tool, and onefang's utilities scripts.
3
4// Copyright (C) 2013 David Seikel (onefang rejected).
5//
6// Permission is hereby granted, free of charge, to any person obtaining a copy
7// of this software and associated documentation files (the "Software"), to
8// deal in the Software without restriction, including without limitation the
9// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10// sell copies of the Software, and to permit persons to whom the Software is
11// furnished to do so, subject to the following conditions:
12//
13// The above copyright notice and this permission notice shall be included in
14// all copies of the Software and its Copyright notices. In addition publicly
15// documented acknowledgment must be given that this software has been used if no
16// source code of this software is made available publicly. This includes
17// acknowledgments in either Copyright notices, Manuals, Publicity and Marketing
18// documents or any documentation provided with any product containing this
19// software. This License does not apply to any software that links to the
20// libraries provided by this software (statically or dynamically), but only to
21// the software provided.
22//
23// Please see the COPYING-PLAIN for a plain-english explanation of this notice
24// and it's intent.
25//
26// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
29// THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
30// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
31// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32
33list chatters = []; // npc key, relay channel.
34integer CHAT_KEY = 0;
35integer CHAT_CHANNEL = 1;
36integer CHAT_STRIDE = 2;
37
38list sensorRequests = []; // name to search for, type of search, range, command, user key, npc key, title for menu, agents flag, every flag
39integer sensorInFlight = FALSE;
40
41// NPC commands.
42string NPC_ANIMATE = "animate"; // NPC name, animation name
43string NPC_ATTENTION= "attention"; // NPC name.
44string NPC_CHANGE = "change"; // NPC name, NPC (notecard) name.
45string NPC_CLONE = "clone"; // Avatar's name.
46string NPC_COME = "come"; // NPC name.
47string NPC_COMPLETE = "complete"; // NPC name.
48string NPC_CREATE = "create"; // NPC (notecard) name, (optional) position vector.
49string NPC_DELETE = "delete"; // NPC name.
50string NPC_DISMISSED= "dismissed"; // NPC name.
51string NPC_FOLLOW = "follow"; // NPC name, (optional) distance (float or vector).
52string NPC_FLY = "fly"; // NPC name, position vector or object/agent name/key.
53string NPC_GO = "go"; // NPC name, position vector or object/agent name/key.
54string NPC_LAND = "land"; // NPC name, position vector or object/agent name/key.
55string NPC_LINK = "link"; // Link number, number, string, (optional) key
56string NPC_LISTEN = "listen"; // new command channel
57string NPC_LOCATE = "locate"; // NPC name
58string NPC_NUKE = "nuke"; // No arguments.
59string NPC_ROTATE = "rotate"; // NPC name, Z rotation in degrees.
60string NPC_SAY = "say"; // NPC name, thing to say, or relay channel.
61string NPC_SCRIPT = "script"; // Script notecard name.
62string NPC_SHOUT = "shout"; // NPC name, thing to shout, or relay channel.
63string NPC_SIT = "sit"; // NPC name, object to sit on.
64string NPC_SLEEP = "sleep"; // Seconds.
65string NPC_STALK = "stalk"; // NPC name, avatar name, (optional) distance (float or vector).
66string NPC_STAND = "stand"; // NPC name.
67string NPC_STOP = "stop"; // NPC name.
68string NPC_STOPANIM = "stopanim"; // NPC name, animation name
69string NPC_TOUCH = "touch"; // NPC name, object to touch.
70string NPC_WHISPER = "whisper"; // NPC name, thing to whisper, or relay channel.
71
72string NPC_MAIN = "Main"; // TODO - does not really matter what this is I think, so use it as the registered menu button.
73string NPC_PICK = "Pick";
74string NPC_NPC = "NPC";
75string NPC_RELAY = "Relay";
76
77string NPC_NPC_EXT = ".avatar";
78string NPC_SCRIPT_EXT = ".npc";
79string NPC_BACKUP_CARD = "Restore";
80string NPC_RECORD_CARD = "Recorded";
81
82integer NPC_RECORD = -100;
83integer NPC_RECORD_STOP = -101;
84integer NPC_NEW_NPC = -102;
85integer NPC_ADD_CHATTER = -103;
86integer NPC_DEL_CHATTER = -104;
87
88
89// Stuff for onefangs common utilities.
90key NPCscriptKey;
91key scriptKey;
92
93string UTILITIES_SCRIPT_NAME = "onefang's utilities";
94string LIST_SEP = "$!#"; // Used to seperate lists when sending them as strings.
95integer UTILITIES_RESET = -1;
96integer UTILITIES_RESET_DONE = -2;
97integer UTILITIES_READ = -3;
98integer UTILITIES_READ_DONE = -4;
99integer UTILITIES_SUBSTITUTE = -5;
100integer UTILITIES_SUBSTITUTE_DONE = -6;
101integer UTILITIES_NEXT_WORD = -7;
102integer UTILITIES_NEXT_WORD_DONE = -8;
103integer UTILITIES_PAYMENT_MADE = -9;
104integer UTILITIES_PAYMENT_MADE_DONE = -10;
105integer UTILITIES_MENU = -11;
106integer UTILITIES_MENU_DONE = -12;
107integer UTILITIES_WARP = -13;
108integer UTILITIES_WARP_DONE = -14;
109integer UTILITIES_AVATAR_KEY = -15;
110integer UTILITIES_AVATAR_KEY_DONE = -16;
111integer UTILITIES_CHAT = -17;
112integer UTILITIES_CHAT_DONE = -18;
113integer UTILITIES_CHAT_FAKE = -19;
114integer UTILITIES_CHAT_FAKE_DONE = -20;
115
116
117showMenu(key id, string type, string title, list buttons, string extra)
118{
119 llMessageLinked(LINK_SET, UTILITIES_MENU, llDumpList2String([id, type, title] + buttons, LIST_SEP), (key) scriptKey + "|" + extra);
120}
121
122NPCmenu(key id, key npc)
123{
124 string name = llKey2Name(npc);
125
126 if ((0 == osIsUUID(npc)) || (npc == NULL_KEY) || ("" == name))
127 {
128 showMenu(id, (string) INVENTORY_NONE, "Main NPC menu.",
129 [
130 "clone avatar..", "create NPC..", "run script..",
131 "nearby NPCs..", "local NPCs..", "NPCs in sim..",
132 "backup NPCs", "nuke NPCs", "restore NPCs",
133 "start recording", "stop recording"
134 ], NPC_MAIN);
135 }
136 else
137 {
138 integer found = llListFindList(chatters, [npc]);
139 string chat = "";
140
141 if (0 <= found)
142 chat = "\nUse /" + llList2String(chatters, found + CHAT_CHANNEL) + " to relay chat to " + llKey2Name(npc);
143 showMenu(id, (string) INVENTORY_NONE, "Play with " + llKey2Name(npc) + " :" + chat,
144 [
145 "change..", "chat relay..", "come here",
146 "go to..", "fly to..", "land at..",
147 "follow me", "stalk them..", "stop moving",
148 "sit..", NPC_STAND,
149 NPC_LOCATE, "take controls", "touch..",
150 "start animation..", "stop animation..",
151 NPC_DELETE
152 ], NPC_NPC + "|" + npc);
153 }
154}
155
156startSensor(key user, key npc, string name, integer type, float range,
157 string command, string title, integer agents, integer everything)
158{
159 if ((AGENT == type) && (range > 9999.9999)) // Assumes we are only looking for NPCs.
160 {
161 integer i;
162 list menu = [];
163 list avatars = osGetAvatarList(); // Strided list, UUID, position, name.
164 integer length = llGetListLength(avatars);
165
166 for (i = 0; i < length; i++)
167 {
168 key this = (key) llList2String(avatars, i * 3);
169
170 if (osIsNpc(this))
171 menu += [llKey2Name(this) + "|" + (string) this];
172 }
173 if (llGetListLength(menu) > 0)
174 showMenu(user, (string) INVENTORY_NONE, "Choose NPC :", menu, NPC_PICK);
175 }
176 else
177 {
178 sensorRequests += [name, type, range, command, user, npc, title, agents, everything];
179 nextSensor();
180 }
181}
182
183nextSensor()
184{
185 if (sensorInFlight)
186 return;
187 if (0 < llGetListLength(sensorRequests))
188 {
189 string name = llList2String(sensorRequests, 0);
190 integer type = llList2Integer(sensorRequests, 1);
191 float range = llList2Float(sensorRequests, 2);
192 string command = llList2String(sensorRequests, 3);
193 key user = llList2String(sensorRequests, 4);
194 key npc = llList2String(sensorRequests, 5);
195 string title = llList2String(sensorRequests, 6);
196 integer agents = llList2Integer(sensorRequests, 7);
197 integer every = llList2Integer(sensorRequests, 8);
198
199 sensorInFlight = TRUE;
200 llSensor(name, "", type, range, TWO_PI);
201 }
202}
203
204sendCommand(key user, string command)
205{
206 // Work around the other script getting reset later, with a fresh key
207 NPCscriptKey = llGetInventoryKey("NPC tool");
208 llMessageLinked(LINK_SET, UTILITIES_CHAT_FAKE, llDumpList2String([0, llKey2Name(user), user, command], LIST_SEP), NPCscriptKey);
209}
210
211init()
212{
213 scriptKey = llGetInventoryKey(llGetScriptName());
214 // Register our interest in touch menus.
215 llMessageLinked(LINK_SET, UTILITIES_MENU, llDumpList2String([NULL_KEY, INVENTORY_NONE, "NPC tool|" + llGetScriptName()], LIST_SEP), (key) scriptKey);
216}
217
218
219default
220{
221 state_entry()
222 {
223 init();
224 }
225
226 on_rez(integer param)
227 {
228 init();
229 }
230
231 attach(key attached)
232 {
233 init();
234 }
235
236 link_message(integer sender_num, integer num, string value, key id)
237 {
238 list keys = llParseStringKeepNulls((string) id, ["|"], []);
239 string extra = llList2String(keys, 1);
240
241 id = (key) llList2String(keys, 0);
242 // Work around the other script getting reset later, with a fresh key
243 NPCscriptKey = llGetInventoryKey("NPC tool");
244//llSay(0, "id = " + (string) id + " extra = " + extra + " VALUE " + value);
245 if (UTILITIES_RESET_DONE == num)
246 init();
247 else if ((NPC_NEW_NPC == num) && (NPCscriptKey == id))
248 {
249 list args = llParseString2List(value, ["|"], []);
250
251 NPCmenu(llList2String(args, 0), llList2String(args, 1));
252 }
253 else if ((NPC_ADD_CHATTER == num) && (NPCscriptKey == id))
254 {
255 list args = llParseString2List(value, ["|"], []);
256 key npc = llList2String(args, 0);
257 integer channel = llList2Integer(args, 1);
258 integer found = llListFindList(chatters, [npc]);
259
260 if (0 <= found)
261 chatters = llDeleteSubList(chatters, found, found + CHAT_STRIDE - 1);
262 chatters += [npc, channel];
263 }
264 else if ((NPC_DEL_CHATTER == num) && (NPCscriptKey == id))
265 {
266 integer found = llListFindList(chatters, [value]);
267
268 if (0 <= found)
269 chatters = llDeleteSubList(chatters, found, found + CHAT_STRIDE - 1);
270 }
271 else if ((UTILITIES_CHAT_DONE == num) && (NPCscriptKey == id))
272 {
273 // incoming channel | incoming name | incoming key | incoming message | prefix | command | list of arguments | rest of message
274 list result = llParseStringKeepNulls(value, [LIST_SEP], []);
275 //integer inchannel = llList2Integer(result, 0);
276 //string inName = llList2String (result, 1);
277 key user = llList2Key (result, 2);
278 //string inMessage = llList2String (result, 3);
279 //string prefix = llList2String (result, 4);
280 string command = llList2String (result, 5);
281 list arguments = llList2List (result, 6, -1); // Includes "rest of message" as the last one.
282
283 if (NPC_NUKE == command)
284 {
285 chatters = [];
286 sensorRequests = [];
287 sensorInFlight = FALSE;
288 }
289 }
290 else if ((UTILITIES_MENU_DONE == num) && (scriptKey == id)) // Big menu button pushed
291 {
292 list input = llParseStringKeepNulls(value, [LIST_SEP], []);
293 key user = (key) llList2String(input, 0);
294 string selection = llList2String(input, 1);
295 list parts = llParseString2List(selection, ["|"], []);
296 key uuid = (key) llList2String(parts, 1);
297 key npc = (key) llList2String(keys, 2);
298 list details = llGetObjectDetails(uuid, [OBJECT_POS]);
299 string menu = extra;
300//llSay(0, extra + " MENU " + value + " KEYS " + llDumpList2String(keys, " "));
301
302 // See if it was our top level menu requested via touch registration.
303 if ("" == selection)
304 {
305 NPCmenu(user, NULL_KEY);
306 return;
307 }
308
309 // Figure out what the user picked.
310 if ((NPC_MAIN == extra) || (NPC_NPC == extra))
311 menu = selection;
312 // Make sure main menu items don't return to an NPC menu, by setting npc to null.
313 if (NPC_MAIN == extra)
314 npc = NULL_KEY;
315
316 // Check if the NPC still exists.
317 if (NPC_NPC == extra)
318 {
319 list npcDetails = llGetObjectDetails(npc, [OBJECT_POS]);
320
321 if (llGetListLength(npcDetails) == 0)
322 {
323 // Bail out if the NPC went AWOL.
324 npc = NULL_KEY;
325 selection = "Exit";
326 }
327 }
328
329 if ("Exit" == selection)
330 {
331 if (NPC_NPC == extra) npc = NULL_KEY;
332 if (NPC_MAIN == extra) return;
333 }
334 // Commands.
335 else if (NPC_ANIMATE == menu) sendCommand(user, NPC_ANIMATE + " " + npc + " " + selection);
336 else if (NPC_CHANGE == menu) sendCommand(user, NPC_CHANGE + " " + npc + " " + selection);
337 else if (NPC_CLONE == menu) sendCommand(user, NPC_CLONE + " " + uuid);
338 else if (NPC_FLY == menu) sendCommand(user, NPC_FLY + " " + npc + " " + llList2String(details, 0));
339 else if (NPC_GO == menu) sendCommand(user, NPC_GO + " " + npc + " " + llList2String(details, 0));
340 else if (NPC_LAND == menu) sendCommand(user, NPC_LAND + " " + npc + " " + llList2String(details, 0));
341 else if (NPC_LOCATE == menu) sendCommand(user, NPC_LOCATE + " " + npc);
342 else if (NPC_RELAY == menu) sendCommand(user, NPC_SAY + " " + npc + " " + selection);
343 else if (NPC_SCRIPT == menu) sendCommand(user, NPC_SCRIPT + " " + selection);
344 else if (NPC_SIT == menu) sendCommand(user, NPC_SIT + " " + npc + " " + uuid);
345 else if (NPC_STALK == menu) sendCommand(user, NPC_STALK + " " + npc + " " + uuid);
346 else if (NPC_STAND == menu) sendCommand(user, NPC_STAND + " " + npc);
347 else if (NPC_STOPANIM == menu) sendCommand(user, NPC_STOPANIM + " " + npc + " " + selection);
348 else if (NPC_TOUCH == menu) sendCommand(user, NPC_TOUCH + " " + npc + " " + uuid);
349 else if ("come here" == menu) sendCommand(user, NPC_COME + " " + npc);
350 else if ("follow me" == menu) sendCommand(user, NPC_FOLLOW + " " + npc);
351 else if ("nuke NPCs" == menu) sendCommand(user, NPC_NUKE);
352 else if ("restore NPCs" == menu) sendCommand(user, NPC_SCRIPT + " " + NPC_BACKUP_CARD + NPC_SCRIPT_EXT);
353 else if ("stop moving" == menu) sendCommand(user, NPC_STOP + " " + npc);
354
355 // Menus.
356 else if ("change.." == menu) showMenu(user, ((string) INVENTORY_NOTECARD) + "|.+\\" + NPC_NPC_EXT,
357 "Choose an NPC to change to :", [], NPC_CHANGE + "|" + npc);
358 else if ("chat relay.." == menu) showMenu(user, (string) INVENTORY_NONE,
359 "Choose a channel to relay chat from :", ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"], NPC_RELAY + "|" + npc);
360 else if ("create NPC.." == menu) showMenu(user, ((string) INVENTORY_NOTECARD) + "|.+\\" + NPC_NPC_EXT,
361 "Choose an NPC to create :", [], NPC_CREATE);
362 else if ("run script.." == menu) showMenu(user, ((string) INVENTORY_NOTECARD) + "|.+\\" + NPC_SCRIPT_EXT,
363 "Choose a script to run :", [], NPC_SCRIPT);
364 else if ("start animation.." == menu) showMenu(user, (string) INVENTORY_ANIMATION,
365 "Choose an animation to start :", [], NPC_ANIMATE + "|" + npc);
366 else if ("stop animation.." == menu) showMenu(user, (string) INVENTORY_ANIMATION,
367 "Choose an animation to stop :", [], NPC_STOPANIM + "|" + npc);
368
369 // Sensor menus.
370 else if ("clone avatar.." == menu) startSensor(user, "", "", AGENT, 256.0, NPC_CLONE, "Choose person to clone :", TRUE, TRUE);
371 else if ("fly to.." == menu) startSensor(user, npc, "", ACTIVE | PASSIVE, 1024.0, NPC_FLY, "Choose thing to fly to :", FALSE, TRUE);
372 else if ("go to.." == menu) startSensor(user, npc, "", ACTIVE | PASSIVE, 1024.0, NPC_GO, "Choose thing to go to :", FALSE, TRUE);
373 else if ("land at.." == menu) startSensor(user, npc, "", ACTIVE | PASSIVE, 1024.0, NPC_LAND, "Choose thing to land at :", FALSE, TRUE);
374 else if ("local NPCs.." == menu) startSensor(user, "", "", AGENT, 256.0, NPC_PICK, "Choose NPC :", FALSE, FALSE);
375 else if ("nearby NPCs.." == menu) startSensor(user, "", "", AGENT, 20.0, NPC_PICK, "Choose NPC :", FALSE, FALSE);
376 else if ("NPCs in sim.." == menu) startSensor(user, "", "", AGENT, 16384.0, NPC_PICK, "Choose NPC :", FALSE, FALSE);
377 else if ("sit.." == menu) startSensor(user, npc, "", ACTIVE | SCRIPTED, 1024.0, NPC_SIT, "Choose thing to sit on :", FALSE, TRUE);
378 else if ("stalk them.." == menu) startSensor(user, npc, "", AGENT, 1024.0, NPC_STALK, "Choose person to stalk :", TRUE, TRUE);
379 else if ("touch.." == menu) startSensor(user, npc, "", ACTIVE | SCRIPTED, 1024.0, NPC_TOUCH, "Choose thing to touch :", FALSE, TRUE);
380
381 // Misc.
382 else if (NPC_PICK == menu) npc = uuid;
383 else if ("start recording" == menu) llMessageLinked(LINK_SET, NPC_RECORD, "", NPCscriptKey);
384 else if ("stop recording" == menu) llMessageLinked(LINK_SET, NPC_RECORD_STOP, "", NPCscriptKey);
385 else if (NPC_CREATE == menu)
386 {
387 sendCommand(user, NPC_CREATE + " " + selection);
388 // Avoid the NPCmenu() below. An odd one out, coz the NPC wont exist yet, but we want their menu when they do exist.
389 return;
390 }
391 else if (NPC_DELETE == menu)
392 {
393 sendCommand(user, NPC_DELETE + " " + npc);
394 // An odd one out, the NPC wont exist, so return to the main menu.
395 npc = NULL_KEY;
396 }
397 else if ("backup NPCs" == menu)
398 {
399 list avatars = osGetAvatarList(); // Strided list, UUID, position, name.
400 list delete = [];
401 list backup = [];
402 integer length = llGetListLength(avatars);
403 integer i;
404
405 llRemoveInventory(NPC_BACKUP_CARD + NPC_SCRIPT_EXT);
406 for (i = 0; i < length; i++)
407 {
408 key this = (key) llList2String(avatars, i * 3);
409
410 if (osIsNpc(this))
411 {
412 string aName = llKey2Name(this);
413
414 osAgentSaveAppearance(this, aName + NPC_NPC_EXT);
415 delete += [NPC_DELETE + " " + aName];
416 backup += [NPC_CREATE + " " + aName + " " + llList2String(avatars, (i * 3) + 1)];
417 }
418 }
419 osMakeNotecard(NPC_BACKUP_CARD + NPC_SCRIPT_EXT,
420 ["script " + NPC_BACKUP_CARD + ".before" + NPC_SCRIPT_EXT]
421 + delete + backup
422 + ["script " + NPC_BACKUP_CARD + ".after" + NPC_SCRIPT_EXT]);
423 }
424
425 // If the menu name ends in "..", then it's expected that we are waiting on another menu, so don't show one now.
426 if (0 == osRegexIsMatch(menu, ".+\\.\\.$"))
427 NPCmenu(user, npc);
428 } // End of menu block.
429 }
430
431 no_sensor()
432 {
433 sensorInFlight = FALSE;
434 if (llGetListLength(sensorRequests))
435 {
436 sensorRequests = llDeleteSubList(sensorRequests, 0, 8);
437 nextSensor();
438 }
439 }
440
441 sensor(integer numberDetected)
442 {
443 sensorInFlight = FALSE;
444 if (llGetListLength(sensorRequests))
445 {
446 string name = llList2String(sensorRequests, 0);
447 integer type = llList2Integer(sensorRequests, 1);
448 float range = llList2Float(sensorRequests, 2);
449 string command = llList2String(sensorRequests, 3);
450 key user = llList2String(sensorRequests, 4);
451 key npc = llList2String(sensorRequests, 5);
452 string title = llList2String(sensorRequests, 6);
453 integer agents = llList2Integer(sensorRequests, 7);
454 integer every = llList2Integer(sensorRequests, 8);
455
456 sensorRequests = llDeleteSubList(sensorRequests, 0, 8);
457 if ("" == title)
458 sendCommand(user, command + " " + npc + " " + llDetectedKey(0));
459 else
460 {
461 integer i;
462 list menu = [];
463
464 if (agents)
465 menu += ["you|" + (string) user];
466 for (i = 0; i < numberDetected; i++)
467 {
468 key this = llDetectedKey(i);
469
470 if (!(agents && (this == user)))
471 if (every || osIsNpc(this))
472 menu += [llDetectedName(i) + "|" + (string) this];
473 }
474 if (llGetListLength(menu) > 0)
475 showMenu(user, (string) INVENTORY_NONE, title, menu, command + "|" + npc);
476 }
477 nextSensor();
478 }
479 }
480
481} \ 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 @@
1// Onefang's general purpose NPC tool version 1.0.
2// Requires onefang's utilities script.
3
4// Copyright (C) 2013 David Seikel (onefang rejected).
5//
6// Permission is hereby granted, free of charge, to any person obtaining a copy
7// of this software and associated documentation files (the "Software"), to
8// deal in the Software without restriction, including without limitation the
9// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10// sell copies of the Software, and to permit persons to whom the Software is
11// furnished to do so, subject to the following conditions:
12//
13// The above copyright notice and this permission notice shall be included in
14// all copies of the Software and its Copyright notices. In addition publicly
15// documented acknowledgment must be given that this software has been used if no
16// source code of this software is made available publicly. This includes
17// acknowledgments in either Copyright notices, Manuals, Publicity and Marketing
18// documents or any documentation provided with any product containing this
19// software. This License does not apply to any software that links to the
20// libraries provided by this software (statically or dynamically), but only to
21// the software provided.
22//
23// Please see the COPYING-PLAIN for a plain-english explanation of this notice
24// and it's intent.
25//
26// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
29// THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
30// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
31// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32
33// This is bad listening for commands on channel 0.
34integer commandChannel = 0;
35
36list attention = []; // npc key or script number, listen handle, list of attentive NPCs
37integer ATTENT_KEY = 0;
38integer ATTENT_HANDLE = 1;
39integer ATTENT_NPCS = 2;
40integer ATTENT_STRIDE = 3;
41
42list chatters = []; // npc key, chat type, listen handle. In channel order.
43integer CHAT_KEY = 0;
44integer CHAT_TYPE = 1;
45integer CHAT_HANDLE = 2;
46integer CHAT_STRIDE = 3;
47
48list followers = []; // npc key, stalkee key, distance vector
49integer FOLLOW_KEY = 0;
50integer FOLLOW_STALK = 1;
51integer FOLLOW_DIST = 2;
52integer FOLLOW_STRIDE = 3;
53
54list movers = []; // npc key, destination position, least distance, timestamp
55integer MOVE_KEY = 0;
56integer MOVE_DEST = 1;
57integer MOVE_LEAST = 2;
58integer MOVE_TIME = 3;
59integer MOVE_STRIDE = 4;
60
61float scriptTick = 0.5;
62float lastTick = -1.0;
63list scripts = []; // user key, script card name, flags, command list LIST_SEP separated
64integer SCRIPTS_KEY = 0;
65integer SCRIPTS_NAME = 1;
66integer SCRIPTS_FLAGS = 2;
67integer SCRIPTS_COMMANDS = 3;
68integer SCRIPTS_STRIDE = 4;
69
70// Script flags.
71integer SCRIPT_READING = 1;
72integer SCRIPT_ATTENTION = 2;
73
74list sensorRequests = []; // name to search for, type of search, command, user key, npc key
75integer sensorInFlight = FALSE;
76
77integer recording = FALSE;
78list record = []; // recorded commands
79
80float restartTimer = -1.0;
81
82// NPC commands.
83string NPC_ANIMATE = "animate"; // NPC name, animation name
84string NPC_ATTENTION= "attention"; // NPC name.
85string NPC_CHANGE = "change"; // NPC name, NPC (notecard) name.
86string NPC_CLONE = "clone"; // Avatar's name.
87string NPC_COME = "come"; // NPC name.
88string NPC_COMPLETE = "complete"; // NPC name.
89string NPC_CREATE = "create"; // NPC (notecard) name, (optional) position vector.
90string NPC_DELETE = "delete"; // NPC name.
91string NPC_DISMISSED= "dismissed"; // NPC name.
92string NPC_FOLLOW = "follow"; // NPC name, (optional) distance (float or vector).
93string NPC_FLY = "fly"; // NPC name, position vector or object/agent name/key.
94string NPC_GO = "go"; // NPC name, position vector or object/agent name/key.
95string NPC_LAND = "land"; // NPC name, position vector or object/agent name/key.
96string NPC_LINK = "link"; // Link number, number, string, (optional) key
97string NPC_LISTEN = "listen"; // new command channel
98string NPC_LOCATE = "locate"; // NPC name
99string NPC_NUKE = "nuke"; // No arguments.
100string NPC_ROTATE = "rotate"; // NPC name, Z rotation in degrees.
101string NPC_SAY = "say"; // NPC name, thing to say, or relay channel.
102string NPC_SCRIPT = "script"; // Script notecard name.
103string NPC_SHOUT = "shout"; // NPC name, thing to shout, or relay channel.
104string NPC_SIT = "sit"; // NPC name, object to sit on.
105string NPC_SLEEP = "sleep"; // Seconds.
106string NPC_STALK = "stalk"; // NPC name, avatar name, (optional) distance (float or vector).
107string NPC_STAND = "stand"; // NPC name.
108string NPC_STOP = "stop"; // NPC name.
109string NPC_STOPANIM = "stopanim"; // NPC name, animation name
110string NPC_TOUCH = "touch"; // NPC name, object to touch.
111string NPC_WHISPER = "whisper"; // NPC name, thing to whisper, or relay channel.
112
113string NPC_MAIN = "Main";
114string NPC_PICK = "Pick";
115string NPC_NPC = "NPC";
116string NPC_RELAY = "Relay";
117
118// LSL allows setting variables at this level to the values of previously declared variables.
119// But the OpenSim script engine developer had other ideas.
120// Otherwise we would not need this duplication.
121list OUR_COMMANDS = ["0", "", "", ""
122 + "animate|LA|"
123 + "attention|L|"
124 + "change|LC.avatar|"
125 + "clone|L|"
126 + "come|L|"
127// "complete|L|" a script only command handled outside the usual system.
128 + "create|Nv|" // Yes first argument should be C, but we want an NPC name anyway.
129 + "delete|L|"
130 + "dismissed|L|"
131 + "follow|Ls|" // Just ask for a string second argument, sort them out when they get here.
132 + "fly|Ls|" // Just ask for a string second argument, sort them out when they get here.
133 + "go|Ls|" // Just ask for a string second argument, sort them out when they get here.
134 + "land|Ls|" // Just ask for a string second argument, sort them out when they get here.
135 + "link|IISk|" // TODO - the system can't handle that k on the end yet.
136 + "listen|I|"
137 + "locate|L|"
138 + "nuke||"
139 + "rotate|LF|"
140 + "say|Ls|" // Just ask for a string second argument, sort them out when they get here.
141 + "script|X.npc|"
142 + "shout|Ls|" // Just ask for a string second argument, sort them out when they get here.
143 + "sit|Ls|"
144 + "sleep|F|"
145 + "stalk|LLs|" // Just ask for a string second argument, sort them out when they get here.
146 + "stand|L|"
147 + "stop|L|"
148 + "stopanim|LA|"
149 + "touch|LS|"
150 + "whisper|Ls|" // Just ask for a string second argument, sort them out when they get here.
151 ];
152
153// When sending multiple commands, some don't need to be expanded.
154list OUR_COMMANDS_NONAMES = ["attention", "create", "link", "listen", "nuke", "script", "sleep"];
155
156integer IS_NPC;
157integer IS_BOTH;
158integer OBJECT;
159
160vector STALK_DISTANCE = <-3.0, 0.0, 0.0>;
161integer SIM_CHANNEL = -65767365;
162
163string NPC_NPC_EXT = ".avatar";
164string NPC_SCRIPT_EXT = ".npc";
165string NPC_BACKUP_CARD = "Restore";
166string NPC_RECORD_CARD = "Recorded";
167string NPC_TEMP_CARD = "Temporary";
168
169integer NPC_RECORD = -100;
170integer NPC_RECORD_STOP = -101;
171integer NPC_NEW_NPC = -102;
172integer NPC_ADD_CHATTER = -103;
173integer NPC_DEL_CHATTER = -104;
174
175
176// Stuff for onefangs common utilities.
177key scriptKey;
178
179string UTILITIES_SCRIPT_NAME = "onefang's utilities";
180string LIST_SEP = "$!#"; // Used to seperate lists when sending them as strings.
181integer UTILITIES_RESET = -1;
182integer UTILITIES_RESET_DONE = -2;
183integer UTILITIES_READ = -3;
184integer UTILITIES_READ_DONE = -4;
185integer UTILITIES_SUBSTITUTE = -5;
186integer UTILITIES_SUBSTITUTE_DONE = -6;
187integer UTILITIES_NEXT_WORD = -7;
188integer UTILITIES_NEXT_WORD_DONE = -8;
189integer UTILITIES_PAYMENT_MADE = -9;
190integer UTILITIES_PAYMENT_MADE_DONE = -10;
191integer UTILITIES_MENU = -11;
192integer UTILITIES_MENU_DONE = -12;
193integer UTILITIES_WARP = -13;
194integer UTILITIES_WARP_DONE = -14;
195integer UTILITIES_AVATAR_KEY = -15;
196integer UTILITIES_AVATAR_KEY_DONE = -16;
197integer UTILITIES_CHAT = -17;
198integer UTILITIES_CHAT_DONE = -18;
199integer UTILITIES_CHAT_FAKE = -19;
200integer UTILITIES_CHAT_FAKE_DONE = -20;
201
202
203integer checkChat(key npc, string message, integer type, key newNpc)
204{
205 // Check if it's a single word.
206 if (-1 == llSubStringIndex(message, " "))
207 {
208 integer channel = (integer) message;
209
210 // Check if it's a sane number.
211 if ((message == (string) channel) && (channel < 12))
212 {
213 integer old = llListFindList(chatters, [npc]);
214
215 if (NULL_KEY != newNpc)
216 chatters = llListReplaceList(chatters, [newNpc], old, old);
217 else
218 {
219 // We are using llList2String here, coz the above find might return -1, and only llList2String can handle that.
220 key oldNpc = (key) llList2String(chatters, old + CHAT_KEY);
221 integer oldType = (integer) llList2String(chatters, old + CHAT_TYPE);
222 integer handle = (integer) llList2String(chatters, old + CHAT_HANDLE);
223
224 // No matter what, remove the old one for this NPC.
225 if (-1 != old)
226 {
227 llListenRemove(handle);
228 chatters = llListReplaceList(chatters, ["", 0, 0], old, old + CHAT_STRIDE - 1);
229 }
230 if (0 < channel)
231 {
232 handle = (integer) llList2String(chatters, (channel * CHAT_STRIDE) + CHAT_HANDLE);
233 if (handle)
234 llListenRemove(handle);
235 handle = llListen(channel, "", NULL_KEY, "");
236 chatters = llListReplaceList(chatters, [npc, type, handle], channel * CHAT_STRIDE, (channel * CHAT_STRIDE) + CHAT_STRIDE - 1);
237 llMessageLinked(LINK_SET, NPC_ADD_CHATTER, npc + "|" + (string) channel, scriptKey);
238 }
239 else
240 llMessageLinked(LINK_SET, NPC_DEL_CHATTER, npc, scriptKey);
241 }
242 return TRUE;
243 }
244 }
245
246 if (NULL_KEY != npc)
247 {
248 string command = "#";
249
250 if (0 == type) {command = "whisper"; osNpcWhisper(npc, 0, message);}
251 else if (1 == type) {command = "say"; osNpcSay(npc, message);}
252 else if (2 == type) {command = "shout"; osNpcShout(npc, 0, message);}
253 if (recording)
254 recordIt(command, [llKey2Name(npc), message]);
255 }
256 return FALSE;
257}
258
259startSensor(key user, key npc, string name, integer type, string command)
260{
261 sensorRequests += [name, type, command, user, npc];
262 nextSensor();
263}
264
265nextSensor()
266{
267 if (sensorInFlight)
268 return;
269 if (0 < llGetListLength(sensorRequests))
270 {
271 string name = llList2String(sensorRequests, 0);
272 integer type = llList2Integer(sensorRequests, 1);
273 string command = llList2String(sensorRequests, 2);
274 key user = llList2String(sensorRequests, 3);
275 key npc = llList2String(sensorRequests, 4);
276
277 sensorInFlight = TRUE;
278 llSensor(name, "", type, 1024.0, TWO_PI);
279 }
280}
281
282integer newScript(key user, string name)
283{
284 integer i;
285 integer length = llGetListLength(scripts);
286
287 if (NULL_KEY == llGetInventoryKey(name))
288 {
289 llSay(0, "No such script notecard - " + name);
290 return -1;
291 }
292 // Scan one past the end, so that if there's no free ones, we stick it at the end.
293 for (i = 0; i <= length; i += SCRIPTS_STRIDE)
294 {
295 if ("" == llList2String(scripts, i + SCRIPTS_KEY))
296 {
297 scripts = llListReplaceList(scripts, [user, name, SCRIPT_READING, ""], i, i + SCRIPTS_STRIDE - 1);
298 llMessageLinked(LINK_SET, UTILITIES_READ, name, scriptKey + "|" + (string) i);
299 // A break command is a wonderful thing LL.
300 return i;
301 }
302 }
303 // Should never get here, but just in case, try to trigger an error later on.
304 return -1;
305}
306
307recordIt(string command, list arguments)
308{
309 integer length = llGetListLength(arguments);
310 integer i;
311 float now = llGetTime();
312
313 // Record pauses between commands, but not if it's less than the script tick time, no point.
314 if ((0.0 < lastTick) && ((now - lastTick) > scriptTick))
315 record += ["sleep " + (string) (now - lastTick)];
316 lastTick = now;
317 for (i = 0; i < length; ++i)
318 {
319 string arg = llList2String(arguments, i);
320
321 if (osIsUUID(arg))
322 arg = llKey2Name(arg);
323 command += " " + arg;
324 }
325 record += [command];
326}
327
328key string2key(string name, integer type)
329{
330 if (osIsUUID(name))
331 return (key) name;
332 else if (AGENT == type)
333 {
334 list names = llParseString2List(name, [" "], []);
335 key uuid = osAvatarName2Key(llList2String(names, 0), llList2String(names, 1));
336
337 return uuid;
338 }
339 else if ((IS_BOTH == type) || (IS_NPC == type)) // osAvatarName2Key() does not work on NPCs, so we gotta scan the entire sim.
340 {
341 // osGetAvatarList() skips the owner, so we need to add it..
342 list avatars = [llGetOwner(), ZERO_VECTOR, llKey2Name(llGetOwner())] + osGetAvatarList(); // Strided list, UUID, position, name.
343 integer length = llGetListLength(avatars);
344 integer i;
345
346 for (i = 0; i < length; i +=3)
347 {
348 key this = (key) llList2String(avatars, i);
349 string thisName = llList2String(avatars, i + 2);
350
351 if ((thisName == name) && (osIsNpc(this) || (IS_BOTH == type)))
352 return this;
353 }
354 }
355 else if (llGetObjectName() == name)
356 return llGetKey();
357
358 return NULL_KEY;
359}
360
361vector string2pos(string name)
362{
363 key uuid = NULL_KEY;
364
365 if (("<" == llGetSubString(name, 0, 0)) && (">" == llGetSubString(name, -1, -1)))
366 {
367 // Looks like a vector, cast it.
368 return (vector) name;
369 }
370
371 uuid = string2key(name, IS_BOTH);
372 if (NULL_KEY != uuid)
373 return (vector) llList2String(llGetObjectDetails(uuid, [OBJECT_POS]), 0);
374
375 return ZERO_VECTOR;
376}
377
378integer goThere(key user, string name, string dest, string type)
379{
380 key npc = string2key(name, IS_NPC);
381 integer executed = FALSE;
382
383 if (NULL_KEY != npc)
384 {
385 vector pos = string2pos(dest);
386
387 if (ZERO_VECTOR == pos)
388 startSensor(user, npc, dest, ACTIVE | PASSIVE, type);
389 else
390 {
391 integer found = llListFindList(movers, [npc]);
392 integer method = OS_NPC_FLY | OS_NPC_LAND_AT_TARGET;
393 list this = [npc, pos, llVecMag((vector) llList2String(llGetObjectDetails(npc, [OBJECT_POS]), 0) - pos), llGetTime()];
394
395 if ((0 <= found) && ((found % MOVE_STRIDE) == 0))
396 llListReplaceList(movers, this, found, found + MOVE_STRIDE - 1);
397 else
398 movers += this;
399
400 if (NPC_GO == type)
401 method = OS_NPC_NO_FLY;
402 else if (NPC_FLY == type)
403 method = OS_NPC_FLY;
404 else if (NPC_LAND == type)
405 method = OS_NPC_FLY | OS_NPC_LAND_AT_TARGET;
406 // Telling a sitting NPC to move results in an error, so tell them to stand up, just in case.
407 osNpcStand(npc);
408 osNpcMoveToTarget(npc, pos, method);
409 executed = TRUE;
410 }
411 }
412 return executed;
413}
414
415killNPC(key npc, key newNpc)
416{
417 integer found = llListFindList(followers, [npc]);
418 integer length = llGetListLength(attention);
419
420 osNpcRemove(npc);
421 // Stop this attention whore from chatting, moving, and stalking.
422 checkChat(npc, "0", 0, newNpc);
423
424 while (0 <= found)
425 {
426 if ((found % FOLLOW_STRIDE) == FOLLOW_KEY) // Change of stalker.
427 {
428 if (NULL_KEY != newNpc)
429 followers = llListReplaceList(followers, [newNpc], found, found);
430 else
431 followers = llDeleteSubList(followers, found, found + FOLLOW_STRIDE - 1);
432 }
433 else if ((found % FOLLOW_STRIDE) == FOLLOW_STALK) // Change of stalkee.
434 {
435 if (NULL_KEY != newNpc)
436 followers = llListReplaceList(followers, [newNpc], found, found);
437 else
438 followers = llDeleteSubList(followers, found - FOLLOW_STALK, found - FOLLOW_STALK + FOLLOW_STRIDE - 1);
439 }
440 found = llListFindList(followers, [npc]);
441 }
442 found = llListFindList(movers, [npc]);
443 if ((0 <= found) && ((found % MOVE_STRIDE) == 0))
444 {
445 // The only user of newNpc wants even the new one gone from the movers list.
446 // But we do this anyway, coz it will get confused, and might need it later.
447 if (NULL_KEY != newNpc)
448 movers = llListReplaceList(movers, [newNpc], found, found);
449 else
450 movers = llDeleteSubList(movers, found, found + MOVE_STRIDE - 1);
451 }
452 // Search the attention lists to and remove/replace them from that.
453 for (found = 0; found < length; found += ATTENT_STRIDE)
454 delAttention(llList2String(attention, found + ATTENT_KEY), npc, newNpc);
455}
456
457addAttention(key user, key npc)
458{
459 list npcs = [];
460 integer handle = 0;
461 integer isUser = FALSE;
462 integer found = llListFindList(attention, [(string) user]);
463
464 // TODO - This should not be happening, I think, but it does.
465 if (NULL_KEY == npc)
466 return;
467
468 if (osIsUUID(user))
469 isUser = TRUE;
470
471 if (0 != (found % ATTENT_STRIDE))
472 found = -1;
473
474 if (-1 == found)
475 {
476 if (isUser)
477 handle = llListen(commandChannel, llKey2Name(user), user, "");
478 }
479 else
480 {
481 handle = llList2Integer(attention, found + ATTENT_HANDLE);
482 npcs = llParseString2List(llList2String(attention, found + ATTENT_NPCS), ["|"], []);
483 attention = llDeleteSubList(attention, found, found + ATTENT_STRIDE - 1);
484 found = llListFindList(npcs, [(string) npc]);
485 if (-1 != found)
486 npcs = llDeleteSubList(npcs, found, found);
487 }
488 attention += [user, handle, llDumpList2String(npcs + [npc], "|")];
489 if (isUser)
490 llSay(0, llKey2Name(npc) + " pays attention to " + llKey2Name(user));
491}
492
493// Replaces instead of deletes if newNpc is not NULL.
494// Deletes them all if npc is NULL.
495delAttention(key user, key npc, key newNpc)
496{
497 list npcs = [];
498 integer handle;
499 integer isUser = FALSE;
500 integer found = llListFindList(attention, [(string) user]);
501
502 if (osIsUUID(user))
503 isUser = TRUE;
504 if (0 != (found % ATTENT_STRIDE))
505 found = -1;
506
507 if (-1 != found)
508 {
509 handle = llList2Integer(attention, found + ATTENT_HANDLE);
510 npcs = llParseString2List(llList2String(attention, found + ATTENT_NPCS), ["|"], []);
511 attention = llDeleteSubList(attention, found, found + ATTENT_STRIDE - 1);
512 if (NULL_KEY == npc)
513 npcs = [];
514 else
515 {
516 found = llListFindList(npcs, [(string) npc]);
517 if (-1 != found)
518 {
519 if (NULL_KEY != newNpc)
520 npcs = llListReplaceList(npcs, [newNpc], found, found);
521 else
522 npcs = llDeleteSubList(npcs, found, found);
523 }
524 }
525 if (0 == llGetListLength(npcs))
526 llListenRemove(handle);
527 else
528 attention += [user, handle, llDumpList2String(npcs, "|")];
529 if (isUser && (NULL_KEY == newNpc))
530 llSay(0, llKey2Name(npc) + " ignores " + llKey2Name(user));
531 }
532}
533
534sendManyCommands(key user, string command, key index)
535{
536 list npcs = [];
537 integer handle;
538 integer found = llListFindList(attention, [(string) user]);
539
540 if (user != index)
541 found = llListFindList(attention, [(string) index]);
542 if (0 != (found % ATTENT_STRIDE))
543 found = -1;
544
545 if (-1 != found)
546 {
547 integer length;
548 integer i;
549 string thisCommand;
550 string rest = "";
551
552 handle = llList2Integer(attention, found + ATTENT_HANDLE);
553 npcs = llParseString2List(llList2String(attention, found + ATTENT_NPCS), ["|"], []);
554 length = llGetListLength(npcs);
555 // Split the command on the first space, if there is one.
556 found = llSubStringIndex(command, " ");
557 thisCommand = llGetSubString(command, 0, found);
558 if (-1 != found)
559 {
560 thisCommand = llGetSubString(command, 0, found - 1);
561 rest = " " + llGetSubString(command, found, -1);
562 }
563 else
564 thisCommand = command;
565// TODO - the original command will go through utilities as well,
566// if it's a chat command, and generate an error.
567 // If it's on the no names list, then don't bother expanding the names.
568 if (-1 != llListFindList(OUR_COMMANDS_NONAMES, [thisCommand]))
569 {
570 // Don't bother if it's from chat, it got done already through the usual method.
571 if (0 == handle)
572 sendCommand(user, command);
573 }
574 else
575 {
576 for (i = 0; i < length; ++i)
577 sendCommand(user, thisCommand + " " + llList2String(npcs, i) + rest);
578 }
579 }
580}
581
582sendCommand(key user, string command)
583{
584 llMessageLinked(LINK_SET, UTILITIES_CHAT_FAKE, llDumpList2String([0, llKey2Name(user), user, command], LIST_SEP), scriptKey);
585}
586
587init()
588{
589 integer i;
590 if (llGetAttached())
591 {
592 // We are attached to an avatar, so get it's key.
593 key realId = llGetOwnerKey(llGetKey());
594 if (osIsNpc(realId))
595 {
596 // TODO - Instead we should go into a "only control this NPC" mode.
597 llSay(0, "Deleting onefang's NPC scripts from this NPC.");
598 llRemoveInventory(UTILITIES_SCRIPT_NAME);
599 llRemoveInventory(llGetScriptName());
600 }
601 else
602 {
603 // Only listen to the attachment wearer if attached.
604 OUR_COMMANDS = llListReplaceList(OUR_COMMANDS, [realId], 1, 1);
605 }
606 }
607 IS_NPC = ACTIVE;
608 IS_BOTH = SCRIPTED;
609 OBJECT = PASSIVE;
610 for (i = 0; i < 16; ++i)
611 chatters += ["", 0, 0];
612 scriptKey = llGetInventoryKey(llGetScriptName());
613 llMessageLinked(LINK_SET, UTILITIES_RESET, "reset", scriptKey);
614 llSetTimerEvent(scriptTick);
615}
616
617default
618{
619 state_entry()
620 {
621 init();
622 }
623
624 on_rez(integer param)
625 {
626 init();
627 }
628
629 attach(key attached)
630 {
631 init();
632 }
633
634 changed(integer change)
635 {
636 // Restore NPCs if the sim restarted, after a delay to let the sim settle.
637 if (change & CHANGED_REGION_START)
638 restartTimer = llGetTime() + 60.0;
639 }
640
641 link_message(integer sender_num, integer num, string value, key id)
642 {
643 list keys = llParseStringKeepNulls((string) id, ["|"], []);
644 string extra = llList2String(keys, 1);
645
646 id = (key) llList2String(keys, 0);
647//llSay(0, "id = " + (string) id + " extra = " + extra + " VALUE " + value);
648 if ((NPC_RECORD == num) && (scriptKey == id))
649 {
650 record = [];
651 recording = TRUE;
652 }
653 else if ((NPC_RECORD_STOP == num) && (scriptKey == id))
654 {
655 recording = FALSE;
656 lastTick = -1.0;
657 llRemoveInventory(NPC_RECORD_CARD + NPC_SCRIPT_EXT);
658 osMakeNotecard(NPC_RECORD_CARD + NPC_SCRIPT_EXT, record);
659 }
660 else if ((UTILITIES_RESET_DONE == num) && (llGetInventoryKey(UTILITIES_SCRIPT_NAME) == id))
661 {
662 // Set our commands.
663 OUR_COMMANDS = llListReplaceList(OUR_COMMANDS, [commandChannel], 0, 0);
664 llMessageLinked(LINK_SET, UTILITIES_CHAT, llDumpList2String(OUR_COMMANDS, LIST_SEP), scriptKey);
665 }
666 else if ((UTILITIES_READ_DONE == num) && (scriptKey == id))
667 {
668 // The script is done, remove the reading flag.
669 list command = llParseStringKeepNulls(value, [LIST_SEP], []);
670 string card = llList2String(command, 0);
671 // extra was pre strided when it was sent off.
672 integer i = ((integer) extra);
673
674 if ("" != llList2String(scripts, i))
675 {
676 integer flags = llList2Integer(scripts, i + SCRIPTS_FLAGS);
677
678 scripts = llListReplaceList(scripts, [flags & (~SCRIPT_READING)], i + SCRIPTS_FLAGS, i + SCRIPTS_FLAGS);
679//llSay(0, "Done reading " + (string) i + " " + card + "\n" + llDumpList2String(scripts, "~"));
680 }
681 }
682 else if (-1000 >= num) // SettingsReader telling us to change a setting.
683 {
684 // num is the line number : -1000 - settingsLine
685 // Not sure if llMessageLinked is a FIFO, but lets hope so.
686 // Otherwise we may have to resort to OpenSim card reading, and drop SL compatibility.
687 list command = llParseStringKeepNulls(value, [LIST_SEP], []);
688 list result = [];
689 string card = llList2String(command, 0);
690 integer length = llGetListLength(command);
691 // extra was pre strided when it was sent off.
692 integer i = (integer) extra;
693 integer j;
694 string commands = llList2String(scripts, i + SCRIPTS_COMMANDS);
695
696 if ("" != commands)
697 result = [commands];
698//llSay(0, " reading " + (string) i + " " + card);
699//if (0 == (num % 100)) {llSay(0, "read line " + (string) num); llSleep(0.1);}
700 for (j = 1; j < length; j += 2)
701 result += [llList2String(command, j) + LIST_SEP + llList2String(command, j + 1)];
702 scripts = llListReplaceList(scripts, [llDumpList2String(result, "|")], i + SCRIPTS_COMMANDS, i + SCRIPTS_COMMANDS);
703 }
704 else if ((UTILITIES_CHAT_DONE == num) && (scriptKey == id))
705 {
706 integer executed = FALSE;
707 // incoming channel | incoming name | incoming key | incoming message | prefix | command | list of arguments | rest of message
708 list result = llParseStringKeepNulls(value, [LIST_SEP], []);
709 //integer inchannel = llList2Integer(result, 0);
710 //string inName = llList2String (result, 1);
711 key user = llList2Key (result, 2);
712 //string inMessage = llList2String (result, 3);
713 //string prefix = llList2String (result, 4);
714 string command = llList2String (result, 5);
715 list arguments = llList2List (result, 6, -1); // Includes "rest of message" as the last one.
716//llSay(0, "COMMAND " + value);
717
718 // WARNING - Don't return out of this "if" chain, unless you don't want the command recorded.
719 // TODO - Maybe just do that instead of using the executed flag.
720 if (NPC_ATTENTION == command)
721 addAttention(user, string2key(llList2String(arguments, 0), IS_NPC));
722 if (NPC_DISMISSED == command)
723 delAttention(user, string2key(llList2String(arguments, 0), IS_NPC), NULL_KEY);
724 else if (NPC_CLONE == command)
725 {
726 string name = llList2String(arguments, 0);
727 key uuid = string2key(name, AGENT);
728
729 if (osIsUUID(name))
730 name = llKey2Name(name);
731 if (NULL_KEY != uuid)
732 {
733 osAgentSaveAppearance(uuid, name + NPC_NPC_EXT);
734 executed = TRUE;
735 }
736 }
737 else if (NPC_CREATE == command)
738 {
739 string name = llList2String(arguments, 0);
740 list names = llParseString2List(name, [" "], []);
741 string last = llList2String(names, 1);
742 vector pos = llList2String(arguments, 1);
743 key npc;
744 integer fromMenu = FALSE;
745
746 // Either strip off the extension, or add it.
747 if (llSubStringIndex(last, NPC_NPC_EXT) != -1)
748 {
749 last = llGetSubString(last, 0, -1 - llStringLength(NPC_NPC_EXT));
750 // This is an evil hack, the menu system will hand us the full name of the notecard.
751 // Other methods are likely to not do so. But only likely.
752 // TODO - I think this idea fails with the new argument parsing. DOH!
753 fromMenu = TRUE;
754 }
755 else
756 name += NPC_NPC_EXT;
757 if (ZERO_VECTOR == pos)
758 pos = llGetPos() + (<1.0, 0.0, 1.5> * llGetRot());
759 npc = osNpcCreate(llList2String(names, 0), last, pos, name, OS_NPC_SENSE_AS_AGENT | OS_NPC_NOT_OWNED);
760 executed = TRUE;
761 if (fromMenu)
762 llMessageLinked(LINK_SET, NPC_NEW_NPC, user + "|" + npc, scriptKey);
763 }
764 else if (NPC_CHANGE == command)
765 {
766 string card = llList2String(arguments, 1);
767
768 if (llSubStringIndex(card, NPC_NPC_EXT) == -1)
769 card += NPC_NPC_EXT;
770 osNpcLoadAppearance(string2key(llList2String(arguments, 0), IS_NPC), card);
771 executed = TRUE;
772 }
773 else if (NPC_COME == command)
774 executed = goThere(user, llList2String(arguments, 0), user, NPC_GO);
775 else if (NPC_FOLLOW == command)
776 {
777 key npc = string2key(llList2String(arguments, 0), IS_NPC);
778
779 if (NULL_KEY != npc)
780 {
781 integer found = llListFindList(followers, [npc]);
782 vector pos = (vector) llList2String(arguments, 1);
783
784 if (ZERO_VECTOR == pos)
785 {
786 float distance = llList2Float(arguments, 1);
787
788 if (0.0 == distance)
789 pos = STALK_DISTANCE;
790 else
791 pos = <distance, 0.0, 0.0>;
792 }
793 if ((0 <= found) && ((found % FOLLOW_STRIDE) == 0))
794 llListReplaceList(followers, [npc, user, pos], found, found + FOLLOW_STRIDE - 1);
795 else
796 followers += [npc, user, pos];
797 executed = TRUE;
798 }
799 }
800 else if (NPC_STALK == command)
801 {
802 key npc = string2key(llList2String(arguments, 0), IS_NPC);
803 key avatar = string2key(llList2String(arguments, 1), IS_BOTH);
804
805 // No stalking yourself, that's just creepy.
806 if ((NULL_KEY != avatar) && (NULL_KEY != npc) && (avatar != npc))
807 {
808 integer found = llListFindList(followers, [npc]);
809 vector pos = (vector) llList2String(arguments, 2);
810
811 if (ZERO_VECTOR == pos)
812 {
813 float distance = llList2Float(arguments, 2);
814
815 if (0.0 == distance)
816 pos = STALK_DISTANCE;
817 else
818 pos = <distance, 0.0, 0.0>;
819 }
820 if ((0 <= found) && ((found % FOLLOW_STRIDE) == 0))
821 llListReplaceList(followers, [npc, avatar, pos], found, found + FOLLOW_STRIDE - 1);
822 else
823 followers += [npc, avatar, pos];
824 executed = TRUE;
825 }
826 }
827 else if (NPC_GO == command)
828 executed = goThere(user, llList2String(arguments, 0), llList2String(arguments, 1), NPC_GO);
829 else if (NPC_FLY == command)
830 executed = goThere(user, llList2String(arguments, 0), llList2String(arguments, 1), NPC_FLY);
831 else if (NPC_LAND == command)
832 executed = goThere(user, llList2String(arguments, 0), llList2String(arguments, 1), NPC_LAND);
833 else if (NPC_LINK == command)
834 {
835 llMessageLinked(llList2Integer(arguments, 0), llList2Integer(arguments, 1),
836 llList2String(arguments, 2), llList2String(arguments, 3));
837 executed = TRUE;
838 }
839 else if (NPC_LISTEN == command)
840 {
841 // Remove our chat commands from whatever channel they where on before.
842 llMessageLinked(LINK_SET, UTILITIES_CHAT, llDumpList2String(llList2List(OUR_COMMANDS, 0, 0) + ["", "", ""], LIST_SEP), scriptKey);
843 // Set them on the new channel.
844 commandChannel = llList2Integer(arguments, 0);
845 OUR_COMMANDS = llListReplaceList(OUR_COMMANDS, [commandChannel], 0, 0);
846 llMessageLinked(LINK_SET, UTILITIES_CHAT, llDumpList2String(OUR_COMMANDS, LIST_SEP), scriptKey);
847 executed = TRUE;
848 }
849 else if (NPC_LOCATE == command)
850 {
851 key npc = string2key(llList2String(arguments, 0), IS_NPC);
852 vector pos = string2pos(llList2String(arguments, 0));
853 vector size = llGetAgentSize(npc);
854
855 // This wont work, it HAS to be in a touch event. Silly LL and their hobbled thinking.
856 //llMapDestination(llGetRegionName(), pos, pos);
857
858 // Offset by halfish the NPCs size, so it should end up above them.
859 pos.z += size.z / 1.8;
860 // First destroy any existing beacons. Simplifies things.
861 llRegionSay(SIM_CHANNEL, "nobeacon");
862 llRezObject("locator beacon", llGetPos(), ZERO_VECTOR, llEuler2Rot(<180.0 * DEG_TO_RAD, 0.0, 0.0>), SIM_CHANNEL);
863 // Wait for it to finish starting up. A hack I know, avoids making things more complex.
864 // Avoids complications with object_rez(key uuid) events and having to track what we rezzed.
865 llSleep(1.0);
866 llRegionSay(SIM_CHANNEL, "beacon " + (string) pos);
867 }
868 else if (NPC_ANIMATE == command)
869 {
870 osNpcPlayAnimation(string2key(llList2String(arguments, 0), IS_NPC), llList2String(arguments, 1));
871 executed = TRUE;
872 }
873 else if (NPC_STOPANIM == command)
874 {
875 osNpcStopAnimation(string2key(llList2String(arguments, 0), IS_NPC), llList2String(arguments, 1));
876 executed = TRUE;
877 }
878 else if (NPC_ROTATE == command)
879 {
880 rotation rot = llEuler2Rot(<0.0, 0.0, llList2Float(arguments, 1) * DEG_TO_RAD>);
881
882 osNpcSetRot(string2key(llList2String(arguments, 0), IS_NPC), rot);
883 executed = TRUE;
884 }
885 else if (NPC_SCRIPT == command)
886 {
887 newScript(user, llList2String(arguments, 0));
888 // Don't actually record this, since it's commands will be recorded.
889 executed = FALSE;
890 }
891 else if (NPC_SAY == command)
892 executed = checkChat(string2key(llList2String(arguments, 0), IS_NPC), llList2String(arguments, 1), 1, NULL_KEY);
893 else if (NPC_SHOUT == command)
894 executed = checkChat(string2key(llList2String(arguments, 0), IS_NPC), llList2String(arguments, 1), 2, NULL_KEY);
895 else if (NPC_STOP == command)
896 {
897 key npc = string2key(llList2String(arguments, 0), IS_NPC);
898 integer found = llListFindList(followers, [npc]);
899
900 if ((0 <= found) && ((found % FOLLOW_STRIDE) == 0))
901 followers = llDeleteSubList(followers, found, found + FOLLOW_STRIDE - 1);
902 found = llListFindList(movers, [npc]);
903 if ((0 <= found) && ((found % MOVE_STRIDE) == 0))
904 movers = llDeleteSubList(movers, found, found + MOVE_STRIDE - 1);
905 osNpcStopMoveToTarget(npc);
906 executed = TRUE;
907 }
908 else if (NPC_COMPLETE == command) // Do nothing, it's a script only command handled completely in the script.
909 executed = TRUE;
910 else if (NPC_WHISPER == command)
911 executed = checkChat(string2key(llList2String(arguments, 0), IS_NPC), llList2String(arguments, 1), 0, NULL_KEY);
912 else if (NPC_SIT == command)
913 {
914 key npc = string2key(llList2String(arguments, 0), IS_NPC);
915 string name = llList2String(arguments, 1);
916
917 if (NULL_KEY != npc)
918 {
919 key that = string2key(name, OBJECT);
920
921 if (NULL_KEY == that)
922 startSensor(user, npc, name, ACTIVE | SCRIPTED, NPC_SIT);
923 else
924 {
925 osNpcSit(npc, that, OS_NPC_SIT_NOW);
926 executed = TRUE;
927 }
928 }
929 }
930 else if (NPC_TOUCH == command)
931 {
932 key npc = string2key(llList2String(arguments, 0), IS_NPC);
933 string name = llList2String(arguments, 1);
934
935 if (NULL_KEY != npc)
936 {
937 key that = string2key(name, OBJECT);
938
939 if (NULL_KEY == that)
940 startSensor(user, npc, name, ACTIVE | SCRIPTED, NPC_TOUCH);
941 else
942 {
943 osNpcTouch(npc, that, LINK_ROOT);
944 executed = TRUE;
945 }
946 }
947 }
948 else if (NPC_STAND == command)
949 {
950 osNpcStand(string2key(llList2String(arguments, 0), IS_NPC));
951 executed = TRUE;
952 }
953 else if (NPC_DELETE == command)
954 {
955 // Since this deletes the NPC, if we are recording we wont be able to do llKey2Name below.
956 string name = llKey2Name(string2key(llList2String(arguments, 0), IS_NPC));
957
958 killNPC(string2key(llList2String(arguments, 0), IS_NPC), NULL_KEY);
959 arguments = [name];
960 executed = TRUE;
961 }
962 else if (NPC_NUKE == command)
963 {
964 list avatars = osGetAvatarList(); // Strided list, UUID, position, name.
965 integer length = llGetListLength(avatars);
966 integer i;
967
968 for (i = 0; i < length; i++)
969 {
970 key this = (key) llList2String(avatars, i * 3);
971
972 if (osIsNpc(this))
973 osNpcRemove(this);
974 }
975 attention = [];
976 chatters = [];
977 followers = [];
978 movers = [];
979 scripts = [];
980 sensorRequests = [];
981 sensorInFlight = FALSE;
982 // Delete the beacons as well.
983 llRegionSay(SIM_CHANNEL, "nobeacon");
984 executed = TRUE;
985 }
986
987 // Record it, but only if it did something.
988 if (recording && executed)
989 recordIt(command, arguments);
990 }
991 }
992
993 listen(integer channel, string name, key id, string message)
994 {
995 key npc = (key) llList2String (chatters, (channel * CHAT_STRIDE) + CHAT_KEY);
996 integer type = llList2Integer(chatters, (channel * CHAT_STRIDE) + CHAT_TYPE);
997 integer handle = llList2Integer(chatters, (channel * CHAT_STRIDE) + CHAT_HANDLE);
998
999 // It's either in the chatters or the attention list.
1000 // Chatters are on their own channel.
1001 // Attentions are on the usual command channel (common for all users).
1002 // Utilities will also be listening on the command channel. And will get a duplicate utterance, sans the name.
1003 // So choose between them based on channel.
1004 // Users that make a chatter channel the same as the command channel deserve what they get.
1005 if (channel == commandChannel)
1006 sendManyCommands(id, message, id);
1007 if (NULL_KEY != npc)
1008 checkChat(npc, message, type, NULL_KEY);
1009 }
1010
1011 no_sensor()
1012 {
1013 sensorInFlight = FALSE;
1014 if (llGetListLength(sensorRequests))
1015 {
1016 sensorRequests = llDeleteSubList(sensorRequests, 0, 4);
1017 nextSensor();
1018 }
1019 }
1020
1021 sensor(integer numberDetected)
1022 {
1023 sensorInFlight = FALSE;
1024 if (llGetListLength(sensorRequests))
1025 {
1026 string name = llList2String(sensorRequests, 0);
1027 integer type = llList2Integer(sensorRequests, 1);
1028 string command = llList2String(sensorRequests, 2);
1029 key user = llList2String(sensorRequests, 3);
1030 key npc = llList2String(sensorRequests, 4);
1031
1032 sensorRequests = llDeleteSubList(sensorRequests, 0, 4);
1033 sendCommand(user, command + " " + npc + " " + llDetectedKey(0));
1034 nextSensor();
1035 }
1036 }
1037
1038 timer()
1039 {
1040 integer i;
1041 integer length = llGetListLength(followers);
1042
1043 // Check for sim restart.
1044 if ((0.0 < restartTimer) && (llGetTime() > restartTimer))
1045 {
1046 restartTimer = -1.0;
1047 // Start with a nuke, just to clear out all the lists.
1048 sendCommand(llGetOwnerKey(llGetKey()), NPC_NUKE);
1049 sendCommand(llGetOwnerKey(llGetKey()), NPC_SCRIPT + " " + NPC_BACKUP_CARD + NPC_SCRIPT_EXT);
1050 // The sim restarted, no need to take care of followers or scripts this time.
1051 return;
1052 }
1053
1054 // First the followers.
1055 for (i = 0; i < length; i += FOLLOW_STRIDE)
1056 {
1057 key npc = (key) llList2String(followers, i + FOLLOW_KEY);
1058 key stalkee = (key) llList2String(followers, i + FOLLOW_STALK);
1059 vector distance = (vector) llList2String(followers, i + FOLLOW_DIST);
1060 float mag = llVecMag(distance);
1061 list details = llGetObjectDetails(stalkee, [OBJECT_POS, OBJECT_VELOCITY, OBJECT_ROT]);
1062 list npcDetails = llGetObjectDetails(npc, [OBJECT_POS, OBJECT_VELOCITY, OBJECT_ROT]);
1063 vector pos = (vector) llList2String(details, 0);
1064 vector speed = (vector) llList2String(details, 1);
1065 rotation rot = (rotation) llList2String(details, 2);
1066 integer npcInfo = llGetAgentInfo(npc);
1067 integer info = llGetAgentInfo(stalkee);
1068 integer isFlying = info & AGENT_FLYING;
1069 integer isWalking = info & AGENT_WALKING;
1070 integer isInAir = info & AGENT_IN_AIR;
1071 integer isRunning = info & AGENT_ALWAYS_RUN;
1072 integer isSitting = info & AGENT_SITTING;
1073 vector newPos = pos + (distance * rot);
1074 float newDist = llVecMag((vector) llList2String(npcDetails, 0) - pos);
1075
1076 if (newDist > mag)
1077 {
1078 if (npcInfo & AGENT_SITTING) // Tell the lazy bum to stand up.
1079 osNpcStand(npc);
1080 if (isRunning)
1081 osNpcMoveToTarget(npc, newPos, OS_NPC_NO_FLY | OS_NPC_RUNNING);
1082 else if (isFlying || isInAir)
1083 osNpcMoveToTarget(npc, newPos, OS_NPC_FLY);
1084 else if (isWalking)
1085 osNpcMoveToTarget(npc, newPos, OS_NPC_NO_FLY);
1086 else
1087 osNpcMoveToTarget(npc, newPos, OS_NPC_NO_FLY);
1088// osNpcMoveToTarget(npc, pos, OS_NPC_FLY | OS_NPC_LAND_AT_TARGET);
1089 }
1090 else
1091 osNpcStopMoveToTarget(npc);
1092 }
1093
1094 // Then movers.
1095 length = llGetListLength(movers);
1096 for (i = 0; i < length; i += MOVE_STRIDE)
1097 {
1098 key npc = (key) llList2String (movers, i + MOVE_KEY);
1099 vector dest = (vector) llList2String (movers, i + MOVE_DEST);
1100 float least = llList2Float (movers, i + MOVE_LEAST);
1101 float time = llList2Float (movers, i + MOVE_TIME);
1102 float left = llVecMag((vector) llList2String(llGetObjectDetails(npc, [OBJECT_POS]), 0) - dest);
1103
1104 if (least > left) // Progress has been made.
1105 movers = llListReplaceList(movers, [left, llGetTime()], i + MOVE_LEAST, i + MOVE_TIME);
1106 else if ((llGetTime() - time) > 5.0) // No progress, they are stuck.
1107 {
1108 key oldNpc = npc;
1109 string name = llKey2Name(npc);
1110 list names = llParseString2List(name, [" "], []);
1111 list anims = llGetAnimationList(npc);
1112 integer animLen = llGetListLength(anims);
1113 integer invLen = llGetInventoryNumber(INVENTORY_ANIMATION);
1114 integer j;
1115
1116 llShout(0, name + " is stuck!");
1117if ("" == name)
1118{
1119 llSay(0, "stuck key " + (string) i + " " + npc + " - " + oldNpc + " -- " + llDumpList2String(movers, "|"));
1120 killNPC(oldNpc, NULL_KEY);
1121 return;
1122}
1123 osAgentSaveAppearance(npc, NPC_TEMP_CARD + NPC_NPC_EXT);
1124 length = llGetListLength(movers);
1125 npc = osNpcCreate(llList2String(names, 0), llList2String(names, 1), dest, NPC_TEMP_CARD + NPC_NPC_EXT, OS_NPC_SENSE_AS_AGENT | OS_NPC_NOT_OWNED);
1126 llRemoveInventory(NPC_TEMP_CARD + NPC_NPC_EXT);
1127 // We want to replace them into the Attention, chatters, and followers lists.
1128 // Though no point adding them back to movers.
1129//llSay(0, "stuck key " + npc + " - " + oldNpc + " -- " + llDumpList2String(movers, "|"));
1130 killNPC(oldNpc, npc);
1131
1132 // Try to re instate the animations.
1133 for (j = 0; j < animLen; ++j)
1134 {
1135 key anim = (key) llList2String(anims, j);
1136 integer k;
1137
1138 // For each of the anims that was playing on the old NPC,
1139 // See if we can find a match in our inventory.
1140 for (k = 0; k < invLen; ++k)
1141 {
1142 string thisName = llGetInventoryName(INVENTORY_ANIMATION, k);
1143
1144 if (llGetInventoryKey(thisName) == anim)
1145 {
1146 osNpcPlayAnimation(npc, thisName);
1147 k = invLen;
1148 }
1149 }
1150 }
1151 }
1152
1153 if (left < 2.5) // They have arrived.
1154 {
1155 movers = llDeleteSubList(movers, i, i + MOVE_STRIDE - 1);
1156 length -= MOVE_STRIDE;
1157 osNpcStopMoveToTarget(npc);
1158 }
1159 }
1160
1161 // Then the scripts.
1162 length = llGetListLength(scripts);
1163 for (i = 0; i < length; i += SCRIPTS_STRIDE)
1164 {
1165 string user = llList2String(scripts, i + SCRIPTS_KEY);
1166
1167 if ("" != user)
1168 {
1169 string name = llList2String(scripts, i + SCRIPTS_NAME);
1170 integer flags = llList2Integer(scripts, i + SCRIPTS_FLAGS);
1171 list commands = llParseStringKeepNulls(llList2String(scripts, i + SCRIPTS_COMMANDS), ["|"], []);
1172 list statement = llParseStringKeepNulls(llList2String(commands, 0), [LIST_SEP], []);
1173 string command = llList2String(statement, 0);
1174 string value = llList2Key(statement, 1);
1175
1176 commands = llDeleteSubList(commands, 0, 0);
1177
1178 if ("" == value) // A command with no =. Run it as a pretend chat command from the user.
1179 {
1180 if ("sleep " == llGetSubString(command, 0, 5))
1181 {
1182 float time = (float) llGetSubString(command, 6, -1);
1183
1184 commands = llListInsertList(commands, ["until " + (string)(llGetTime() + time)], 0);
1185 }
1186 else if ("until " == llGetSubString(command, 0, 5))
1187 {
1188 float time = (float) llGetSubString(command, 6, -1);
1189
1190 if (llGetTime() < time)
1191 commands = llListInsertList(commands, [command], 0);
1192 }
1193 else if ("script " == llGetSubString(command, 0, 6))
1194 {
1195 string new = llGetSubString(command, 7, -1);
1196 integer index = newScript(user, new);
1197
1198 if (0 == llGetListLength(commands))
1199 ;//llSay(0, "tail recursion detected - " + name + " -> " + new + " " + (string) i + " -> " + (string) index);
1200 else if (-1 != index)
1201 commands = llListInsertList(commands, ["wait " + (string) index + " " + new], 0);
1202 }
1203 else if ("wait " == llGetSubString(command, 0, 4))
1204 {
1205 list parts = llParseStringKeepNulls(command, [" "], []);
1206 integer index = llList2Integer(parts, 1);
1207 string card = llList2String(parts, 2);
1208
1209 // Check if the script this user started is still in the same slot we created above.
1210 // Note, still possible to get a, hopefully rare, race condition here.
1211 if ((llList2String(scripts, index + SCRIPTS_KEY) == user) && (llList2String(scripts, index + SCRIPTS_NAME) == card))
1212 commands = llListInsertList(commands, [command], 0);
1213 }
1214 else if ("complete" == llGetSubString(command, 0, 7) && (8 == llStringLength(command)))
1215 {
1216 // Deal with attention seekers, we have to wait for all of them to get there.
1217 integer found = llListFindList(attention, [(string) i]);
1218
1219 if (0 != (found % ATTENT_STRIDE))
1220 found = -1;
1221
1222 if (-1 != found)
1223 {
1224 list npcs = llParseString2List(llList2String(attention, found + ATTENT_NPCS), ["|"], []);
1225 integer nLength = llGetListLength(npcs);
1226 integer j;
1227
1228 for (j = 0; j < nLength; ++j)
1229 {
1230 key npc = llList2String(npcs, j);
1231 found = llListFindList(movers, [npc]);
1232
1233 // If any are still a mover, keep waiting for the move to complete.
1234 if ((0 <= found) && ((found % MOVE_STRIDE) == 0))
1235 {
1236 commands = llListInsertList(commands, [command], 0);
1237 j = nLength;
1238 }
1239 }
1240 }
1241 }
1242 else if ("complete " == llGetSubString(command, 0, 8))
1243 {
1244 key npc = string2key(llGetSubString(command, 9, -1), IS_NPC);
1245 integer found = llListFindList(movers, [npc]);
1246
1247 // If they are still a mover, keep waiting for the move to complete.
1248 if ((0 <= found) && ((found % MOVE_STRIDE) == 0))
1249 commands = llListInsertList(commands, [command], 0);
1250 }
1251 else if ("attention " == llGetSubString(command, 0, 9))
1252 {
1253 addAttention((string) i, string2key(llGetSubString(command, 10, -1), IS_NPC));
1254 scripts = llListReplaceList(scripts, [flags | SCRIPT_ATTENTION], i + SCRIPTS_FLAGS, i + SCRIPTS_FLAGS);
1255 }
1256 else if ("dismissed " == llGetSubString(command, 0, 9))
1257 {
1258 scripts = llListReplaceList(scripts, [flags & (~SCRIPT_ATTENTION)], i + SCRIPTS_FLAGS, i + SCRIPTS_FLAGS);
1259 delAttention((string) i, string2key(llGetSubString(command, 10, -1), IS_NPC), NULL_KEY);
1260 }
1261 else if ("" != command)
1262 {
1263//llSay(0, "DOING " + (string) (flags & SCRIPT_ATTENTION) + " " + command);
1264 if (flags & SCRIPT_ATTENTION)
1265 sendManyCommands(user, command, (string) i);
1266 else
1267 sendCommand(user, command);
1268 }
1269 }
1270 else // A variable assignment.
1271 {
1272// if ("DEBUG" == command)
1273// DEBUG = ("TRUE" == value);
1274 }
1275
1276 if ((flags & SCRIPT_READING) || (0 < llGetListLength(commands)))
1277 scripts = llListReplaceList(scripts, [llDumpList2String(commands, "|")], i + SCRIPTS_COMMANDS, i + SCRIPTS_COMMANDS);
1278 else
1279 {
1280 scripts = llListReplaceList(scripts, ["", "", 0, ""], i, i + SCRIPTS_STRIDE - 1);
1281 // Remove all our attention seekers.
1282 delAttention((string) i, NULL_KEY, NULL_KEY);
1283 while ((llGetListLength(scripts) > 0) && ("" == llList2String(scripts, 0 - SCRIPTS_STRIDE)))
1284 scripts = llDeleteSubList(scripts, 0 - SCRIPTS_STRIDE, -1);
1285//llSay(0, "Finished script " + (string) i + " " + llDumpList2String(commands, "^") + "\n" + llDumpList2String(scripts, "~"));
1286 }
1287 }
1288 }
1289 }
1290
1291}
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 @@
1NPC tool.
2-------------
3
4This 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.
5
6NOTE - 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.
7
8Menu system.
9--------------------
10
11When 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.
12
13The "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.
14
15As a safety feature, if the NPC tool script is in an NPC's attachments, the script gets deleted.
16
17
18Script and chat command system.
19-----------------------------------------------
20
21All 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.
22
23The 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.
24
25In 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.
26
27Any 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.
28
29There 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.
30
31There is also the "Recorded.npc" notecard, it holds anything you recorded with the "start recording" menu option.
32
33You can send these commands from some other LSL script in the same object. Add something like this to your script -
34
35string LIST_SEP = "$!#"; // Used to seperate lists when sending them as strings.
36integer UTILITIES_CHAT_FAKE = -19;
37key NPCscriptKey = llGetInventoryKey("NPC tool");
38sendCommand(key user, string command)
39{
40 llMessageLinked(LINK_SET, UTILITIES_CHAT_FAKE, llDumpList2String([0, llKey2Name(user), user, command], LIST_SEP), NPCscriptKey);
41}
42
43
44Creation / deletion commands.
45 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.
46
47clone onefang rejected
48 Creates an appearance notecard of the named avatar. In this example, that will be "onefang rejected.avatar". Yes, you can clone NPCs as well.
49
50create onefang's clone <1.0,2.0,3.0>
51 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.
52
53change onefang's clone other avatar
54 Causes the named NPC to change to match the appearance notecard named, "other avatar.avatar" in this example.
55
56delete onefang's clone
57 Remove the named NPC.
58
59nuke
60 Remove all NPCs in the sim.
61
62
63Animation commands.
64 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.
65
66animate onefang's clone animation name
67 Start animating the named NPC using the named animation.
68
69stopanim onefang's clone animation name
70 Stop the named animation from animating the named NPC.
71
72
73Chatting commands.
74 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.
75
76say onefang's clone Stuff to say.
77shout onefang's clone Stuff to shout.
78whisper onefang's clone Stuff to whisper.
79
80
81Moving commands.
82 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.
83
84come onefang's clone
85 The NPC walks to the person using this command, or the person running the script via the menu system.
86
87complete onefang's clone
88 Wait for their movement to be completed. Only covers come, goto, flyto, and landat.
89
90go onefang's clone <1.0,2.0,3.0>
91 The NPC walks to the given destination.
92
93follow onefang's clone <-3.0, 0.0, 0.0>
94 Start to follow you at the (optional) distance.
95
96fly onefang's clone <1.0,2.0,3.0>
97 The NPC flies to the given destination, then remains hovering.
98
99land onefang's clone<1.0,2.0,3.0>
100 The NPC flies to the given destination, then lands.
101
102rotate onefang's clone 180
103 The NPC rotates to the given direction in degrees. Note that there seems to be an OpenSim bug, not getting the rotations I expect.
104
105stalk onefang's clone onefang rejected <-3.0, 0.0, 0.0>
106 Start to follow the given avatar or NPC at the (optional) distance.
107
108stop onefang's clone
109 Stops the NPC from moving to a destination.
110
111
112Scripting commands.
113
114link 1 2 Third thing to send.
115 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.
116
117listen 123
118 Changes the NPC tool to listen to commands from a different channel.
119
120script scriptcard.npc
121 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.
122
123sleep 10.0
124 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.
125
126
127World interaction commands.
128 The objects named in these commands have to be scripted. Names and UUIDs can be used for the objects.
129
130locate onefang's clone
131 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.
132
133sit onefang's clone object to sit on
134 The NPC sits on the named in world object.
135
136stand onefang's clone
137 The NPC stands up.
138
139touch onefang's clone object to touch
140 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.
141
142
143
144Copyright (C) 2013 David Seikel (onefang rejected).
145
146Permission is hereby granted, free of charge, to any person obtaining a copy
147of this software and associated documentation files (the "Software"), to
148deal in the Software without restriction, including without limitation the
149rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
150sell copies of the Software, and to permit persons to whom the Software is
151furnished to do so, subject to the following conditions:
152
153The above copyright notice and this permission notice shall be included in
154all copies of the Software and its Copyright notices. In addition publicly
155documented acknowledgment must be given that this software has been used if no
156source code of this software is made available publicly. This includes
157acknowledgments in either Copyright notices, Manuals, Publicity and Marketing
158documents or any documentation provided with any product containing this
159software. This License does not apply to any software that links to the
160libraries provided by this software (statically or dynamically), but only to
161the software provided.
162
163Please see the COPYING-PLAIN for a plain-english explanation of this notice
164and it's intent.
165
166THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
167IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
168FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
169THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
170IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
171CONNECTION 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 @@
1NPC-tool This is an early release of onefang's NPC tool, for controlling NPCs in OpenSim.
2========
3
4A tool for dealing with OpenSIm NPCs.
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 @@
1// Onefang's locator beacon script version 1.0.
2
3// A lot of this is just my tweaked up warpPos(), the rest is mostly trivial.
4// So I'll put this into the public domain.
5
6integer SIM_CHANNEL = -65767365;
7float offset = 128.0;
8integer jumpSize = 512;
9
10goThere(vector destPos)
11{
12 float distance = llVecDist(llGetPos(), destPos);
13
14 if (distance > 0.001)
15 {
16 integer jumps = 0;
17 integer time = llGetUnixTime();
18 float oldDistance = 0;
19 //llSetPos(destPos); // This is not any faster, and only has a range of 10 meters.
20 //destPos += <0.0, 0.0, 2.0>; // Try to work around that damned Havok 4 bug.
21
22 // We call the central routine several times to free the massive amount of memory used.
23 do
24 {
25 jumps += warpPos(distance, destPos);
26 distance = llVecDist(llGetPos(), destPos);
27 }
28 while (distance > 1.0 && ((llGetUnixTime() - time) < 2)); // Long jump, no limit.
29
30 // OK, this is just being paranoid, but has been known to be needed in the past.
31 while ((distance > 0.001) && (0.001 < (distance - oldDistance)) // Failsafe.
32 && ((--jumps) > 0) && ((llGetUnixTime() - time) < 5)) // Time out.
33 {
34 llOwnerSay("Short hop from " + (string) llGetPos() + " of " + (string) distance + ". Jumps " + (string) jumps);
35 llSetPos(destPos);
36 oldDistance = distance;
37 distance = llVecDist(llGetPos(), destPos);
38 llSleep(0.5);
39 }
40 if (distance > 0.001)
41 {
42 llShout(0, "Failed to get to " + (string) destPos + ", I am @ " + (string) llGetPos());
43 llInstantMessage(llGetOwner(), "Failed to get to " + (string) destPos + ", I am @ " + (string) llGetPos());
44 }
45 }
46}
47
48integer warpPos(float distance, vector destPos)
49{ // R&D by Keknehv Psaltery, 05/25/2006
50 // with a little pokeing by Strife, and a bit more
51 // some more munging by Talarus Luan
52 // Final cleanup by Keknehv Psaltery
53 // Extended distance by onefang Rejected.
54 // More optimizations by 1fang Fang.
55
56 // Compute the number of jumps necessary.
57 integer jumps = (integer) (distance / 10.0) + 1;
58 list rules = [PRIM_POSITION, destPos]; // The start for the rules list.
59 integer count = 1;
60
61 // Try and avoid stack/heap collisions.
62 if (jumps > jumpSize)
63 {
64 jumps = jumpSize;
65 llOwnerSay("Extra warp needed");
66 }
67 while ((count = count << 1 ) < jumps)
68 rules += rules;
69
70 //Changed by Eddy Ofarrel to tighten memory use some more
71 llSetPrimitiveParams(rules + llList2List(rules, (count - jumps) << 1, count));
72//llOwnerSay("Jumps " + (string) jumps + " free " + (string) llGetFreeMemory());
73 return jumps;
74}
75
76init()
77{
78 vector scale = llGetScale();
79
80 offset = scale.z / 2;
81 llListen(SIM_CHANNEL, "", NULL_KEY, "");
82}
83
84default
85{
86 state_entry()
87 {
88 init();
89 }
90
91 on_rez(integer param)
92 {
93 SIM_CHANNEL = param;
94 init();
95 }
96
97 attach(key attached)
98 {
99 init();
100 }
101
102 listen(integer channel, string name, key id, string message)
103 {
104 if ("beacon " == llGetSubString(message, 0, 6))
105 {
106 vector pos = (vector) llGetSubString(message, 7, -1);
107
108 if (ZERO_VECTOR != pos)
109 {
110 pos.z += offset;
111 // OpenSim bug, first one doesn't quite get there, second one needed.
112 // Happens with llSetPos() as well, llSetPrimitiveParams() at least does them both at once.
113 // Need warpPos anyway, which hides this bug.
114 goThere(pos);
115 llSetPrimitiveParams([PRIM_GLOW, ALL_SIDES, 1.0]);
116 llSetAlpha(1.0, ALL_SIDES);
117 }
118 }
119 else if ("nobeacon" == message)
120 llDie();
121 }
122
123 touch_start(integer num_detected)
124 {
125 vector pos = llGetPos();
126
127 pos.z -= offset;
128 llMapDestination(llGetRegionName(), pos, pos);
129 }
130}
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 @@
1// onefang's utilites version 3.0
2// Read a complete settings notecard and send settings to other scripts.
3// Also other useful functions.
4
5// Copyright (C) 2007 David Seikel (onefang rejected).
6//
7// Permission is hereby granted, free of charge, to any person obtaining a copy
8// of this software and associated documentation files (the "Software"), to
9// deal in the Software without restriction, including without limitation the
10// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
11// sell copies of the Software, and to permit persons to whom the Software is
12// furnished to do so, subject to the following conditions:
13//
14// The above copyright notice and this permission notice shall be included in
15// all copies of the Software and its Copyright notices. In addition publicly
16// documented acknowledgment must be given that this software has been used if no
17// source code of this software is made available publicly. This includes
18// acknowledgments in either Copyright notices, Manuals, Publicity and Marketing
19// documents or any documentation provided with any product containing this
20// software. This License does not apply to any software that links to the
21// libraries provided by this software (statically or dynamically), but only to
22// the software provided.
23//
24// Please see the COPYING-PLAIN for a plain-english explanation of this notice
25// and it's intent.
26//
27// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
30// THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
31// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
32// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
33//
34// As a special exception to the above conditions, the Second Life user known
35// as Winter Ventura may ignore all permissions and conditions and provide her
36// own.
37
38// See the notecard "onefang's utilities manual".
39
40
41float MENU_TIMEOUT = 45.0;
42
43list channelHandles = []; // channel | listener
44list chatChannels = []; // channel | script list
45list chatOwners = []; // owner+channel | script list
46list chatPrefixes = []; // channel+" "+prefix | script list (PREFIXES WITH SPACE)
47list chatPrefixes2 = []; // channel+" "+prefix | script list (PREFIXES WITH NO SPACE)
48list chatCommands = []; // script+channel | command list { command | argument defining stuff }
49// TODO might be better to do - script+channel+" "+prefix | command list { command | argument defining stuff }
50// and do away with the chatPrefix lists. Makes no space prefix checks harder I think.
51// or add a prefix field to the list.
52
53// SettingsReaderAndUtilities constants for copying into other code.
54string LIST_SEP = "$!#"; // Used to seperate lists when sending them as strings.
55integer UTILITIES_RESET = -1;
56integer UTILITIES_RESET_DONE = -2;
57integer UTILITIES_READ = -3;
58integer UTILITIES_READ_DONE = -4;
59integer UTILITIES_SUBSTITUTE = -5;
60integer UTILITIES_SUBSTITUTE_DONE = -6;
61integer UTILITIES_NEXT_WORD = -7;
62integer UTILITIES_NEXT_WORD_DONE = -8;
63integer UTILITIES_PAYMENT_MADE = -9; // Not used by this script
64integer UTILITIES_PAYMENT_MADE_DONE = -10; // Not used by this script
65integer UTILITIES_MENU = -11;
66integer UTILITIES_MENU_DONE = -12;
67integer UTILITIES_WARP = -13;
68integer UTILITIES_WARP_DONE = -14;
69integer UTILITIES_AVATAR_KEY = -15; // Redundant in OpenSim.
70integer UTILITIES_AVATAR_KEY_DONE = -16; // Redundant in OpenSim.
71integer UTILITIES_CHAT = -17;
72integer UTILITIES_CHAT_DONE = -18;
73integer UTILITIES_CHAT_FAKE = -19;
74integer UTILITIES_CHAT_FAKE_DONE = -20; // Never used anywhere.
75// TODO reuse -20 for CHAT_PURGE - which removes this scriptKey from all chat lists, usually before replacing them.
76// Which was done some other way if I remember.
77
78// internal constant and variable declarations.
79integer KEYS_SCRIPT = 0;
80integer KEYS_TYPE = 1;
81integer KEYS_NAME = 2;
82integer KEYS_EXTRA = 3;
83integer KEYS_ID = 4;
84integer KEYS_STRIDE = 5;
85
86string REQUEST_KEY = "1";
87string REQUEST_IM = "2";
88string REQUEST_ONLINE = "3";
89string REQUEST_AVATAR = "4";
90
91integer MENU_USER = 0; // Key of user the big menu is for.
92integer MENU_CHANNEL = 1; // Listen channel for big menu dialogs.
93integer MENU_SCRIPT = 2; // ScriptKey of the script calling the menu.
94integer MENU_HANDLE = 3; // Listen ID for big menu dialogs.
95integer MENU_TYPE = 4; // Type of menu.
96integer MENU_MESSAGE = 5; // Message for the top of big menu dialog.
97integer MENU_MENU = 6; // The big menu itself. LIST_SEP separated when they come in.
98integer MENU_FILTER = 7; // Regex filter for inventory menus. | separated when they come in.
99integer MENU_POS = 8; // Current position within big menu.
100integer MENU_MAXPOS = 9; // Maximum position within big menu.
101integer MENU_NAMES = 10; // Long names to get around stupid SL menu limitations.
102integer MENU_TIME = 11; // Otherwise this list is ever growing. Damn Ignore button. sigh
103integer MENU_STRIDE = 12;
104
105list settingsCards = []; // Queue of cards to read.
106string settingsName = ".settings"; // Name of a notecard in the object's inventory.
107key settingsKey = NULL_KEY; // ScriptKey of the script reading the card.
108integer settingsLine = 0; // Current line number.
109key settingsQueryID = NULL_KEY; // ID used to identify dataserver queries.
110
111list menus = []; // The menus.
112list registeredMenus = []; // Any scripts that have registered as needing a touch menu.
113list keyRequests = []; // A list of avatar key requests.
114
115list ANIMATIONS =
116[
117 "aim_L_bow", "aim_R_bazooka", "aim_R_handgun", "aim_R_rifle", "angry_fingerwag",
118 "angry_tantrum", "away", "backflip", "blowkiss", "bow", "brush", "clap",
119 "courtbow", "cross_arms", "crouch", "crouchwalk", "curtsy",
120 "dance1", "dance2", "dance3", "dance4", "dance5", "dance6", "dance7", "dance8",
121 "dead", "drink", "express_afraid", "express_anger", "express_bored",
122 "express_cry", "express_embarrased", "express_laugh", "express_repulsed",
123 "express_sad", "express_shrug", "express_surprise", "express_wink",
124 "express_worry", "falldown", "female_walk", "fist_pump", "fly", "flyslow",
125 "hello", "hold_R_bow", "hold_R_bazooka", "hold_R_handgun", "hold_R_rifle",
126 "hold_throw_R", "hover", "hover_down", "hover_up", "impatient",
127 "jump", "jumpforjoy", "kick_roundhouse_R", "kissmybutt", "kneel_left",
128 "kneel_right", "land", "laugh_short", "motorcycle_sit", "musclebeach", "no_head",
129 "no_unhappy", "nyanya", "peace", "point_me", "point_you",
130 "prejump", "punch_L", "punch_onetwo", "punch_R",
131 "RPS_countdown", "RPS_paper", "RPS_rock", "RPS_scissors",
132 "run", "salute", "shoot_L_bow", "shout", "sit", "sit_female", "sit_ground",
133 "sit_to_stand", "sleep", "slowwalk", "smoke_idle", "smoke_inhale",
134 "smoke_throw_down", "snapshot", "soft_land", "stand", "standup", "stand_1",
135 "stand_2", "stand_3", "stand_4", "stretch", "stride", "surf", "sword_strike_R",
136 "talk", "throw_R", "tryon_shirt", "turnback_180", "turnleft", "turnright",
137 "turn_180", "type", "uphillwalk", "walk", "whisper", "whistle", "wink_hollywood",
138 "yell", "yes_happy", "yes_head", "yoga_float"
139];
140
141
142list addChatScripts(list thisList, string name, string value, integer stride)
143{
144 integer found = llListFindList(thisList, [name]);
145
146 if (0 <= found)
147 {
148 list values = llParseString2List(llList2String(thisList, found + 1), ["|"], []);
149
150 if (1 == stride)
151 {
152 integer subFound = llListFindList(values, [value]);
153
154 if (0 > subFound)
155 values += [value];
156 }
157 else
158 {
159 integer length = llGetListLength(values);
160 integer i;
161
162 for (i = 0; i < length; i += stride)
163 {
164 list sub = llList2List(values, i, i + stride - 1);
165 integer subFound = llListFindList(values, [llList2String(sub, 0)]);
166
167 if (0 > subFound)
168 values += sub;
169 }
170 }
171 thisList = llListReplaceList(thisList, [name, llDumpList2String(values, "|")], found, found + 1);
172 }
173 else
174 thisList += [name, value];
175 return thisList;
176}
177
178list delChatScripts(list thisList, string name, string value, integer stride)
179{
180 integer found = llListFindList(thisList, [name]);
181
182 if (0 <= found)
183 {
184 list values = llParseString2List(llList2String(thisList, found + 1), ["|"], []);
185
186 if (1 == stride)
187 {
188 integer subFound = llListFindList(values, [value]);
189
190 if (0 <= subFound)
191 values = llDeleteSubList(values, subFound, subFound);
192 }
193 else
194 {
195 integer length = llGetListLength(values);
196 integer i;
197
198 for (i = 0; i < length; i += stride)
199 {
200 list sub = llList2List(values, i, i + stride - 1);
201 integer subFound = llListFindList(values, [llList2String(sub, 0)]);
202
203 if (0 <= subFound)
204 values = llDeleteSubList(values, subFound, subFound + stride - 1);
205 }
206 }
207 if (llGetListLength(values))
208 thisList = llListReplaceList(thisList, [name, llDumpList2String(values, "|")], found, found + 1);
209 else
210 thisList = llDeleteSubList(thisList, found, found + 1);
211 }
212 return thisList;
213}
214
215resetPrimShit()
216{
217 llParticleSystem([]);
218// Comment this out for now. Causes problems with updates, and is probably not needed.
219// llSetRemoteScriptAccessPin(0);
220 llSetText("", <1, 1, 1 >, 1.0);
221 llSetTextureAnim(FALSE | SMOOTH | LOOP, ALL_SIDES, 1, 1, 0, 0, 0.0);
222 llTargetOmega(<0, 0, 0 >, 0, 0);
223}
224
225// Get the next word.
226// The last parameter is the last word returned from the previous call.
227// The list results are -
228// 0 = The rest of the text.
229// 1 = The next word.
230list nextWord(string separator, string message, string last)
231{
232 list result = [];
233 integer index;
234
235 index = llSubStringIndex(message, separator);
236 if (0 <= index)
237 {
238 if (0 != index) // Check for a corner case.
239 last += llGetSubString(message, 0, index - 1);
240 if ((index + 1) < llStringLength(message))
241 message = llGetSubString(message, index + 1, -1);
242 else
243 message = "";
244 result += message;
245 result += last;
246 }
247 else
248 {
249 result += "";
250 result += last + message;
251 }
252
253 return (result);
254}
255
256// Substitute params from a list.
257string substitute(list values, string separator)
258{
259 string result = "";
260 string original = llList2String(values, 0);
261 integer length = llGetListLength(values);
262
263// integer length = (values != []); // Speed hack.
264 integer index = 1;
265
266 while ((0 <= index) && ("" != original))
267 {
268 index = llSubStringIndex(original, separator);
269 if (0 <= index)
270 {
271 string last = separator;
272 integer i;
273
274 if (0 != index) // Check for a corner case.
275 result += llGetSubString(original, 0, index - 1);
276 if ((index + 2) < llStringLength(original))
277 {
278 last = llGetSubString(original, index + 1, index + 1);
279 original = llGetSubString(original, index + 2, -1);
280 }
281 else
282 original = "";
283
284 for (i = 1; i < length; ++i)
285 {
286 string pattern = llList2String(values, i);
287
288 if (llGetSubString(pattern, 0, 0) == last)
289 {
290 last = llGetSubString(pattern, 1, -1);
291 i = length; // A break statement would be nice.
292 }
293 }
294 result += last;
295 }
296 }
297
298 return result + original;
299}
300
301startMenu(key id, list input)
302{
303 key menuUser = NULL_KEY;
304 integer menuChannel = -1;
305 key menuScript = NULL_KEY;
306 integer menuHandle = 0;
307 integer menuType = INVENTORY_NONE;
308 string menuMessage = "";
309 list menu = [];
310 list menuFilter = [];
311 integer menuPos = 0;
312 integer menuMaxPos = 1;
313// list menuNames = [];
314 integer menuIndex = llGetListLength(menus);
315
316 menuPos = 0;
317 menuScript = id;
318 menuUser = llList2Key(input, 0);
319 menuFilter = llParseStringKeepNulls(llList2String(input, 1), ["|"], []);
320 menuType = llList2Integer(menuFilter, 0);
321 if (llGetListLength(menuFilter) > 1)
322 menuFilter = llList2List(menuFilter, 1, -1);
323 else
324 menuFilter = [];
325 menuMessage = llList2String(input, 2);
326 menu = llList2List(input, 3, -1);
327 if (NULL_KEY == menuUser)
328 {
329 // Only add it if it's not there.
330 if (-1 == llListFindList(registeredMenus, [menuMessage + "|" + menuScript]))
331 registeredMenus += [menuMessage + "|" + menuScript];
332 registeredMenus = llListSort(registeredMenus, 1, TRUE);
333 return;
334 }
335 else if (INVENTORY_NONE == menuType)
336 menuMaxPos = llGetListLength(menu);
337 else if (INVENTORY_ALL == menuType) // TODO - NONE and ALL are both -1. lol
338 menuMaxPos = llGetListLength(ANIMATIONS);
339 else
340 {
341 integer i;
342 integer j;
343 integer number = llGetInventoryNumber(menuType); // NOTE this may change while we are in the menus.
344 integer length = llGetListLength(menuFilter);
345
346 menuMaxPos = 0;
347 menu= [];
348 for (i = 0; i < number; ++i)
349 {
350 string name = llGetInventoryName(menuType, i);
351
352 if (length)
353 {
354 for (j = 0; j < length; ++j)
355 {
356 if (osRegexIsMatch(name, llList2String(menuFilter, j)))
357 {
358 ++menuMaxPos;
359 menu += name;
360 }
361 }
362 }
363 else
364 {
365 ++menuMaxPos;
366 menu += name;
367 }
368 }
369 }
370 menuChannel = (integer) (llFrand(10000) + 1000);
371 menuHandle = llListen(menuChannel, "", menuUser, "");
372 menuMaxPos /= 9;
373 menuMaxPos *= 9;
374 ++menuMaxPos;
375 menus += [menuUser, menuChannel, menuScript, menuHandle, menuType, menuMessage,
376 llDumpList2String(menu, LIST_SEP), llDumpList2String(menuFilter, "|"),
377 menuPos, menuMaxPos, "", llGetTime()];
378 showMenu(menuIndex);
379}
380
381showMenu(integer menuIndex)
382{
383 key menuUser = llList2String (menus, menuIndex + MENU_USER);
384 integer menuChannel = llList2Integer(menus, menuIndex + MENU_CHANNEL);
385 key menuScript = llList2String (menus, menuIndex + MENU_SCRIPT);
386 integer menuHandle = llList2Integer(menus, menuIndex + MENU_HANDLE);
387 integer menuType = llList2Integer(menus, menuIndex + MENU_TYPE);
388 string menuMessage = llList2String (menus, menuIndex + MENU_MESSAGE);
389 list menu = llParseString2List(llList2String (menus, menuIndex + MENU_MENU), [LIST_SEP], []);
390 list menuFilter = llParseString2List(llList2String (menus, menuIndex + MENU_FILTER), ["|"], []);
391 integer menuPos = llList2Integer(menus, menuIndex + MENU_POS);
392 integer menuMaxPos = llList2Integer(menus, menuIndex + MENU_MAXPOS);
393 list menuNames = [];
394
395 list thisMenu = [];
396 integer length;
397 integer i;
398
399 for (i = 0; i < 12; ++i)
400 {
401 string name;
402 integer index;
403
404 if (INVENTORY_NONE == menuType)
405 name = llList2String(menu, menuPos + i);
406 else if (INVENTORY_ALL == menuType)
407 name = llList2String(ANIMATIONS, menuPos + i);
408 else
409 name = llList2String(menu, menuPos + i);
410// name = llGetInventoryName(menuType, menuPos + i);
411
412 index = llSubStringIndex(name, "|");
413 if (index != -1)
414 {
415 list parts = llParseStringKeepNulls(name, ["|"], []);
416 name = llGetSubString(llList2String(parts, 0) + " ", 0, 15)
417 + "|" + llList2String(parts, 1)+ "|" + llList2String(parts, 2);
418 }
419 if (llStringLength(name) > 24)
420 {
421 menuNames += [name];
422 name = llGetSubString(name, 0, 23);
423 }
424 // TODO - Only allow blank ones for arbitrary menus, but screws with the showMenu() code.
425// if ((INVENTORY_NONE == menuType) && ("" == name))
426// name = ".";
427 if ("" != name)
428 thisMenu += [name];
429 }
430
431 length = llGetListLength(thisMenu);
432 if ((12 > length) && (0 == menuPos))
433 {
434 integer j = length % 3;
435 if (0 == j)
436 thisMenu += [".", "Exit", "."];
437 else if (1 == j)
438 {
439 string last = llList2String(thisMenu, -1);
440 thisMenu = llDeleteSubList(thisMenu, -1, -1) + [last, "Exit", "."];
441 }
442 else if (2 == j)
443 {
444 string penultimate = llList2String(thisMenu, -2);
445 string last = llList2String(thisMenu, -1);
446 thisMenu = llDeleteSubList(thisMenu, -2, -1) + [penultimate, "Exit", last];
447 }
448 }
449 else if (9 >= length)
450 thisMenu += ["<<", "Exit", ">>"];
451 else
452 thisMenu = llList2List(thisMenu, 0, 8) + ["<<", "Exit", ">>"];
453
454 // Re order them to make LSL happy.
455 for (i = 0; i < length; i += 3)
456 thisMenu = llListInsertList(llDeleteSubList(thisMenu, -3, -1), llList2List(thisMenu, -3, -1), i);
457 llDialog(menuUser, menuMessage, thisMenu, menuChannel);
458 menus = llListReplaceList(menus,
459 [
460 menuUser, menuChannel, menuScript, menuHandle, menuType, menuMessage,
461 llDumpList2String(menu, LIST_SEP), llDumpList2String(menuFilter, "|"),
462 menuPos, menuMaxPos, llDumpList2String(menuNames, LIST_SEP), llGetTime()
463 ], menuIndex, menuIndex + MENU_STRIDE - 1);
464}
465
466myListen(integer channel, string name, key id, string message)
467{
468 // check if this message came from an object or avatar,
469 // use the objects owner if it came from an object.
470 // Avatars own themselves. B-)
471 key realId = llGetOwnerKey(id);
472 integer menuIndex = llListFindList(menus, [realId, channel]);
473
474 if (0 <= menuIndex) // Menu response
475 {
476 key menuUser = llList2String (menus, menuIndex + MENU_USER);
477 integer menuChannel = llList2Integer(menus, menuIndex + MENU_CHANNEL);
478 key menuScript = llList2String (menus, menuIndex + MENU_SCRIPT);
479 integer menuHandle = llList2Integer(menus, menuIndex + MENU_HANDLE);
480 integer menuType = llList2Integer(menus, menuIndex + MENU_TYPE);
481 string menuMessage = llList2String (menus, menuIndex + MENU_MESSAGE);
482 list menu = llParseString2List(llList2String (menus, menuIndex + MENU_MENU), [LIST_SEP], []);
483 list menuFilter = llParseString2List(llList2String (menus, menuIndex + MENU_FILTER), ["|"], []);
484 integer menuPos = llList2Integer(menus, menuIndex + MENU_POS);
485 integer menuMaxPos = llList2Integer(menus, menuIndex + MENU_MAXPOS);
486 list menuNames = llParseString2List(llList2String (menus, menuIndex + MENU_NAMES), [LIST_SEP], []);
487 integer delete = FALSE;
488
489 if ("<<" == message)
490 {
491 menuPos -= 9;
492 if (menuPos < 0)
493 menuPos = menuMaxPos - 1;
494 }
495 else if (">>" == message)
496 {
497 menuPos += 9;
498 if (menuPos > (menuMaxPos - 1))
499 menuPos = 0;
500 }
501 else if ("." == message)
502 delete = TRUE;
503 else
504 {
505 delete = TRUE;
506 if (menuHandle)
507 llListenRemove(menuHandle);
508 if (llStringLength(message) == 24)
509 {
510 integer i;
511 integer length = llGetListLength(menuNames);
512
513 for (i = 0; i < length; ++i)
514 {
515 string lName = llList2String(menuNames, i);
516
517 if (message == llGetSubString(lName, 0, 23))
518 message = lName;
519 }
520 }
521
522 if (NULL_KEY == menuScript)
523 {
524 menuScript = (key) llList2String(llParseStringKeepNulls(message, ["|"], []), 2);
525 message = "";
526 }
527 llMessageLinked(LINK_SET, UTILITIES_MENU_DONE, llDumpList2String([menuUser, message], LIST_SEP), menuScript);
528 }
529 if (delete)
530 menus = llDeleteSubList(menus, menuIndex, menuIndex + MENU_STRIDE - 1);
531 else
532 {
533 menus = llListReplaceList(menus,
534 [
535 menuUser, menuChannel, menuScript, menuHandle, menuType, menuMessage,
536 llDumpList2String(menu, LIST_SEP), llDumpList2String(menuFilter, "|"),
537 menuPos, menuMaxPos, llDumpList2String(menuNames, LIST_SEP), llGetTime()
538 ], menuIndex, menuIndex + MENU_STRIDE - 1);
539 showMenu(menuIndex);
540 }
541 }
542 else // Chat command.
543 {
544 list scripts = [];
545 list words = [];
546 string prefix = "";
547 string command = "";
548 string idChannel = (string) realId + (string) channel;
549 integer thisOwner = llListFindList(chatOwners, [idChannel]);
550
551//llOwnerSay("->>" + (string) channel + " " + message);
552
553//llOwnerSay("owners " + llDumpList2String(chatOwners, "^"));
554//llOwnerSay("channels " + llDumpList2String(chatChannels, "^"));
555//llOwnerSay("prefixes " + llDumpList2String(chatPrefixes, "^"));
556//llOwnerSay("prefixes2 " + llDumpList2String(chatPrefixes2, "^"));
557//llOwnerSay("commands " + llDumpList2String(chatCommands, "^"));
558 if (0 <= thisOwner)
559 scripts = llParseString2List(llList2String(chatOwners, thisOwner + 1), ["|"], []);
560 else
561 {
562 integer thisChannel = llListFindList(chatChannels, [(string) channel]);
563
564 if (0 <= thisChannel)
565 scripts = llParseString2List(llList2String(chatChannels, thisChannel + 1), ["|"], []);
566 }
567//llOwnerSay(llDumpList2String(scripts, "|"));
568 if ([] != scripts)
569 {
570 integer thisPrefix;
571 string candidate;
572
573 words = llParseString2List(message, [" "], []);
574 candidate = llList2String(words, 0);
575 thisPrefix = llListFindList(chatPrefixes, [(string) channel + " " + candidate]);
576//llOwnerSay(llDumpList2String(words, "~"));
577//llSay(0, candidate);
578//llSay(0, (string) thisPrefix);
579 if (0 <= thisPrefix)
580 {
581 prefix = candidate;
582 scripts = llParseString2List(llList2String(chatPrefixes, thisPrefix + 1), ["|"], []);
583 words = llList2List(words, 1, -1);
584 }
585 else
586 {
587 integer length = llGetListLength(chatPrefixes2);
588 integer i;
589
590 for (i = 0; i < length; i += 2)
591 {
592 string pName = llList2String(chatPrefixes2, i);
593 integer pLength = llStringLength(pName);
594
595 if (pName == ((string) channel + " " + llGetSubString(candidate, 0, pLength)))
596 {
597// prefix = pName;
598 prefix = candidate;
599 scripts = llParseString2List(llList2String(chatPrefixes2, i + 1), ["|"], []);
600// words = [llGetSubString(candidate, pLength + 1, -1)] + llList2List(words, 1, -1);
601 words = llList2List(words, 1, -1);
602 i = length;
603 }
604 }
605 }
606 }
607//llOwnerSay(llDumpList2String(scripts, "|"));
608//llSay(0, prefix);
609
610 // Finally found it, process it.
611 if ([] != scripts)
612 {
613 integer length = llGetListLength(scripts);
614 integer wordLength = llGetListLength(words);
615 integer i;
616
617 // Wont put up with laggy scripts. Use a prefix if you insist on using local chat.
618// if ((0 == channel) && ("" == prefix))
619// return;
620
621 // The problem with this loop is that if a bunch of scripts are wanting the same commands,
622 // then it's not efficient. In the expected use cases, that should not me much of a problem.
623 for (i = 0; i < length; ++i)
624 {
625 string script = llList2String(scripts, i);
626 integer theseCommands = llListFindList(chatCommands, [script + (string) channel]);
627
628 if (0 <= theseCommands)
629 {
630 list commands = llParseStringKeepNulls(llList2String(chatCommands, theseCommands + 1), ["|"], []);
631 integer thisCommand = llListFindList(commands, [llList2String(words, 0)]);
632//llOwnerSay(llDumpList2String(words, "~"));
633//llOwnerSay(llDumpList2String(commands, "~"));
634//llSay(0, (string) thisCommand);
635
636 if (0 != (thisCommand % 2))
637 thisCommand = -1;
638 if (0 <= thisCommand)
639 {
640 list result = [channel, name, realId, message, prefix, llList2String(commands, thisCommand)];
641 string argsTypes = llList2String(commands, thisCommand + 1);
642 integer argsLength = llStringLength(argsTypes);
643 integer required = 0;
644 integer optional = 0;
645 integer multiple = -1;
646// integer oldStyle = FALSE;
647
648 if (0 < argsLength) // Arguments expected.
649 {
650 required = (integer) llGetSubString(argsTypes, 0, 0);
651 optional = (integer) llGetSubString(argsTypes, 1, 1);
652
653 {
654 list arguments = []; // type, required, extra
655 integer a;
656 integer argsCount = 0;
657 integer w = 1;
658
659 for (a = 0; a < argsLength; a++)
660 {
661 string type = llGetSubString(argsTypes, a, a);
662 string TYPE = llToUpper(type);
663 integer isNeeded = (TYPE == type);
664 string extra = "";
665
666 if (isNeeded)
667 ++required;
668 else
669 ++optional;
670 // Sort out the extra string for those that support it.
671 if (("C" == TYPE) || ("X" == TYPE))
672 {
673 string subbie = llGetSubString(argsTypes, a + 1, -1);
674 integer comma = llSubStringIndex(subbie, ",");
675
676 if (-1 == comma)
677 {
678 extra = llGetSubString(argsTypes, a + 1, -1);
679 a = argsLength;
680 }
681 else
682 {
683 extra = llGetSubString(argsTypes, a + 1, a + 1 + comma - 1);
684 a += comma + 1;
685 }
686 }
687 arguments += [TYPE, isNeeded, extra];
688 ++argsCount;
689 }
690 for (a = 0; a < argsCount; ++a)
691 {
692 string TYPE = llList2String (arguments, (a * 3) + 0);
693 integer isNeeded = llList2Integer(arguments, (a * 3) + 1);
694 string extra = llList2String (arguments, (a * 3) + 2);
695 string value = "";
696
697 // a animation, b bodypart, clothing, g gesture, m landmark, o object, sound, t texture.
698 // c notecard, x script, Extension is what follows up to the next comma, or end of line.
699 // f float, i integer, k key, r rotation, s rest of string, v vector.
700 // n name of avatar or NPC. Full two word variety, no other checking done.
701 // l local name, could be just first or last name, but they need to be in the sim.
702 if ("F" == TYPE)
703 value = (string) llList2Float(words, w);
704 else if ("I" == TYPE)
705 value = (string) llList2Integer(words, w);
706 else if ("K" == TYPE)
707 value = (key) llList2String(words, w);
708 else if (("R" == TYPE) || ("V" == TYPE))
709 {
710 string next = llList2String(words, w);
711
712 if ("<" == llGetSubString(next, 0, 0))
713 {
714 integer l;
715
716 for (l = 0; (w + l) < wordLength; ++l)
717 {
718 value += next;
719 if (">" == llGetSubString(value, -1, -1))
720 {
721 w += l;
722 l = wordLength; // BREAK!
723 }
724 if (("V" == TYPE) && (3 <= l))
725 l = wordLength; // BREAK!
726 else if (("R" == TYPE) && (4 <= l))
727 l = wordLength; // BREAK!
728 next = llList2String(words, w + 1 + l);
729 }
730 if (">" != llGetSubString(value, -1, -1)) // Seems OpenSim at least can cast partial vectors.
731 value = "";
732 if ("V" == TYPE)
733 {
734 vector v = (vector) value;
735 value = (string) v;
736 }
737 else if ("R" == TYPE)
738 {
739 rotation r = (rotation) value;
740 value = (string) r;
741 }
742 }
743 }
744 else if ("L" == TYPE)
745 {
746 string first = llList2String(words, w);
747 string last = llList2String(words, w + 1);
748
749 if (osIsUUID(first))
750 value = first;
751 else if ("" != last)
752 {
753 list avatars = [llGetOwner(), ZERO_VECTOR, llKey2Name(llGetOwner())] + osGetAvatarList(); // Strided list, UUID, position, name.
754 integer avaLength = llGetListLength(avatars);
755 string aName = llToLower(first + " " + last);
756 integer n;
757
758 for (n = 0; n < avaLength; n +=3)
759 {
760 if (llToLower(llList2String(avatars, n + 2)) == aName)
761 {
762 value = llKey2Name(llList2String(avatars, n));
763 ++w;
764 n = avaLength; // BREAK!
765 }
766 }
767 }
768 if (("" == value) && ("" != first)) // Try scanning the sim for a matching first or last name.
769 {
770 list candidates = [];
771 list avatars = [llGetOwner(), ZERO_VECTOR, llKey2Name(llGetOwner())] + osGetAvatarList(); // Strided list, UUID, position, name.
772 integer avaLength = llGetListLength(avatars);
773 integer n = llSubStringIndex(first, "'");
774
775 // Check if we are searching for multiples.
776 // There can be only one multiple, and it should be the first,
777 // so skip multiples checking if multiple is set already.
778 if ((-1 != n) && (-1 == multiple))
779 {
780 multiple = a;
781 first = llGetSubString(first, 0, n - 1);
782 }
783 last = llToLower(first);
784 for (n = 0; n < avaLength; n +=3)
785 {
786 list names = llParseString2List(llToLower(llList2String(avatars, n + 2)), [" "], []);
787
788 if ((llList2String(names, 0) == last) || (llList2String(names, 1) == last))
789 candidates += [llList2String(avatars, n)];
790 }
791 avaLength = llGetListLength(candidates);
792 if (0 == avaLength)
793 llSay(0, "No one matching the name " + first + " here.");
794 else if ((1 < avaLength) && (-1 == multiple))
795 llSay(0, "More than one matching the name " + first + " here.");
796 else if (-1 != multiple)
797 value = llDumpList2String(candidates, "|");
798 else
799 value = llKey2Name(llList2String(candidates, 0));
800 }
801 }
802 else if ("N" == TYPE)
803 {
804 string first = llList2String(words, w);
805 string last = llList2String(words, w + 1);
806
807 if (osIsUUID(first))
808 value = first;
809 else if ("" != last)
810 {
811 value = first + " " + last;
812 ++w;
813 }
814 }
815 else // The rest are "rest of string".
816 {
817 if (w < wordLength)
818 result += [llDumpList2String(llList2List(words, w, -1), " ")];
819 w = wordLength;
820 }
821 result += [value];
822 ++w;
823 if (w > wordLength)
824 a = argsCount; // BREAK!
825 }
826
827 // Put the rest of the words back together as "rest of message".
828 if (w < wordLength)
829 result += [llDumpList2String(llList2List(words, w, -1), " ")];
830//llSay(0, "ARGUMENTS for " + llList2String(words, 0) + " = " + llDumpList2String(arguments, "|"));
831//llSay(0, "RESULTS " + llDumpList2String(result, "~"));
832 }
833 }
834
835 if ((1 + required) > wordLength)
836 {
837 // bitch
838 if (id != realId)
839 llInstantMessage(realId, "Not enough required arguments in a command from your object " + llKey2Name(id) + " The command was - " + message);
840 else
841 llInstantMessage(realId, "Not enough required arguments in your command. The command was - " + message);
842 }
843 else
844 {
845 // RETURNS incoming channel | incoming name | incoming key | incoming message | prefix | command | list of arguments | rest of message
846 if (-1 != multiple)
847 {
848 list candidates = llParseString2List(llList2String(result, 6 + multiple), ["|"], []);
849 integer candiLength = llGetListLength(candidates);
850 integer c;
851
852 for (c = 0; c < candiLength; ++c)
853 {
854 result = llListReplaceList(result, [llList2String(candidates, c)], 6 + multiple, 6 + multiple);
855 llMessageLinked(LINK_SET, UTILITIES_CHAT_DONE, llDumpList2String(result, LIST_SEP), (key) script);
856 }
857 }
858 else
859 llMessageLinked(LINK_SET, UTILITIES_CHAT_DONE, llDumpList2String(result, LIST_SEP), (key) script);
860 }
861 }
862 }
863 }
864 }
865 }
866}
867
868startNextRead()
869{
870 if (0 < llGetListLength(settingsCards))
871 {
872 settingsName = llList2String(settingsCards, 0);
873 settingsKey = llList2Key(settingsCards, 1);
874 settingsLine = 0;
875 settingsQueryID = llGetNotecardLine(settingsName, settingsLine); // request first line
876 settingsCards = llDeleteSubList(settingsCards, 0, 1);
877 }
878 else
879 {
880 settingsName = ".settings";
881 settingsKey = NULL_KEY;
882 settingsQueryID = NULL_KEY;
883 }
884}
885
886list readThisLine(string data)
887{
888 list result = [];
889
890 data = llStringTrim(data, STRING_TRIM);
891 if ((0 < llStringLength(data)) && ("#" != llGetSubString(data, 0, 0)))
892 {
893 list commands = llParseStringKeepNulls(data, [";"], []);
894 list new = [];
895 string newCommand = "";
896 integer length = llGetListLength(commands);
897 integer i;
898
899 for (i = 0; i < length; ++i)
900 {
901 string command = llList2String(commands, i);
902
903 // Check for line continuation. I think. lol
904 if ("\\" == llGetSubString(command, -1, -1))
905 newCommand += llGetSubString(command, 0, -2) + ";";
906 else
907 {
908 command = llStringTrim(newCommand + command, STRING_TRIM);
909 if (0 < llStringLength(command))
910 new += [command];
911 newCommand = "";
912 }
913//llOwnerSay("|" + newCommand + "|" + command + "|");
914 }
915
916 length = llGetListLength(new);
917 for (i = 0; i < length; ++i)
918 {
919 string name;
920 string value = llList2String(new, i);
921 integer equals = llSubStringIndex(value, "=");
922
923 name = "";
924 if (0 <= equals)
925 {
926 name = llStringTrim(llGetSubString(value, 0, equals - 1), STRING_TRIM_TAIL);
927 if ((equals + 1) < llStringLength(value))
928 value = llStringTrim(llGetSubString(value, equals + 1, -1), STRING_TRIM_HEAD);
929 else
930 value = "";
931 }
932 else
933 {
934 name = value;
935 value = "";
936 }
937 result += [name, value];
938 }
939 }
940 ++settingsLine;
941 return result;
942}
943
944init()
945{
946 llMessageLinked(LINK_SET, UTILITIES_RESET_DONE, "", llGetInventoryKey(llGetScriptName()));
947 // Pointless in OpenSim, always reports 16384. Pffft
948 //llOwnerSay("Free memory " + (string) llGetFreeMemory() + " in " + llGetScriptName());
949 llSetTimerEvent(MENU_TIMEOUT);
950}
951
952
953default
954{
955 state_entry()
956 {
957 init();
958 }
959
960 on_rez(integer param)
961 {
962 init();
963 }
964
965 attach(key attached)
966 {
967 init();
968 }
969
970 listen(integer channel, string name, key id, string message)
971 {
972 myListen(channel, name, id, message);
973 }
974
975 // Handle commands from other scripts.
976 // Negative odd values of num are the commands, return num - 1 as the result.
977 link_message(integer sender_num, integer num, string value, key id)
978 {
979 list input = llParseStringKeepNulls(value, [LIST_SEP], []);
980
981 if (UTILITIES_RESET == num) // Request stuff to be reset
982 {
983 if ("reset" == value) // Request us to be reset.
984 llResetScript();
985 resetPrimShit();
986 llMessageLinked(LINK_SET, num - 1, value, id);
987 }
988 else if (UTILITIES_READ == num) // Request a notecard to be read.
989 {
990 list result = [];
991 integer length = osGetNumberOfNotecardLines(value);
992
993 settingsLine = 0;
994 while (settingsLine <= length)
995 {
996 result += readThisLine(osGetNotecardLine(value, settingsLine));
997 if (0 == (settingsLine % 500)) // Don't send too many at once.
998 {
999 integer percent = (integer) ((((float) settingsLine) / ((float) length)) * 100.0);
1000
1001 llSay(0, "Reading '" + value + "' " + (string) percent + "% done.");
1002 // Sending a negative line to try to avoid triggering foreign scripts.
1003 // Sending from -1000 downwards to avoid triggering our scripts.
1004 llMessageLinked(LINK_SET, -1000 - settingsLine, llDumpList2String([value] + result, LIST_SEP), id);
1005 result = [];
1006 }
1007 }
1008 // Send the last batch.
1009 if (0 != llGetListLength(result))
1010 llMessageLinked(LINK_SET, -1000 - settingsLine, llDumpList2String([value] + result, LIST_SEP), id);
1011 llMessageLinked(LINK_SET, UTILITIES_READ_DONE, value, id);
1012 }
1013 else if (UTILITIES_SUBSTITUTE == num) // Request a param substitution.
1014 {
1015 llMessageLinked(LINK_SET, num - 1, substitute([substitute(input, "%"), "n\n", "t\t", "\\\\", "\"\""], "\\"), id);
1016 }
1017 else if (UTILITIES_NEXT_WORD == num) // Get the next word
1018 {
1019 llMessageLinked(LINK_SET, num - 1, llDumpList2String(nextWord(llList2String(input, 0), llList2String(input, 1), llList2String(input, 2)), LIST_SEP), id);
1020 }
1021 else if (UTILITIES_MENU == num) // Request big menu to be displayed
1022 {
1023 startMenu(id, input);
1024 }
1025 else if (UTILITIES_CHAT == num)
1026 {
1027 // channel list | owner list | prefix list | command list
1028 list channels = llParseString2List(llList2String(input, 0), ["|"], []);
1029 list owners = llParseString2List(llList2String(input, 1), ["|"], []);
1030 list prefixes = llParseString2List(llList2String(input, 2), ["|"], []);
1031 string commands = llList2String(input, 3);
1032 integer chLength = llGetListLength(channels);
1033 integer i;
1034
1035 for (i = 0; i < chLength; ++i)
1036 {
1037 string channel = llList2String(channels, i);
1038 integer j;
1039
1040 if ("" != commands)
1041 {
1042//llSay(0, "ADDING " + channel + "= " + commands);
1043 integer oLength = llGetListLength(owners);
1044 integer pLength = llGetListLength(prefixes);
1045 integer found = llListFindList(channelHandles, [channel]);
1046
1047 if ("0" == channel)
1048 llOwnerSay("WARNING: Script using local chat for commands may cause lag - " + llKey2Name(id));
1049 chatChannels = addChatScripts(chatChannels, channel, (string) id, 1);
1050 chatCommands = addChatScripts(chatCommands, (string) id + channel, commands, 2);
1051 for (j = 0; j < oLength; ++j)
1052 chatOwners = addChatScripts(chatOwners, llList2String(owners, j) + channel, (string) id, 1);
1053
1054 for (j = 0; j < pLength; ++j)
1055 {
1056 string prefix = llList2String(prefixes, j);
1057
1058 if (" " == llGetSubString(prefix, -1, -1))
1059 chatPrefixes = addChatScripts(chatPrefixes, channel + " " + llGetSubString(prefix, 0, -2), (string) id, 1);
1060 else
1061 chatPrefixes2 = addChatScripts(chatPrefixes2, channel + " " + prefix, (string) id, 1);
1062 }
1063
1064 // Do this last, so all the rest is setup already.
1065 if (0 > found)
1066 {
1067//llOwnerSay("LISTEN " + channel + " " + llList2String(owners, j));
1068// TODO - Closing and reopening that channel if details change.
1069 // NOTE - only the FIRST owner is supported.
1070 // TODO - this is not right anyway.
1071 // If a channel gets more than one owner from different invocations of this command,
1072 // then it should re open the listener.
1073 if (1 == oLength)
1074 channelHandles += [channel, llListen((integer) channel, "", llList2Key(owners, 0), "")];
1075 else
1076 channelHandles += [channel, llListen((integer) channel, "", NULL_KEY, "")];
1077 }
1078 }
1079 else // if ("" != commands)
1080 {
1081 integer index = llListFindList(chatCommands, [(string) id + channel]);
1082 // Yes, I know, UUIDs are a fixed length, but there's talk of using SHA1 hashes instead.
1083 integer keyLength = llStringLength((string) id);
1084 integer length;
1085
1086 chatChannels = delChatScripts(chatChannels, channel, (string) id, 1);
1087 if (0 <= index)
1088 chatCommands = llDeleteSubList(chatCommands, index, index + 1);
1089
1090 length = llGetListLength(chatOwners);
1091 for (j = 0; j < length; j += 2)
1092 {
1093 string this = llList2String(chatOwners, j);
1094
1095 if (llGetSubString(this, keyLength, -1) == channel)
1096 chatOwners = delChatScripts(chatOwners, this, (string) id, 1);
1097 }
1098
1099// TODO - go through chatPrefixes/2 removing script from any on this channel
1100
1101 if (0 > llListFindList(chatChannels, [channel]))
1102 {
1103 length = llGetListLength(channelHandles);
1104 for (j = 0; j < length; j += 2)
1105 {
1106 if (llList2String(channelHandles, j) == channel)
1107 {
1108 llListenRemove(llList2Integer(channelHandles, j + 1));
1109 channelHandles = llDeleteSubList(channelHandles, j, j + 1);
1110 length -= 2;
1111 j -= 2;
1112 }
1113 }
1114 }
1115//llOwnerSay("owners " + llDumpList2String(chatOwners, "^"));
1116//llOwnerSay("channels " + llDumpList2String(chatChannels, "^"));
1117//llOwnerSay("prefixes " + llDumpList2String(chatPrefixes, "^"));
1118//llOwnerSay("prefixes2 " + llDumpList2String(chatPrefixes2, "^"));
1119//llOwnerSay("commands " + llDumpList2String(chatCommands, "^"));
1120 } // if ("" != commands)
1121 } // for (i = 0; i < chLength; ++i)
1122
1123 }
1124 else if (UTILITIES_CHAT_FAKE == num)
1125 {
1126 myListen(llList2Integer(input, 0), llList2String(input, 1), llList2Key(input, 2), llList2String(input, 3));
1127 }
1128 }
1129
1130 touch_start(integer num)
1131 {
1132 integer length = llGetListLength(registeredMenus);
1133 integer i;
1134
1135 // Scan through the list, checking if those scripts still exist in inventory.
1136 for (i = length - 1; i >= 0; --i)
1137 {
1138 string this = llList2String(llParseStringKeepNulls(llList2String(registeredMenus, 0), ["|"], []), 1);
1139
1140 if (INVENTORY_NONE == llGetInventoryType(this))
1141 {
1142 registeredMenus = llDeleteSubList(registeredMenus, i, i);
1143 --length;
1144 }
1145 }
1146 for (i = 0; i < num; ++i)
1147 {
1148 key id = llDetectedKey(i);
1149 string desc = llList2String(llGetObjectDetails(llGetLinkKey(llDetectedLinkNumber(i)), [OBJECT_DESC]), 0);
1150
1151 // If there's a description, then it's likely a scriptlet.
1152 if ("" != desc)
1153 myListen(0, llKey2Name(id), id, desc); // TODO - the problem here is that the first argument is a channel,
1154 // and we don't know which channel to fake.
1155 // Maybe use the debug channel as a wildcard?
1156 else if (1 == length) // Only one registered, select it directly.
1157 {
1158 llMessageLinked(LINK_SET, UTILITIES_MENU_DONE, llDumpList2String([id, ""], LIST_SEP),
1159 llList2String(llParseStringKeepNulls(llList2String(registeredMenus, 0), ["|"], []), 2));
1160 }
1161 else if (0 != length) // More than one, put up a menu of them.
1162 startMenu(NULL_KEY, [id, INVENTORY_NONE, "Choose a function :"] + registeredMenus);
1163 // If there's zero registered menus, then do nothing.
1164 }
1165 }
1166
1167 timer()
1168 {
1169 integer length = llGetListLength(menus);
1170 float time = llGetTime();
1171 integer i;
1172
1173 // Run through menus, removing any that timed out.
1174 for (i = 0; i < length; i += MENU_STRIDE)
1175 {
1176 if (time > (llList2Float(menus, i + MENU_TIME) + MENU_TIMEOUT))
1177 {
1178 integer menuHandle = llList2Integer(menus, i + MENU_HANDLE);
1179
1180 llSay(0, "Menu for " + llKey2Name(llList2String(menus, i + MENU_USER)) + " timed out.");
1181 if (menuHandle)
1182 llListenRemove(menuHandle);
1183 menus = llDeleteSubList(menus, i, i + MENU_STRIDE - 1);
1184 length = llGetListLength(menus);
1185 i -= MENU_STRIDE;
1186 }
1187 }
1188 }
1189}
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 @@
1A bunch of stuff copied from the script, since it's too close to the 64Kb limit.
2Below the license is some rough instructions.
3
4// onefang's utilites version 3.0
5// Read a complete settings notecard and send settings to other scripts.
6// Also other useful functions.
7
8// Copyright (C) 2007 David Seikel (onefang rejected).
9//
10// Permission is hereby granted, free of charge, to any person obtaining a copy
11// of this software and associated documentation files (the "Software"), to
12// deal in the Software without restriction, including without limitation the
13// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
14// sell copies of the Software, and to permit persons to whom the Software is
15// furnished to do so, subject to the following conditions:
16//
17// The above copyright notice and this permission notice shall be included in
18// all copies of the Software and its Copyright notices. In addition publicly
19// documented acknowledgment must be given that this software has been used if no
20// source code of this software is made available publicly. This includes
21// acknowledgments in either Copyright notices, Manuals, Publicity and Marketing
22// documents or any documentation provided with any product containing this
23// software. This License does not apply to any software that links to the
24// libraries provided by this software (statically or dynamically), but only to
25// the software provided.
26//
27// Please see the COPYING-PLAIN for a plain-english explanation of this notice
28// and it's intent.
29//
30// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
31// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
32// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
33// THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
34// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
35// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
36//
37// As a special exception to the above conditions, the Second Life user known
38// as Winter Ventura may ignore all permissions and conditions and provide her
39// own.
40
41// All scripts in this object will get link messages with -
42// num = -1000 - line number in the notecard.
43// message = a list,
44// settings card name,
45// name of this setting,
46// data for this setting (may be "").
47// id = scriptKey as passed to us by the calling script.
48// Settings are in the format "setting=data" with whitespace ignored.
49// # as first non space character means ignore the line.
50// For best efficiency, keep notecards short, 0.1 seconds per line read.
51// You can seperate settings with a ";", and include a literal ";" with "\;".
52// Objects using this must be tolerant of having their settings passed
53// to them at any time, even multiple times.
54// Other scripts should ignore settings that are meaningless to them,
55// but can complain if settings they know about are broken.
56//
57// Other commands are passed in link messages in the form -
58// llMessageLink(LINK_SET, UTILITIES_COMMAND, llDumpList2String(argumentList, LIST_SEP), scriptKey);
59// UTILITIES_COMMAND is one of the commands listed below.
60// argumentList is a list of the arguments for the command.
61// For those commands with one or less arguments, a dumped list is not needed.
62// scriptKEy is llGetInventoryKey(llGetScriptName()) from the caller script.
63// It's used by the caller to make sure it gets it's returned message and not that of other scripts.
64
65// llGetNotecardLine() delays for 0.1 seconds, truncates at 255 chars, no warning, 64KiB notecard limit.
66// llResetOtherScript("SettingsReaderAndUtilities") to restart this from another script.
67
68// TODO (LSL permitting) -
69// Use numbers for the returned chat commands.
70// These numbers are set by the caller.
71// They only should set a base number, that gets incremented for the other commands.
72// Performance monitor.
73// onefangs special boolean parser, put it in there somewhere.
74// More complex parsing -
75// No = required, just parse first word as key, rest as data.
76// Does not work so well for "Avatar Name=key".
77// Other comment types, embedded comments.
78
79// type channel owner prefix commands menu scripts users
80// emoter 12+123 only none no 1 owner
81// translator 1+2+3 only none no 1 owner
82// online HUD 5 only fixed+ no 1 owner
83// online IRC 0 +IRC fixed+ no 1 owner+IRC nick
84// hug 1 only config later 1 owner+hugee
85// TeddyPorter 2 owners TeddyPorter fixed+ yes 1+ owners+bookers+occupier+group+users
86// collar 0+x owners *|an per script yes several owner+secowners+group+sub+everyone
87
88// Online IRC is TODO'd to move away from the need to listen to local chat.
89// Collars are icky, but mired in historical precedence.
90// TeddyPorter is the only other one with an open listener, and it uses a prefix to filter because of that.
91
92
93
94integer isKey(key thisKey)
95{//by: Strife Onizuka
96 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)
97 return (thisKey == NULL_KEY); // key is valid AND equal to NULL_KEY (return 1 or TRUE), or is not valid (return 0 or FALSE)
98}
99
100key forceKey(key thisKey)
101{//force a string or key to be a valid key assuming you want invalids to become NULL_KEY
102 if (thisKey) return thisKey;
103 return NULL_KEY;
104}
105
106// If the above key checking turns out to be wrong, do it the hard way.
107integer isKeyHard(key thisKey)
108{
109 integer i;
110
111 if (llStringLength(thisKey) != 36)
112 return FALSE;
113 // Hyphenation tests:
114 if (llGetSubString(thisKey, 8, 8) != "-")
115 return FALSE;
116 if (llGetSubString(thisKey, 13, 13) != "-")
117 return FALSE;
118 if (llGetSubString(thisKey, 18, 18) != "-")
119 return FALSE;
120 if (llGetSubString(thisKey, 23, 23) != "-")
121 return FALSE;
122 // Hex test:
123 // Remove dashes (fixed, thanks Kek :-))
124 thisKey = llDeleteSubString(llDeleteSubString(llDeleteSubString(llDeleteSubString((string) thisKey, 23, 23), 18, 18), 13, 13), 8, 8);
125
126 for (i = 0; i < 32; ++i)
127 {
128 string char = llGetSubString(thisKey, i, i);
129
130 if ((0 == ((integer) ("0x" + char))) && ("0" != char))
131 return FALSE;
132 }
133 return TRUE; // Passed all tests:
134}
135
136// Send an avatar key request to the server.
137addKeyRequest(key script, string type, string name, string extra)
138{
139 keyRequests += [script, type, name, extra, llHTTPRequest("http://w-hat.com/name2key?terse=1&name=" + llEscapeURL(name), [], "")];
140}
141
142
143
144
145 // Check if anything changed.
146 // Including - adding inv, deleting inv, change name or desc of inv, saving notecard, recompiling script.
147 // Not including - script reset, no-copy inv is dragged out, inv drop by non owner.
148 //changed(integer change)
149 //{
150 //if (CHANGED_INVENTORY & change)
151 //init(settingsName);
152 //}
153
154 // Deal with each notecard line.
155 dataserver(key query_id, string data)
156 {
157 if (query_id == settingsQueryID)
158 {
159 if (data != EOF)
160 {
161 readThisLine(data);
162 settingsQueryID = llGetNotecardLine(settingsName, settingsLine);
163 }
164 else
165 {
166 llMessageLinked(LINK_SET, UTILITIES_READ_DONE, settingsName, settingsKey);
167 startNextRead();
168 }
169 }
170 }
171
172 http_response(key id, integer status, list meta, string body)
173 {
174 integer i;
175 integer length = llGetListLength(keyRequests) / KEYS_STRIDE;
176
177 for (i = 0; i <= length; ++i)
178 {
179 integer thisRequest = i * KEYS_STRIDE;
180
181 if ((NULL_KEY != id) && (llList2Key(keyRequests, thisRequest + KEYS_ID) == id))
182 {
183 string script = llList2String(keyRequests, thisRequest + KEYS_SCRIPT);
184 string type = llList2String(keyRequests, thisRequest + KEYS_TYPE);
185 string name = llList2String(keyRequests, thisRequest + KEYS_NAME);
186 string extra = llList2String(keyRequests, thisRequest + KEYS_EXTRA);
187 key result = (key) body;
188
189 keyRequests = llDeleteSubList(keyRequests, thisRequest, thisRequest + KEYS_STRIDE - 1);
190 i -= KEYS_STRIDE;
191 length -= KEYS_STRIDE;
192
193 if (status == 499)
194 {
195 llOwnerSay("name2key request timed out for " + name + ". Trying again.");
196 addKeyRequest(script, type, name, extra);
197 }
198 else if (status != 200)
199 {
200 llOwnerSay("The internet exploded!! Trying again.");
201 addKeyRequest(script, type, name, extra);
202 }
203 else if ((!isKey(result)) || ("" == body))
204 llOwnerSay("No key found for " + name);
205 else if (36 != llStringLength(body))
206 llOwnerSay("Server broken for " + name);
207 else
208 llMessageLinked(LINK_SET, UTILITIES_AVATAR_KEY_DONE, llDumpList2String([type, name, result, extra], LIST_SEP), script);
209 }
210 }
211 }
212
213
214 else if (UTILITIES_AVATAR_KEY == num)
215 {
216 string type = llList2String(input, 0);
217 string name = llList2String(input, 1);
218 string extra = llList2String(input, 2);
219 key result = NULL_KEY;
220
221// TODO add a check for the key already existing in name. Should be done by caller if caller worries about speed.
222
223 if (NULL_KEY == result)
224 addKeyRequest(id, type, name, extra);
225 else
226 llMessageLinked(LINK_SET, num - 1, llDumpList2String([type, name, result, extra], LIST_SEP), id);
227 }
228