gopher.moo September 21, Version 1.1 Copyright (c) 1992, 1993, Larry Masinter, Erik Ostrom All Rights Reserved Permission granted to use this software for non-commercial purposes; we'd like to be notified of any enhancements, applications, or bug-fixes in the software. This is a general MOO interface to Gopher. To use it, you need a MOO server. The MOO software is available from . It's split up into 2 files: network.moo includes $network and $gopher, and this file includes the generic gopher slate. $network and $gopher are now part of LambdaCore, so if you're starting a new MOO you don't need them. Be sure you're running LambdaMOO 1.7.5 or later, and that you have networking enabled. Create the slate: @create $thing named Generic Gopher Slate and then edit the following script to replace #50122 with the number of Generic Gopher Slate. Using the Generic slate, use 'goto host port on generic slate' and 'remember on slate' to set up the default 'top level' menu of new gopher slates. Change log: Version 0.1 -- initial release Version 0.2 -- use $network:open instead of raw o_n_c validity check on host names limit on retrievals add (some) documentation to $gopher.room verbs gopher rooms have a remembered set add CSO phone book entries use .desclines property instead of :description (exam won't spam). add gopher lists Version 0.3: change gopher room to portable slate subsumes notes differential cache timeout (shorter for failures) Version 0.4: Include $network in release (Thanks to unattributed JHM programmers) Add 'controlled' state on slate. Slates show headers when they update if watcher isn't controller. Version 0.5: clean up the $network dump some Version 0.6: Version 0.7: very minor patches: more general mailing, hopefully better installation instructions Version 1.0: after port to LambdaMOO, simplify $network, $gopher Version 1.1: split $network and $gopher into separate file ================================================================ @prop #50122."value" {} r @prop #50122."stack" {} r @prop #50122."busy" 0 r @prop #50122."remembered" {} r @prop #50122."desclines" {} r @prop #50122."seen" {} r @prop #50122."length" 20 rc @prop #50122."help_msg" {} rc ;;#50122.("help_msg") = {"Moving around:", " pick on slate", " select the given menu item (either a number or partial name).", " If it is a text item, it will show it to you.", " on slate", " e.g., 12 on slate. You can omit `pick' when chosing items", " by their number.", " back slate [for n]", " go back up a level; with n supplied, goes back n levels", " reset slate", " reset slate to the default list of `remember'-ed nodes", " goto host [port [path]] on slate", " make a direct jump to a specified host. Please be careful --", " at the moment this slows everyone down if the host isn't valid.", "", "Controlling noise:", " ignore slate", " stop listening when other people fiddle with the slate", " watch slate", " start watching while other people fiddle with the slate", " show slate to ", " show the contents of the slate to someone even if they're not watching", "", "Modifying the `reset' list:", " remember [] on slate", " adds item to the list you get when you `reset' slate", " will prompt you for title", " remember on slate", " remembers the current menu choice rather than any ", " particular item", " forget on slate", " (Only when the slate is `reset')", " deletes the given item", "", "In long menus and text:", " next [] on slate", " prev [] on slate", " move you forward/backward in the set of visible menu items.", " You can give a `number of pages' to move forward.", " read slate", " show you the entire contents of the slate", " read on slate", " if is a text menu, it will show the text without", " actually changing the state of the slate.", "", "Miscellaneous:", " stack slate", " show stack, where `back' will go", " details on slate", " show host, port number, and selection string for a given item. ", " mailme slate", " if you have a valid registration address: send mail with the", " slate contents to your email address.", " mailme on slate", " this will mail you the , if it is text.", "", "When you first make a gopher slate, you will need to use `goto'", "and then `remember' to set up the default list of nodes."} @prop #50122."locked" 0 r @prop #50122."ignoring" {} r @prop #50122."watching" {} r @prop #50122."controlled" #-1 r @prop #50122."work_with_msg" "%N % to work with %d." rc ;;#50122.("description") = "A laptop size computer, with various controls on it." @verb #50122:"p*ick" any on this rxd @program #50122:pick "pick on slate"; " entry is either a line number or an initial substring of a line description"; " select that entry: if it is a menu, go to that node. If it is a search,"; " asks you for the search term & does the search."; " Some kinds of nodes are not implemented."; if (this:_textp() || (!(this.stack || this.remembered))) return player:tell("There's nothing to pick."); endif if (this:busy("picking")) return; endif if (!(which = this:match_choice(dobjstr))) "match_choice took care of it."; this:busy(0); return; endif if ((tostr(tonum(dobjstr)) == dobjstr) && (!({player, @this:_place()} in this.seen))) player:tell($string_utils:pronoun_sub("Oooops, perhaps you should look at the %t first.")); this:busy(0); return; endif parse = $gopher:parse(this.value[which]); desc = this.desclines[which]; this:announce_op("%N % '", desc, "' on the %t."); this:do_pick(@parse); return; . @verb #50122:"reset" this none none rxd @program #50122:reset "reset slate"; " reset the slate to its set of 'remembered' selections"; if (why = this:is_locked(player)) return player:tell($string_utils:pronoun_sub("Sorry, %t seems to be "), why, "."); elseif (this:busy("resetting")) return; endif this:announce_op("%N % the %t."); this.seen = {}; this:set_pointer(); this:busy(0); . @verb #50122:"pop back" any any any rxd @program #50122:pop "back this [by ]"; " move back up the gopher stack to the previous menu"; " or previous N menus."; n = 1; if (iobjstr && (!(iobjstr == tostr(n = tonum(iobjstr))))) return player:tell("Sorry, '", iobjstr, "' doesn't look like a number."); endif if (length(this.stack) < n) player:tell("Sorry, there aren't ", n, " levels to go back."); return; endif if (this:busy("going back")) return; endif this:announce_op("%N % back up ", (n == 1) ? "a level" | tostr(n, " levels"), " on the %t."); this:set_pointer(@this.stack[n + 1..length(this.stack)]); this:busy(0); . @verb #50122:"location_string" this none this rx @program #50122:location_string "location_string([location])"; "A nice-looking version of the location provided, or current location."; loc = ((args && args[1]) || this.stack[1]); where = loc[1]; if (st = loc[4]) "human readable string"; return ((st[2..length(st)] + " (from ") + where) + ")"; return (where + ": ") + st[2..length(st)]; endif if (loc[3]) return ((loc[3] + " (from ") + where) + ")"; return (where + ": ") + loc[3]; endif return where; . @verb #50122:"stack" this none none rxd @program #50122:stack "stack slate"; " show a summary of the gopher stack"; max = 0; if (!this.stack) return player:tell($string_utils:pronoun_sub("%T is at the top level.")); endif for x in (this.stack) max = max(max, length(x[1])); endfor max = (max + 6); for x in ($list_utils:reverse(this.stack)) summary = $gopher:summary(x); player:tell($string_utils:left(summary[1], max), " ", summary[2]); endfor . @verb #50122:"busy" this none this @program #50122:busy "interlock for caching -- mark cache busy or clear; return true of interlock failed"; if (args[1]) if ((args[1] != "reading") && (why = this:is_locked(player))) player:tell($string_utils:pronoun_sub("Sorry, %t seems to be "), why, "."); return 1; endif "make player running this watch it."; this.watching = setadd(this.watching, player); "set busy"; if (this.busy && (this.busy[1] > time())) player:tell("***Sorry, ", this.name, " is busy ", this.busy[2], " for ", this.busy[3], " -- wait a bit."); return 1; else this.busy = {time() + (60 * 5), args[1], player.name, task_id()}; return 0; endif else this.busy = 0; return 0; endif . @verb #50122:"match_choice" this none this @program #50122:match_choice "match_choice(input string)"; "returns the index of the choice, or 0."; "is noisy."; if (this:_textp()) player:tell($string_utils:pronoun_sub("%T is looking at a text node and has no choices.")); return 0; endif input = args[1]; which = $code_utils:tonum(input); len = length(value = this.value); if (typeof(which) == NUM) if ((which < 1) || (which > len)) player:tell("Sorry, ", input, " isn't a number between 1 and ", len, "."); return 0; endif return which; else exact = (partial = {}); for choice in [1..len] valchoice = value[choice][2..index(value[choice], " ") - 1]; if (input == valchoice) exact = {@exact, choice}; elseif (index(valchoice, input) == 1) partial = {@partial, choice}; endif endfor if (length(exact) > 1) player:tell("I'm not sure whether you meant ", $string_utils:english_list(exact, "", " or "), "."); return 0; elseif (exact) return exact[1]; elseif (length(partial) > 1) player:tell("I'm not sure whether you meant ", $string_utils:english_list(partial, "", " or "), "."); return 0; elseif (partial) return partial[1]; else player:tell("Sorry, there is no choice named ", $string_utils:print(input), "."); return 0; endif endif . @verb #50122:"jump goto" any on this rxd @program #50122:jump "goto [socket] on slate"; " given an explicit host name and optional socket, attempt to open a"; " gopher connection to that socket"; words = $string_utils:words(dobjstr); if (!words) player:tell("Usage: ", verb, " [socket]", prepstr ? tostr(" on ", iobjstr) | ""); return; endif host = words[1]; socket = 70; if (length(words) > 1) socket = tonum(words[2]); if (socket < 3) player:tell("The value '", words[2], "' is not a valid socket."); return; endif endif path = ""; if (length(words) > 2) path = dobjstr[(index(dobjstr, words[2]) + length(words[2])) + 1..length(dobjstr)]; endif if (this:busy(tostr("jumping to ", host, " socket ", socket))) return; endif this:announce_op(tostr("%N % to ", host, " socket ", socket, path ? " " | "", path, " on the %t.")); parse = {host, socket, path, "1"}; this:set_pointer(parse, @this:_textp() ? listdelete(this.stack, 1) | this.stack); this:busy(0); . @verb #50122:"details" any on this rxd @program #50122:details if (!(which = this:match_choice(dobjstr))) "match_choice took care of it."; return; endif parse = $gopher:parse(this.value[which]); sel = parse[4]; if (sel) for x in ({"Type=" + sel[1], "Name=" + sel[2..length(sel)], "Path=" + parse[3], "Host=" + parse[1], "Port=" + tostr(parse[2]), "#"}) player:tell(x); endfor else player:tell("**** ERROR, ", which, " is not a valid entry."); endif . @verb #50122:"set_pointer" this none this rx @program #50122:set_pointer if (!args) value = this.remembered; else value = $gopher:get(@args[1]); endif if (!value) this:busy(0); this:announce_op($gopher:interpret_error(value)); return 0; endif if (value[1][1] == "3") this:busy(0); this:announce_op("The gopher request results in an error:"); for x in (value) this:announce_op(": ", x ? x[2..length(x)] | x); endfor return 0; endif if (args && (args[1][4][1] == "0")) "text node"; desc = value; else desc = {}; cnt = 1; for x in (value) $command_utils:suspend_if_needed(0); type = $gopher:type(x[1]); if (type == "text") type = ""; else type = ((" (" + type) + ")"); endif tab = index(x, " "); label = x[2..tab - 1]; desc = {@desc, tostr(cnt, ". ", label, type)}; cnt = (cnt + 1); endfor endif $command_utils:suspend_if_needed(0); this.desclines = desc; this.stack = args; this.value = value; this:busy(0); this:show_results(); return 1; . @verb #50122:"do_pick" this none this @program #50122:do_pick "do_pick(host, port, path, string) -- take parsed output & interact with user as appropriate."; string = args[4]; if ((!string) || index("1?", type = string[1])) "menu"; this:set_pointer(args, @this.stack); elseif (type == "7") player:tell("Search for what? Enter search line or @abort:"); search = read(); if (search != "@abort") this:announce_op("%N % for ", search, " on %t."); this:set_pointer({args[1], args[2], (args[3] + " ") + search, args[4]}, @this.stack); else this:busy(0); this:announce_op("%N % not to search."); endif elseif (type == "3") this:busy(0); this:announce_op("%N chose an error line."); elseif (type == "0") "slates can point at text nodes"; this:set_pointer(args, @this.stack); elseif (type == "2") search = $command_utils:read("one of 'name=' 'phone=' 'email='"); if (!match(search, "[a-z]+=[a-z0-9@-]+")) this:busy(0); player:tell((search == "@abort") ? "No search." | ("Invalid query: " + search)); return; endif this:announce_op("%N % for ", search, " on %t."); this:set_pointer({args[1], args[2], (args[3] + " query ") + search, args[4]}, @this.stack); elseif ($object_utils:has_property(player, "gopher_local") && player.gopher_local) this:busy(0); notify(player, tostr("#$# gopher ", args[1], " ", args[2], " ", args[4], " ", args[3])); else this:busy(0); this:announce_op("Type ", type, " (", $gopher:type(type), ") gopher requests not implemented."); if (type == "8") player:tell("**** telnet ", args[1], (args[2] in {23, 0}) ? "" | (" " + tostr(args[2]))); if (args[3]) player:tell(" log in as: ", args[3]); endif endif endif . @verb #50122:"remember" any on this rxd @program #50122:remember "remember on "; " add the entry (or this menu) to the 'remembered set' for this room."; " use 'remembered' to retrieve the set."; if (!this.stack) return player:tell("Sorry, remembering remembered nodes doesn't work."); endif if (dobjstr == "") parse = this.stack[1]; desc = "the current menu"; elseif (choice = this:match_choice(dobjstr)) parse = $gopher:parse(this.value[choice]); desc = this.desclines[choice]; else "Match_choice took care of it."; return; endif parse[4] = (parse[4][1] + $command_utils:read("description for " + desc)); this.remembered = {@this.remembered, $gopher:unparse(@parse)}; this:announce_op("%N % ", desc, " on the %t as ", parse[4][2..length(parse[4])], "."); . @verb #50122:"forget delete" any on this rxd @program #50122:forget "forget on slate"; " erase an entry from the 'remembered set'"; " only works if you're looking at the 'remembered set'"; if (this.stack) player:tell("You're not looking at the top."); return; endif if (!(choice = this:match_choice(dobjstr))) return; endif this:announce_op("%N % '", this.desclines[choice], "' on the %t."); this.remembered = listdelete(this.remembered, choice); this:set_pointer(); . @verb #50122:"look_self" this none this @program #50122:look_self if (this.stack) sum = $gopher:summary(this.stack[1]); player:tell(this:titlec(), ": ", sum[1], " ", sum[2]); else player:tell(this:titlec()); endif player:tell_lines(this:description()); this:_tell_desc(); state = ""; if (valid(this.controlled)) state = (($string_utils:pronoun_sub("The %t is being controlled by ") + this.controlled:title()) + "."); endif if ((busy = this:_is_busy()) || state) player:tell(state ? state + " " | "", busy ? $string_utils:pronoun_sub(tostr("The %t is busy ", this.busy[2], " for ", this.busy[3], ".")) | ""); endif . @verb #50122:"_tell_desc" this none this @program #50122:_tell_desc who = (args ? args[1] | player); plen = ((length(args) > 1) ? args[2] | this.length); header = ((length(args) > 2) && args[3]); if (this:_textp()) text = this:text(); len = length(text); if ((!plen) || (len <= plen)) $command_utils:suspend_if_needed(0); "6/24/93 change tell_lines to notify_lines to reduce lag."; if (header) who:tell("--------------- ", this.name, "-----"); who:notify_lines(text); who:tell("--------------- ", this.name, "-----"); else who:notify_lines(text); endif return; endif offset = this:offset(); npages = ((len / plen) + 1); thispage = ((offset / plen) + 1); if ((offset != 1) || header) who:tell("--", thispage, " of ", npages, "----- 'prev on ", this.name, "' for previous----"); endif end = ((offset + plen) - 1); who:tell_lines(text[offset..min(len, end)]); if ((len > end) || header) who:tell("--", thispage, " of ", npages, "----- 'next on ", this.name, "' for more --------"); endif return; endif this.seen = setadd(this.seen, {who, @this:_place()}); len = length(this.desclines); if (header) who:tell("--------------- ", this.name, "-----"); endif if (plen && (len > plen)) offset = this:offset(); who:tell_lines(this.desclines[offset..min((offset + this.length) - 1, len)]); nxt = ("next on " + this.name); prv = ("previous on " + this.name); who:tell("---- '", (offset == 1) ? nxt | (((offset + plen) > len) ? prv | (((("'" + nxt) + "' or '") + prv) + "'")), "' to see additional choices (", len, " total) ---"); else who:tell_lines(this.desclines || {$string_utils:pronoun_sub("%T is empty right now.")}); if (header) who:tell("--------------- ", this.name, "-----"); endif endif . @verb #50122:"next prev*ious" any on this rxd @program #50122:next if (this:busy("reading")) "can't 'next' if it is busy"; return; endif this:busy(0); n = (tonum(dobjstr) || 1); if (verb != "next") n = (-n); verb = "previous"; endif offset = this:offset(); new = (offset + (n * this.length)); if (new < 1) if (offset == 1) return player:tell("You're already at the beginning."); else new = 1; endif elseif (new > length(this.desclines)) return player:tell("You're already at the end."); endif this:announce_op("%N % at the ", verb, " ", this:_textp() ? "page" | "results", " on the %t."); this:offset(new); this:show_results(); . @verb #50122:"initialize" this none this @program #50122:initialize if ((caller == this) || $perm_utils:controls(caller_perms(), this)) "don't call this unless you mean it."; this.seen = {}; this.desclines = {}; "The default is that slate's inherit the 'remembered' from their parent. This means, though, that they're initially blank but have to be 'reset' to fire up. See :do_reset"; "this.remembered = {}"; this.busy = 0; this.stack = {}; this.watching = {}; this.controlled = #-1; pass(@args); endif . @verb #50122:"announce_op" this none this @program #50122:announce_op msg = tostr(@args); player:tell($string_utils:pronoun_sub(msg, $you)); if (this.location != player) this.location:announce($string_utils:pronoun_sub(msg)); endif return; "announcing only to watching"; if (watching = setremove($set_utils:intersection(this.watching, this.location:contents()), player)) msg = $string_utils:pronoun_sub(msg); for x in (watching) x:tell(msg); endfor endif . @verb #50122:"_place" this none this @program #50122:_place return this.stack && this.stack[1][1..3]; . @verb #50122:"_textp" this none this @program #50122:_textp return this.stack && index("02", this.stack[1][4][1]); . @verb #50122:"r*ead" any any any rxd @program #50122:read if ((!argstr) || ((dobj == this) && (!prepstr))) this:_tell_desc(player, 0); elseif (which = this:match_choice((($code_utils:short_prep(prepstr) == "on") && (iobj == this)) ? dobjstr | argstr)) where = $gopher:parse(this.value[which]); if (index("02", where[4][1])) this:announce_op("%N % '", this.desclines[which], "' on the %t."); $gopher:show_text(player, 0, 0, @where); player:tell("-------"); else player:tell("Item '", this.desclines[which], "' isn't text and can't be read."); endif else player:tell("Read what?"); endif . @verb #50122:"lock unlock" this none none rxd @program #50122:lock this.locked = (verb == "lock"); this:announce_op("%N %<", $string_utils:lowercase(verb), "s> %t."); . @verb #50122:"text" this none this @program #50122:text return this.value; "don't update slates"; . @verb #50122:"update" this none none rxd @program #50122:update if (this:busy("updating", 1)) return; endif this:announce_op("%N % %t."); if (this.stack) $gopher:clear_cache(@this.stack[1]); endif this:set_pointer(@this.stack); . @verb #50122:"_mail_text" this none this @program #50122:_mail_text if (this:_textp()) return this.value; else text = {}; for x in (this.value) parse = $gopher:parse(x); sel = parse[4]; text = {@text, "Type=" + sel[1], "Name=" + sel[2..length(sel)], "Path=" + parse[3], "Host=" + parse[1], "Port=" + tostr(parse[2]), "#"}; endfor return text; endif . @verb #50122:"show_results" this none this @program #50122:show_results "after a selection is made, this verb is used to show the results; usually to 'player'"; inhere = ($object_utils:isa(this.location, $room) ? this.location:contents() | {player}); for x in (this.watching = setadd(this.watching, player)) $command_utils:suspend_if_needed(0); if (x in inhere) this:_tell_desc(x, this.length, player != x); else this.watching = setremove(this.watching, x); endif endfor . @verb #50122:"ignore watch" this none none rxd @program #50122:ignore was = (player in this.watching); this.watching = ((verb == "watch") ? setadd(this.watching, player) | setremove(this.watching, player)); is = (player in this.watching); if (was == is) player:tell("You already were ", (verb == "watch") ? "watching" | "ignoring", " ", this:title(), "."); elseif (this.location == player) player:tell("You start to ", verb, " ", this:title(), "."); else $you:say_action(("%N % to " + verb) + " %t."); endif . @verb #50122:"show" this to any rxd @program #50122:show if (!valid(iobj)) return player:tell("I don't see '", iobjstr, "' here."); endif $you:say_action("%N % %t to %i."); this:_tell_desc(iobj, this.length, 1); . @verb #50122:"_is_busy" this none this @program #50122:_is_busy if (this.busy) if (this.busy[1] > time()) return 1; else this.busy = 0; endif endif return 0; . @verb #50122:"control" this none none rxd @program #50122:control if (this.controlled == player) player:tell("You are already controlling ", this:title(), "."); return; endif from = (valid(this.controlled) ? (" from " + this.controlled:title()) + "." | "."); if (this.location != player) this.location:announce_all_but({player}, $string_utils:pronoun_sub("%N takes the controls of %t"), from); endif player:tell("You take the controls of ", this:title(), from); this.controlled = player; . @verb #50122:"release" this none none rxd @program #50122:release if (this.controlled == player) $you:say_action("%N % the controls of %t."); this.controlled = #-1; else player:tell("You weren't holding the controls of ", this.name, "."); endif . @verb #50122:"is_locked" this none this @program #50122:is_locked "is this locked?"; if (this.locked) return "locked"; elseif (valid(this.controlled) && (this.controlled != args[1])) if (this.location in {this.controlled, this.controlled.location}) return "controlled by " + this.controlled.name; else this.controlled = #-1; endif endif return 0; . @verb #50122:"match_command" this none this rx @program #50122:match_command "match_command(vrb, dlist, plist, ilist)"; "return true if this object can handle the command, false otherwise"; "vrb - name of the verb the player typed"; "dlist - list of objspecs that this command matches"; "plist and ilist - likewise for prepspecs, iobjspecs"; if ((player.focus_object == this) && (this.location in {player, player.location})) vrb = args[1]; dlist = args[2]; plist = args[3]; ilist = args[4]; if (((vrb in {"pick", "jump", "goto", "details", "remember", "forget", "delete", "next", "prev", "previ", "previo", "previou", "previous"}) && ("none" in plist)) && ("none" in ilist)) return 1; elseif (((vrb in {"read", "ignore", "watch"}) && ("none" in dlist)) && ("none" in plist)) return 1; elseif (((vrb in {"show"}) && ("none" in dlist)) && ("at/to" in plist)) return 1; elseif ((vrb in {"reset", "stack", "mailme", "lock", "unlock", "update", "control", "release"}) && (!("on top of/on/onto/upon" in plist))) return 1; elseif (((vrb in {"pop", "back"}) && ("none" in dlist)) && (("none" in plist) || ("for/about" in plist))) return 1; endif endif return pass(@args); . @verb #50122:"work" none with this r @program #50122:work "This is a JaysHouseMOO verb -- probably doesn't work on other MOOs without a 'focus' object."; if (valid(player:set_focus_object(this))) $you:say_action(this.work_with_msg); else player:tell("You just can't seem to focus on that."); endif . @verb #50122:"mailme" any any any rxd @program #50122:mailme "mailme note"; if ((caller_perms() != player) && (caller != player)) return player:tell("Someone tried to mail you some text, but it didn't work."); endif if (!player.email_address) return player:tell("Sorry, you don't have a registered email address."); endif if ((!argstr) || ((dobj == this) && (!prepstr))) where = this.stack[1]; elseif (which = this:match_choice((($code_utils:short_prep(prepstr) == "on") && (iobj == this)) ? dobjstr | argstr)) where = $gopher:parse(this.value[which]); endif if (where) player:tell("Mailing ", this:location_string(where), " to ", player.email_address, "."); text = $gopher:_mail_text(where); player:tell("... ", length(text), " lines ..."); text = {tostr("(Mail initiated by ", player.name, " (", player, ") connected from ", $string_utils:connection_hostname(connection_name(player)), " using ", this.name, ")"), @text}; suspend(0); result = $network:sendmail(player.email_address, this:location_string(where), @text); if (result == 0) player:tell("Mail sent successfully."); else player:tell("Mail sending error: ", result, "."); endif else player:tell("Sorry, can't mail this."); endif . @verb #50122:"header" this none this @program #50122:header "used by _tell_desc for prefix & suffix lines"; args[1]:tell("------- ", $string_utils:left($string_utils:pronoun_sub(tostr(@listdelete(args, 1), " ")), args[1]:linelen(), "-")); . @verb #50122:"offset" this none this @program #50122:offset if (!this.stack) return 1; endif menu = this.stack[1]; if (args) if (length(menu) > 4) this.stack[1][5] = args[1]; else this.stack[1] = {@{@menu, "", "", "", ""}[1..4], args[1]}; endif elseif (length(menu) > 4) return menu[5]; else return 1; endif . "***finished loading gopher slate ***