normalized perms on all files, to avoid relying on any stored executable bits in...
[feisty_meow.git] / huffware / huffotronic_scripts / huff-update_client_v20.1.txt
1 
2 // huffware script: huff-update client, by fred huffhines.
3 //
4 // this script is the client side of the update process.  it should reside in an object that
5 // has scripts which should be automatically updated.  it will listen for announcements by
6 // an update server and communicate with the server to ensure that all of its scripts are
7 // the most up to date available with the server.
8 //
9 // this script is licensed by the GPL v3 which is documented at: http://www.gnu.org/licenses/gpl.html
10 // do not use it in objects without fully realizing you are implicitly accepting that license.
11 //
12
13 // global constants...
14
15 integer DEBUGGING = FALSE;  // if TRUE, the script will output status information.
16
17 integer SERVER_IGNORE_TIME = 1200;  // number of seconds between performing an upgrade with the same server.
18
19 integer MAXIMUM_UPDATE_TIME_ALLOWED = 140;  // we allow one upgrade process to take this long overall.
20
21 integer UPDATE_ANNOUNCEMENT_CHANNEL   = -420108;  // used by server to brag about itself.
22 integer OLD_REQUEST_INVENTORY_CHANNEL = -421008;  // used by clients to request an update list.
23
24 string UPDATE_ANNOUNCEMENT_PREFIX   = "#huff-update#";  // first part of any announcement.
25 string REQUEST_INVENTORY_PREFIX     = "#huff-reqinv#";  // first part of request for inventory list.
26 string REPORT_AVAILABLE_SCRIPTS     = "#scripts#";  // server's keyword to let client know script inventory.
27 string REQUEST_SCRIPT_UPDATE        = "#updatego#";  // keyword used by client to request some updates.
28 string SHUT_THEM_DOWN               = "#huffdown#";  // server tells client to stop any non-updater scripts.
29 string READY_TO_UPDATE              = "#listoneeds#";  // the client tells the server the scripts it wants.
30 string SCRIPTS_ARE_CURRENT          = "#gottemthx#";  // client says this when all new scripts are in place.
31 string START_THEM_UP                = "#huffup#";  // server tells client to start up other scripts again.
32 string DONE_UPDATING                = "#finito#";  // the client is done updating.
33 string BUSY_BUSY                    = "#busymuch#";  // a signal that the server is too busy to update us.
34
35 float UPDATE_TIMER_INTERVAL = 2.0;  // interval between checks on our update status.
36
37 integer UPDATER_SCRIPT_PIN = -1231008;  // the hook for our scripts to be modified.
38
39 ///float BUSY_SERVER_PAUSE_TIME = 38.0;  // num seconds to delay when server says it's too busy.
40
41 string UPDATER_PARM_SEPARATOR = "~~~";
42     // three tildes is an uncommon thing to have otherwise, so we use it to separate
43     // our commands in linked messages.
44
45 string SCRIPT_DEPENDENCY_MARK = "DEP";  // signals that a dependency is coming.
46 string ITEM_LIST_SEPARATOR = "``";  // separates dependencies.
47
48 integer MAXIMUM_SERVERS_TRACKED = 32;
49     // we will listen to this many servers before we decide to remove one.
50
51 string CONTINUANCE_MARKER = "...";
52     // a string sent when the update list is too long and needs to be continued in another chat.
53
54 string SERVER_SCRIPT = "a huffotronic update server";
55     // the prefix of our server script that hands out updates.
56
57 // global variables...
58
59 integer inventory_request_channel;  // used for newer version servers to cut down cross chatter.
60 list updaters_heard;  // the update servers we've heard from recently.
61 list last_interactions;  // times of the last update process engaged with the updater.
62 integer update_channel;  // current channel for interaction with specific server.
63 key current_server;  // the updater that is active right now, if any.
64 integer update_start_time;  // when the last update process began.
65 list updates_needed;  // stores the set of scripts that are in need of an update.
66 list known_script_dependencies;  // stores the list of dependency info.
67
68 careful_crankup()
69 {
70     knock_around_other_scripts(TRUE);
71     // clean out the older items and scripts.  we do this after getting everyone running
72     // since we might be whacking ourselves.
73     destroy_older_versions();
74 }
75
76 // reset our variables.
77 initialize()
78 {
79     updaters_heard = [];
80     last_interactions = [];
81     inventory_request_channel = 0;
82     update_channel = 0;
83     current_server = NULL_KEY;
84     llSetTimerEvent(0.0);
85     llSetRemoteScriptAccessPin(UPDATER_SCRIPT_PIN);
86     // a new enhancements; tells the server that this guy has finished an update cycle.  this
87     // only comes into play when the updater script itself has just been updated, but it's
88     // nice for the server to avoid claiming erroneous timeouts occurred.
89     llSay(OLD_REQUEST_INVENTORY_CHANNEL, DONE_UPDATING);
90     llSleep(0.4);  // snooze and repeat to overcome occasionally lossy chats.
91     llSay(OLD_REQUEST_INVENTORY_CHANNEL, DONE_UPDATING);
92 }
93
94 whack_updater_record(key id)
95 {
96     integer prev_indy = find_in_list(updaters_heard, id);
97     if (prev_indy < 0) return;  // not there.
98     updaters_heard = chop_list(updaters_heard, 0, prev_indy - 1)
99             + chop_list(updaters_heard, prev_indy + 1, llGetListLength(updaters_heard) - 1);
100     last_interactions = chop_list(last_interactions, 0, prev_indy - 1)
101             + chop_list(last_interactions, prev_indy + 1, llGetListLength(last_interactions) - 1);
102 }
103
104 // note that this new, lower memory version, depends on the inventory functions returning
105 // items in alphabetical order.
106 scrub_items_by_type(string this_guy, integer inventory_type)
107 {
108     list removal_list;
109     integer outer;
110     for (outer = 0; outer < llGetInventoryNumber(inventory_type); outer++) {
111         string curr = llGetInventoryName(inventory_type, outer);
112         list split = compute_basename_and_version(curr);
113         // make sure there was a comparable version number in this name.
114         if ( (curr != this_guy) && llGetListLength(split)) {
115             string curr_base = llList2String(split, 0);
116             float curr_ver = (float)llList2String(split, 1);
117 //log_it("outer: " + curr_base + " / " + (string)curr_ver);
118             integer inner;
119             for (inner = outer + 1; inner < llGetInventoryNumber(inventory_type); inner++) {
120                 string next_guy = llGetInventoryName(inventory_type, inner);
121                 list comp_split = compute_basename_and_version(next_guy);
122                 if (llGetListLength(comp_split)) {
123                     string comp_base = llList2String(comp_split, 0);
124                     float comp_ver = (float)llList2String(comp_split, 1);
125                     // okay, now we can actually compare.
126                     if (curr_base != comp_base) {
127                         // break out of inner loop.  we are past where the names can matter.
128                         inner = 2 * llGetInventoryNumber(inventory_type);
129                     } else {
130 //log_it("inner: " + comp_base + " / " + (string)comp_ver);
131                         if (curr_ver <= comp_ver) {
132                             // the script at inner index is comparable or better than
133                             // the script at the outer index.
134                             removal_list += curr;
135                         } else {
136                             // this inner script must be inferior to the outer one,
137                             // somehow, which defies our expectation of alphabetical ordering.
138                             removal_list += next_guy;
139                         }
140                     }
141                 }
142             }
143         }
144     }
145
146     // now actually do the deletions.
147     for (outer = 0; outer < llGetListLength(removal_list); outer++) {
148         string to_whack = llList2String(removal_list, outer);
149         if (DEBUGGING)
150             log_it("removing older asset: " + to_whack);
151         llRemoveInventory(to_whack);
152     }
153 }
154
155 // ensures that only the latest version of any script or object is kept in our inventory.
156 destroy_older_versions()
157 {
158     // firstly, iterate across scripts to clean out older versions.
159     scrub_items_by_type(llGetScriptName(), INVENTORY_SCRIPT);
160     // secondly, try to clean out the objects.
161     scrub_items_by_type(llGetScriptName(), INVENTORY_OBJECT);
162     // thirdly, try to clean out the notecards.
163     scrub_items_by_type(llGetScriptName(), INVENTORY_NOTECARD);
164 }
165
166 // sets the object to be listening for update info.
167 // if "just_owner" is true, then we will not listen on the general announcement channel.
168 listen_for_orders(integer just_owner)
169 {
170     if (!just_owner) {
171         // try to hear an update being announced.
172         llListen(UPDATE_ANNOUNCEMENT_CHANNEL, "", NULL_KEY, "");
173     }
174
175     // super secret owner controls.
176     llListen(0, "", llGetOwner(), "");
177 }
178
179 // returns true if this object is a huffotronic updater of some sort.
180 integer inside_of_updater()
181 {
182     return find_substring(llGetObjectName(), "huffotronic") >= 0;
183 }
184
185 // returns true if a script is a version of our update server.
186 integer matches_server_script(string to_check)
187 {
188     return is_prefix(to_check, SERVER_SCRIPT);
189 }
190
191 // stops all the scripts besides this one.
192 knock_around_other_scripts(integer running_state)
193 {
194     integer insider = inside_of_updater();
195     if (running_state == TRUE) {
196         // make sure we crank up the scripts that are new first.  we want to reset them
197         // as well, which we don't want to do for any existing scripts.
198         integer crank_indy;
199         for (crank_indy = 0; crank_indy < llGetListLength(updates_needed); crank_indy++) {
200             string crankee = llList2String(updates_needed, crank_indy);
201             if (find_in_inventory(crankee, INVENTORY_SCRIPT, TRUE) >= 0) {
202                 if (!insider || matches_server_script(crankee)) {
203                     // allow it to run again.
204                     llSetScriptState(crankee, TRUE);
205                     // reset it, to make sure it starts at the top.
206                     llResetOtherScript(crankee);
207                 }
208             }
209         }
210     }
211
212     integer indy;
213     string self_script = llGetScriptName();
214     // we set all other scripts to the running state requested.
215     for (indy = 0; indy < llGetInventoryNumber(INVENTORY_SCRIPT); indy++) {
216         string curr_script = llGetInventoryName(INVENTORY_SCRIPT, indy);
217         if ( (curr_script != self_script)
218             && (!insider || matches_server_script(curr_script)) ) {
219             // this one seems ripe for being set to the state requested.
220             llSetScriptState(curr_script, running_state);
221         }
222     }
223 }
224
225 // a random channel for the interaction with the server.
226 integer random_channel() { return -(integer)(llFrand(800000) + 20000); }
227
228 // make sure that any dependencies for the script with "basename" are added to the list
229 // of requests we make during an update.
230 list add_dependencies(string basename)
231 {
232     list to_return;
233     integer indy;
234     for (indy = 0; indy < llGetListLength(known_script_dependencies); indy++) {
235         list deps = llParseString2List(llList2String(known_script_dependencies, indy),
236             [ITEM_LIST_SEPARATOR], []);
237 //log_it("base=" + llList2String(dep, 0) + " lastver=" + llList2String(dep, 1) + " newdep=" + llList2String(dep, 2));
238         if (basename == llList2String(deps, 0)) {
239             // first off, is this item with new dependencies actually present?
240             integer where = find_in_inventory(basename, INVENTORY_SCRIPT, FALSE);
241             if (where >= 0) {
242                 // we do use the script with deps, but is the dependent item really missing?
243                 where = find_in_inventory(llList2String(deps, 1), INVENTORY_SCRIPT, FALSE);
244                 if (where < 0) {
245                     // we found a dependency match for this script, so we'll ask for the missing item.
246                     if (DEBUGGING)
247                         log_it("missing dep: " + llList2String(deps, 1));
248                     to_return += [ llList2String(deps, 1) ];
249                 }
250             }
251         }
252     }
253     return to_return;
254 }
255
256 // complains if memory seems to be getting tight.
257 test_memory()
258 {
259     if (llGetFreeMemory() < 4096)
260         log_it("mem_free = " + (string)llGetFreeMemory());
261 }
262
263 // starts an update given a list of scripts that the server has available, encoded as
264 // a string in the "encoded_list".
265 integer initiate_update(string encoded_list)
266 {
267     list scripts_avail = llParseString2List(encoded_list, [UPDATER_PARM_SEPARATOR], []);
268     integer continue_listening_for_scripts = FALSE;
269       // if true, we aren't done hearing about available scripts yet.
270     encoded_list = "";
271     // figure out which scripts we need by comparing the list available from the server
272     // against our current inventory.  we only want scripts with newer version numbers.
273     integer sindy;
274     for (sindy = 0; sindy < llGetListLength(scripts_avail); sindy++) {
275         string curr = llList2String(scripts_avail, sindy);
276         if (curr == CONTINUANCE_MARKER) {
277             // this is a special continuation signal.  we need to hear the rest of the list.
278             continue_listening_for_scripts = TRUE;
279         } else if (is_prefix(curr, SCRIPT_DEPENDENCY_MARK)) {
280             // we've found a dependency item.
281             known_script_dependencies += [ llGetSubString(curr, llStringLength(SCRIPT_DEPENDENCY_MARK), -1) ];
282 //log_it("script dep: " + llGetSubString(curr, llStringLength(SCRIPT_DEPENDENCY_MARK), -1));
283         } else {
284             list split = compute_basename_and_version(curr);
285             if (llGetListLength(split) == 2) {
286                 string basename = llList2String(split, 0);
287                 string version = llList2String(split, 1);
288                 split = [];
289                 integer oy_indy;
290 //replace common code with func.
291                 for (oy_indy = 0; oy_indy < llGetInventoryNumber(INVENTORY_OBJECT); oy_indy++) {
292                     list srv_split = compute_basename_and_version
293                         (llGetInventoryName(INVENTORY_OBJECT, oy_indy));
294                     if ( (llGetListLength(srv_split) == 2)
295                             && (basename == llList2String(srv_split, 0))
296                             && ((float)version > (float)llList2String(srv_split, 1)) ) {
297 //                        if (DEBUGGING) {
298                             log_it("i need '" + curr + "' from server " + (string)inventory_request_channel);
299 //                        }
300                         test_memory();
301                         updates_needed += [ curr ];
302                     }
303                 }
304                 for (oy_indy = 0; oy_indy < llGetInventoryNumber(INVENTORY_NOTECARD); oy_indy++) {
305                     list srv_split = compute_basename_and_version
306                         (llGetInventoryName(INVENTORY_NOTECARD, oy_indy));
307                     if ( (llGetListLength(srv_split) == 2)
308                             && (basename == llList2String(srv_split, 0))
309                             && ((float)version > (float)llList2String(srv_split, 1)) ) {
310                         if (DEBUGGING) {
311                             log_it("i need '" + curr + "' from server " + (string)inventory_request_channel);
312                         }
313                         test_memory();
314                         updates_needed += [ curr ];
315                     }
316                 }
317                 for (oy_indy = 0; oy_indy < llGetInventoryNumber(INVENTORY_SCRIPT); oy_indy++) {
318                     list srv_split = compute_basename_and_version
319                         (llGetInventoryName(INVENTORY_SCRIPT, oy_indy));
320                     if ( (llGetListLength(srv_split) == 2)
321                             && (basename == llList2String(srv_split, 0))
322                             && ((float)version > (float)llList2String(srv_split, 1)) ) {
323                         if (DEBUGGING) {
324                             log_it("i need '" + curr + "' from server " + (string)inventory_request_channel);
325                         }
326                         test_memory();
327                         updates_needed += [ curr ];
328                     }
329                 }
330                 updates_needed += add_dependencies(basename);
331             }
332         }
333     }
334     // we skip the next step if we're still waiting to hear about more.
335     if (continue_listening_for_scripts) {
336 //log_it("still listening for more updates...");
337         return FALSE;
338     }
339     if (llGetListLength(updates_needed)) {
340 //log_it("update chan=" + (string)update_channel);
341         llSay(update_channel, REQUEST_SCRIPT_UPDATE);
342         if (DEBUGGING) {
343             log_it("told server " + (string)inventory_request_channel + " that i need updating.");
344         }
345     } else {
346         if (DEBUGGING) {
347             log_it("told server " + (string)inventory_request_channel + " that i am done updating.");
348         }
349         llSay(update_channel, DONE_UPDATING);
350     }
351     return TRUE;
352 }    
353
354 // this alerts the server to our most desired scripts.
355 tell_server_our_wish_list()
356 {
357     llSay(update_channel, READY_TO_UPDATE + wrap_parameters(updates_needed));
358 }
359
360 // checks whether all of the updates needed are present yet.
361 integer check_on_update_presence()
362 {
363     integer indy;
364     for (indy = 0; indy < llGetListLength(updates_needed); indy++) {
365         integer found = find_in_inventory(llList2String(updates_needed, indy), INVENTORY_ALL, TRUE);
366         // any single missing guy means they aren't up to date yet.
367         if (found < 0) {
368             if (DEBUGGING) log_it(llList2String(updates_needed, indy) + " not seen as updated yet.");
369             return FALSE;
370         }
371     }
372     // nothing was detected as missing anymore.
373     return TRUE;
374 }
375
376 // respond to spoken commands from the server.
377 integer process_update_news(integer channel, string name, key id, string message)
378 {
379     if (!channel) {
380         // this is a command.
381         if (message == "ureset") {
382             llResetScript();  // start over.
383         }
384         if (message == "ushow") {
385             integer sindy;
386             integer script_count = llGetInventoryNumber(INVENTORY_SCRIPT);
387             list script_list = [ "scripts--" ];  // first item is just a header.
388             for (sindy = 0; sindy < script_count; sindy++) {
389                 script_list += [ llGetInventoryName(INVENTORY_SCRIPT, sindy) ];
390             }
391             dump_list_to_log(script_list);
392         }
393         return FALSE;  // nothing to do here.
394     }
395     if (!update_channel && (channel == UPDATE_ANNOUNCEMENT_CHANNEL)) {
396 /* never seen.        if (id == llGetKey()) {
397 if (DEBUGGING) log_it("ignoring update from self.");            
398             return FALSE;  // ack, that's our very object.
399         }
400 */
401         if (llStringLength(message) > llStringLength(UPDATE_ANNOUNCEMENT_PREFIX)) {
402             // this is a new style update message.  we can set a different request channel.
403             string just_chan = llDeleteSubString(message, 0, llStringLength(UPDATE_ANNOUNCEMENT_PREFIX) - 1);
404             inventory_request_channel = (integer)just_chan;
405         }
406         integer prev_indy = find_in_list(updaters_heard, id);
407         // find the talker in our list.
408         if (prev_indy >= 0) {
409             // that guy was already heard from.  check when last interacted.
410             integer last_heard = llList2Integer(last_interactions, prev_indy);
411             if (llAbs(llGetUnixTime() - last_heard) < SERVER_IGNORE_TIME) {
412                 return FALSE;  // not time to update with this guy again yet.
413             }
414 //            if (DEBUGGING) { log_it("started listening again to server " + (string)id); }
415             // make sure we think of this as a new updater now.
416             whack_updater_record(id);
417         }
418
419         if (DEBUGGING) { log_it("heard server " + (string)inventory_request_channel + "'s announcement."); }
420         // record our new server.
421         current_server = id;
422         // make a random pause so not all updaters try to crank up at same time.
423         llSleep(randomize_within_range(2.8, 18.2, FALSE));
424
425         if (llGetListLength(updaters_heard) > MAXIMUM_SERVERS_TRACKED) {
426             // oops, this is not good.  we have too many servers now.
427 //hmmm: room for improvement here by tossing out the server that is oldest.
428             updaters_heard = llDeleteSubList(updaters_heard, 0, 0);
429             last_interactions = llDeleteSubList(last_interactions, 0, 0);
430         }
431         
432         // add the talker to our list.
433         updaters_heard += id;
434         last_interactions += llGetUnixTime();
435     
436         // begin the update interaction with this guy.
437         update_channel = random_channel();
438         return TRUE;
439     }
440     if (update_channel && (channel == update_channel) ) {
441         if (is_prefix(message, REPORT_AVAILABLE_SCRIPTS)) {
442             // tasty, this is a list of scripts that can be had.
443             message = llDeleteSubString(message, 0, llStringLength(REPORT_AVAILABLE_SCRIPTS) - 1);
444             if (message == BUSY_BUSY) {
445                 // server has signified that it's too busy (or its owner is a moron) because it is
446                 // claiming it has no scripts at all.
447                 if (DEBUGGING) {
448                     log_it("server " + (string)inventory_request_channel + " is too busy to update us now.");
449                 }
450                 // make it seem like we need to do this one again sooner than normal.
451                 whack_updater_record(id);
452                 // busy server means move no further forward.
453                 return FALSE;
454             }
455             return initiate_update(message);
456         } else if (is_prefix(message, SHUT_THEM_DOWN)) {
457             if (DEBUGGING) { log_it("stopping other scripts."); }
458             knock_around_other_scripts(FALSE);
459             // now that we know for sure the server's ready to update us,
460             // we tell it what we need.
461             tell_server_our_wish_list();
462             return FALSE;
463         } else if (is_prefix(message, START_THEM_UP)) {
464             // let the server know that we've finished, for all intents and purposes.
465             llSay(update_channel, DONE_UPDATING);
466             // we pause a random bit first; we want to ensure we aren't swamping
467             // SL with our inventory loading.
468             llSleep(randomize_within_range(2.5, 8.2, FALSE));
469             if (DEBUGGING) { log_it("starting other scripts."); }
470             careful_crankup();
471             return TRUE;  // change state now.
472 //        } else {
473 //log_it("unknown command on update channel: " + message);
474         }
475     }
476     return FALSE;
477 }
478
479 //////////////
480 // from hufflets...
481
482 integer debug_num = 0;
483
484 // a debugging output method.  can be disabled entirely in one place.
485 log_it(string to_say)
486 {
487     debug_num++;
488     llWhisper(0, llGetScriptName() + " [" + (string)debug_num + "] (" + (string)llGetFreeMemory() + ") " + to_say);
489 }
490
491 // returns a number at most "maximum" and at least "minimum".
492 // if "allow_negative" is TRUE, then the return may be positive or negative.
493 float randomize_within_range(float minimum, float maximum, integer allow_negative)
494 {
495     if (minimum > maximum) {
496         // flip the two if they are reversed.
497         float temp = minimum; minimum = maximum; maximum = temp;
498     }
499     float to_return = minimum + llFrand(maximum - minimum);
500     if (allow_negative) {
501         if (llFrand(1.0) < 0.5) to_return *= -1.0;
502     }
503     return to_return;
504 }
505
506 // returns TRUE if the "pattern" is found in the "full_string".
507 integer matches_substring(string full_string, string pattern)
508 { return (find_substring(full_string, pattern) >= 0); }
509
510 // returns the index of the first occurrence of "pattern" inside
511 // the "full_string".  if it is not found, then a negative number is returned.
512 integer find_substring(string full_string, string pattern)
513 { return llSubStringIndex(llToLower(full_string), llToLower(pattern)); }
514
515 // returns TRUE if the "prefix" string is the first part of "compare_with".
516 integer is_prefix(string compare_with, string prefix)
517 { return find_substring(compare_with, prefix) == 0; }
518
519 // locates the string "text" in the list to "search_in".
520 integer find_in_list(list search_in, string text)
521
522     integer len = llGetListLength(search_in);
523     integer i; 
524     for (i = 0; i < len; i++) { 
525         if (llList2String(search_in, i) == text) 
526             return i; 
527     } 
528     return -1;
529 }
530
531 // returns the portion of the list between start and end, but only if they are
532 // valid compared with the list length.  an attempt to use negative start or
533 // end values also returns a blank list.
534 list chop_list(list to_chop, integer start, integer end)
535 {
536     integer last_len = llGetListLength(to_chop) - 1;
537     if ( (start < 0) || (end < 0) || (start > last_len) || (end > last_len) ) return [];
538     return llList2List(to_chop, start, end);
539 }
540
541 // joins a list of parameters using the parameter sentinel for the library.
542 string wrap_parameters(list to_flatten)
543 { return llDumpList2String(to_flatten, UPDATER_PARM_SEPARATOR); }
544
545 // locates the item with "name_to_find" in the inventory items with the "type".
546 // a value from 0 to N-1 is returned if it's found, where N is the number of
547 // items in the inventory.
548 integer find_in_inventory(string name_to_find, integer inv_type, integer exact_match)
549 {
550     integer num_inv = llGetInventoryNumber(inv_type);
551     if (num_inv == 0) return -1;  // nothing there!
552     integer inv;
553     for (inv = 0; inv < num_inv; inv++) {
554         if (exact_match && (llGetInventoryName(inv_type, inv) == name_to_find) )
555             return inv;
556         else if (!exact_match && is_prefix(llGetInventoryName(inv_type, inv), name_to_find))
557             return inv;
558     }
559     return -2;  // failed to find it.
560 }
561
562 //////////////
563
564 integer MAX_CHAT_LINE = 900;
565     // the most characters we'll try to say in one chat.
566
567 dump_list_to_log(list to_show)
568 {
569     string text = dump_list(to_show);  // get some help from the other version.
570     integer len = llStringLength(text);
571     integer i;
572     for (i = 0; i < len; i += MAX_CHAT_LINE) {
573         integer last_bit = i + MAX_CHAT_LINE - 1;
574         if (last_bit >= len) last_bit = len - 1;
575         string next_line = llGetSubString(text, i, last_bit);
576         llWhisper(0, next_line);
577     }
578 }
579
580 // returns a printable form of the list.
581 string dump_list(list to_show)
582 {
583     integer len = llGetListLength(to_show);
584     integer i;
585     string text;
586     for (i = 0; i < len; i++) {
587         string next_line = llList2String(to_show, i);
588         if (find_substring(next_line, " ") >= 0) {
589             // this guy has a space in it, so quote it.
590             next_line = "\"" + next_line + "\"";
591         }
592         text = text + next_line;
593         if (i < len - 1) text = text + " ";
594     }
595     return text;
596 }
597
598 //////////////
599 // huffware script: auto-retire, by fred huffhines, version 2.8.
600 // distributed under BSD-like license.
601 //   !!  keep in mind that this code must be *copied* into another
602 //   !!  script that you wish to add auto-retirement capability to.
603 // when a script has auto_retire in it, it can be dropped into an
604 // object and the most recent version of the script will destroy
605 // all older versions.
606 //
607 // the version numbers are embedded into the script names themselves.
608 // the notation for versions uses a letter 'v', followed by two numbers
609 // in the form "major.minor".
610 // major and minor versions are implicitly considered as a floating point
611 // number that increases with each newer version of the script.  thus,
612 // "hazmap v0.1" might be the first script in the "hazmap" script continuum,
613 // and "hazmap v3.2" is a more recent version.
614 //
615 // example usage of the auto-retirement script:
616 //     default {
617 //         state_entry() {
618 //            auto_retire();  // make sure newest addition is only version of script.
619 //        }
620 //     }
621 // this script is partly based on the self-upgrading scripts from markov brodsky
622 // and jippen faddoul.
623 //////////////
624 auto_retire() {
625     string self = llGetScriptName();  // the name of this script.
626     list split = compute_basename_and_version(self);
627     if (llGetListLength(split) != 2) return;  // nothing to do for this script.
628     string basename = llList2String(split, 0);  // script name with no version attached.
629     string version_string = llList2String(split, 1);  // the version found.
630     integer posn;
631     // find any scripts that match the basename.  they are variants of this script.
632     for (posn = llGetInventoryNumber(INVENTORY_SCRIPT) - 1; posn >= 0; posn--) {
633         string curr_script = llGetInventoryName(INVENTORY_SCRIPT, posn);
634         if ( (curr_script != self) && (llSubStringIndex(curr_script, basename) == 0) ) {
635             // found a basic match at least.
636             list inv_split = compute_basename_and_version(curr_script);
637             if (llGetListLength(inv_split) == 2) {
638                 // see if this script is more ancient.
639                 string inv_version_string = llList2String(inv_split, 1);  // the version found.
640                 // must make sure that the retiring script is completely the identical basename;
641                 // just matching in the front doesn't make it a relative.
642                 if ( (llList2String(inv_split, 0) == basename)
643                     && ((float)inv_version_string < (float)version_string) ) {
644                     // remove script with same name from inventory that has inferior version.
645                     llRemoveInventory(curr_script);
646                 }
647             }
648         }
649     }
650 }
651 //
652 // separates the base script name and version number.  used by auto_retire.
653 list compute_basename_and_version(string to_chop_up)
654 {
655     // minimum script name is 2 characters plus a version.
656     integer space_v_posn;
657     // find the last useful space and 'v' combo.
658     for (space_v_posn = llStringLength(to_chop_up) - 3;
659         (space_v_posn >= 2) && (llGetSubString(to_chop_up, space_v_posn, space_v_posn + 1) != " v");
660         space_v_posn--) {
661         // look for space and v but do nothing else.
662     }
663     if (space_v_posn < 2) return [];  // no space found.
664     // now we zoom through the stuff after our beloved v character and find any evil
665     // space characters, which are most likely from SL having found a duplicate item
666     // name and not so helpfully renamed it for us.
667     integer indy;
668     for (indy = llStringLength(to_chop_up) - 1; indy > space_v_posn; indy--) {
669         if (llGetSubString(to_chop_up, indy, indy) == " ") {
670             // found one; zap it.  since we're going backwards we don't need to
671             // adjust the loop at all.
672             to_chop_up = llDeleteSubString(to_chop_up, indy, indy);
673         }
674     }
675     string full_suffix = llGetSubString(to_chop_up, space_v_posn, -1);
676     // ditch the space character for our numerical check.
677     string chop_suffix = llGetSubString(full_suffix, 1, llStringLength(full_suffix) - 1);
678     // strip out a 'v' if there is one.
679     if (llGetSubString(chop_suffix, 0, 0) == "v")
680         chop_suffix = llGetSubString(chop_suffix, 1, llStringLength(chop_suffix) - 1);
681     // if valid floating point number and greater than zero, that works for our version.
682     string basename = to_chop_up;  // script name with no version attached.
683     if ((float)chop_suffix > 0.0) {
684         // this is a big success right here.
685         basename = llGetSubString(to_chop_up, 0, -llStringLength(full_suffix) - 1);
686         return [ basename, chop_suffix ];
687     }
688     // seems like we found nothing useful.
689     return [];
690 }
691 //
692 //////////////
693
694 // end hufflets.
695 //////////////
696
697 // no huffotronic trap state for startup, because this script will actually
698 // run (and is expected) inside a huffotronic updater object.
699
700 default
701 {
702     state_entry()
703     {
704         auto_retire();  // only allow the most recent revision.
705         initialize();
706         state awaiting_commands;
707     }
708 }
709
710 state awaiting_commands
711 {
712     state_entry()
713     {
714         if (DEBUGGING) log_it("<awaiting_commands>");
715         careful_crankup();  // we always start by getting everyone running.
716         current_server = NULL_KEY;  // forget previous server.
717         listen_for_orders(FALSE);
718         inventory_request_channel = 0;  // no inventory request channel either.
719         update_channel = 0;  // no channel currently.
720         updates_needed = [];  // we know of no needs right now.
721         known_script_dependencies = [];  // no deps either.
722     }
723
724     state_exit() { llSetTimerEvent(0.0); }
725
726     listen(integer channel, string name, key id, string message)
727     {
728         if ((id != llGetOwner()) && (llGetOwnerKey(id) != llGetOwner())) {
729             return;  // must be same owner to ensure proper perms.
730         }
731         if (process_update_news(channel, name, id, message))
732             state establish_private_channel;
733     }
734 }
735
736 state establish_private_channel
737 {
738     state_entry()
739     {
740         if (DEBUGGING) log_it("<establish_private_channel>");
741         llListen(update_channel, "", current_server, "");
742         listen_for_orders(TRUE);
743         if (inventory_request_channel)
744             llSay(inventory_request_channel, REQUEST_INVENTORY_PREFIX + (string)update_channel);
745         else
746             llSay(OLD_REQUEST_INVENTORY_CHANNEL, REQUEST_INVENTORY_PREFIX + (string)update_channel);
747         llSetTimerEvent(MAXIMUM_UPDATE_TIME_ALLOWED);
748     }
749
750     state_exit() { llSetTimerEvent(0); }
751
752     listen(integer channel, string name, key id, string message)
753     {
754         if ((id != llGetOwner()) && (llGetOwnerKey(id) != llGetOwner())) {
755             return;  // must be same owner to ensure proper perms.
756         }
757         if (process_update_news(channel, name, id, message)) {
758             // ready for a state change, but what kind?
759             if (llGetListLength(updates_needed)) {
760 //log_it("have a list of updates now.");
761                 state performing_update;
762             } else {
763 //log_it("no updates needed in list, going back");
764                 state awaiting_commands;
765             }
766         }
767     }
768     
769     timer() {
770         if (DEBUGGING) {
771             log_it("timed out establishing channel with server " + (string)inventory_request_channel);
772         }
773         whack_updater_record(current_server);
774         state awaiting_commands;
775     }
776
777     on_rez(integer parm) { state default; }
778 }
779
780 state performing_update
781 {
782     state_entry()
783     {
784         // must re-listen after a state change.
785         llListen(update_channel, "", current_server, "");
786         listen_for_orders(TRUE);
787         if (DEBUGGING) log_it("<performing_update>");
788         llSetTimerEvent(UPDATE_TIMER_INTERVAL);
789         update_start_time = llGetUnixTime();
790     }
791
792     state_exit() { llSetTimerEvent(0.0); }
793
794     listen(integer channel, string name, key id, string message)
795     {
796         if ((id != llGetOwner()) && (llGetOwnerKey(id) != llGetOwner())) {
797             return;  // must be same owner to ensure proper perms.
798         }
799         if (process_update_news(channel, name, id, message)) {
800             // normal finish of update process.
801             state awaiting_commands;
802         }
803     }
804
805     timer() {
806         if (llGetListLength(updates_needed) == 0) {
807 //log_it("nothing to update, leaving perform state.");
808             state awaiting_commands;  // we've got nothing to do.
809         } else {
810             // see if all our requested scripts are there yet; if not, we're not done updating.
811             integer ready = check_on_update_presence();
812             if (ready) {
813                 if (DEBUGGING) log_it("reporting scripts are current.");
814                 llSay(update_channel, SCRIPTS_ARE_CURRENT);
815             }
816         }
817         if (llAbs(update_start_time - llGetUnixTime()) >= MAXIMUM_UPDATE_TIME_ALLOWED) {
818             if (DEBUGGING) { log_it("timeout during update process with server " + (string)inventory_request_channel); }
819             whack_updater_record(current_server);
820             state awaiting_commands;
821         }
822     }
823
824     on_rez(integer parm) { state default; }
825 }
826