User:Mauve/TinyFugue

From Discworld MUD Wiki
Jump to: navigation, search

I use an old, simple yet powerful client to play, TinyFugue, with lots and lots of macros.

This page covers some tips and tricks, as well as code snippets and aliases to make one's experience more enjoyable.

General

Ensure you're running the latest version of TinyFugue (5.0 beta 8; there probably won't be any more releases at this point), patched with GMCP support, if possible.

Read the bundled documentation, it's really worth it. If you're new to this, you will have to do it multiple times to really understand it all. Once you're somewhat comfortable, pick a coding style and stick to it. There are many ways to do stuff and having one particular style with help future you read and edit your macros. My style (and therefore recommendation) is as follows:

  • Use macros (/def) for triggers and functions you typically don't call directly, use aliases for the rest.
  • Always declare all variables before you use them, either with /set globally or with /let at the beginning of your macro or alias.
  • Use /_echo if you don't need /echo. Which is almost never.
  • Indent with four spaces.
  • Put macro and alias definitions on one line, the body on the next. Yes, even if it's short, one word alias.
  • In macro and alias bodies, each call goes on its own line. Yes, even simple /ifs.
  • Always use /test to change variables after the initial definition.
  • If assigning a value when declaring a variable, separate the name and the value with =.
  • Use just one style of quoting; I use double quotes.
  • Be consistent regarding the order of macro arguments; I go with priority, trigger/hook, expression, fallthru, attributes, name.
  • Don't quote macro arguments unless it's -t or, in some cases, -h
  • Don't repeat yourself; move common code into macros and then call those macros.
  • Try to minimize the number of triggers and their conditional expressions.
  • Don't use fallthrus unless you need them.
  • Use only regexes or simple-style matching. If it's a simple, exact trigger, go with simple.
  • In regexes, prefix all with (?-i) and be as explicit as possible. Use anchors, use atomic groups.

That should get you started.

Basics

Most of the code here assumes some MUD-side settings, namely:

  • rows 32768, or some other high number, don't use the MUD-side pager, you can scroll in your client.
  • cols 999, this is the maximum the MUD currently allows. It's easier to match things if it's on a single line. Let the client wrap it. Still, some lines might not fit and you will have to deal with multiline matching.
  • options output map {glance|glancecity|look|lookcity} = bottom, or top if you prefer; left won't work with high cols.
  • options output shortinlong = on, this is just prettier if you rewrite it. More on that below.
  • colour combat gray, optional but fades the combat spam a little and is useful if you want to match combat messages.
  • monitor Hp: $hp$($maxhp$) Gp: $gp$($maxgp$) Xp: $xp$ Burden: $burden$ Alignment: $align$, which will display alignment updates in your monitor, assuming you care about alignment being kept passively up-to-date.

Now, that should be all I rely on. On the client side I have my configuration split into three files:

  • vars.tf, which declares most default global variables and is loaded first.
  • setup.tf, which holds status bar, logging and connection definitions.
  • main.tf, which holds everything else and can be safely reloaded without messing with stuff.

I should emphasize that while I try to keep most things generic and rely on variables, my configuration mostly works just for Discworld and a single character.

Snippets

.tfrc

/require alias.tf
/require complete.tf
/require tools.tf

/load ~/tf/vars.tf
/load ~/tf/setup.tf
/load ~/tf/main.tf

vars.tf

; Connection
/set user=yourname
/set pass=yourpassword
/set server=discworld.starturtle.net
/set port=4242
; Default GMCP setup
/set gmcp=0
/set gmcp_debug=0
/set gmcp_echo=0
; Logging
/set logpatch=~/log/discworld
/eval /set logfile=$[ftime("%Y%m%d.log")]
; Default attributes for the status bar
/set default_status_attr=xCbg236
; Status bar helpers
/set alignment_pos=-1
; Vitals and room information
/set hp=0
/set maxhp=0
/set gp=0
/set maxgp=0
/set xp=0
/set rate=0
/set burden=0
/set alignment=unknown
/set identifier=unknown
/set name=unknown
/set visibility=unknown
/set kind=unknown
; Heartbeat increments
/set hpinc=2
/set gpinc=4
; Status bar / control variables
/set activity=
/set danger=
/set ready=
; Heartbeat and status updates
/set heartbeat=0
/set monitor=0
/set ratecounter=0
; Goes idle after 150 heartbeats of no activity
/set default_heartbeat=150
; Declares combat over after two heartbeats of no monitor
/set default_monitor=2
; Recalculates xp rate every five heartbeats
/set default_ratecounter=5
; Notify me when I get more than this much xps
/set xpthreshold=100

setup.tf

; Sets the window title
/xtitle Discworld MUD
; Basic client setup
; Compile all macros immediately
/set defcompile=1
; Set some reasonable highlighting, if you use it
/set hiliteattr=B
; Keep some reasonable number of lines in the buffer
/set histsize=8192
; Just one line for the input, thanks
/set isize=1
; No one needs mail checking
/set maildelay=0
; Default to regexes, glob is evil
/set matching=regexp
; Be pedantic and report all warnings; helps you write better code
/set pedantic=1
; Don't pause after exiting processes executed with /sh, it's annoying
/set shpause=0
; Don't do any magic substitutions on input
/set sub=0
; Don't send empty lines to the server; this shouldn't happen but just in case
/set snarf=1
; Expand tabs into this many spaces
/set tabsize=4
; Pretty delimeter when scrolling back
/set textdiv_str=~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
; Well, time format, in /recall and such
/set time_format=%H:%M:%S
; Don't warn that you're using regexes the way they're meant to be used, duh
/set warn_curly_re=0
; Wrap lines on our end
/set wrap=1
; Indent wrapped lines by this many spaces
/set wrapspace=2
; Status bar configuration
/eval /set status_attr=%{default_status_attr}
/set status_height=2
/set status_pad=
/set status_var_name=strcat(toupper(substr(name, 0, 1)), substr(name, 1))
/set status_var_kind=substr(kind, 0, 1)
/set status_var_alignment=(alignment_pos:=strchr(alignment, " "), strcat(substr(alignment, 0, 1), (alignment_pos != -1 ? substr(alignment, alignment_pos + 1, 1) : "")))
/set status_var_danger=monitor ? "x" : ""
/set status_var_ready=!heartbeat ? "z" (gp >= maxgp ? "r" : "")
/set status_var_activity=activity !~ "" ? substr(activity, 0, 1) : ""
/status_add -r0 -c -A "[ " name :2 kind:1 :1 identifier:8 " ]"
/status_add -r1 -c -A "[ " "h:" hp:-4 "/" maxhp:-4 :2 "g:" gp:-3 "/" maxgp:-3 :2 "x:" xp:-8 :2 "r:" rate:-7 :2 "b:" burden:-2 "%" :2 "a:" alignment :2 @more:8:BCbg238 :1 danger:1 :1 ready:1 :1 activity:1 " ]"
; Connect to an LP-style MUD for automatic login, even though we disable LP later
/eval /addlp dw %{user} %{pass} %{server} %{port}
; Enable logging on connecting; fallthru so that we can extend this in main.tf
/def -msimple -hCONNECT -F logging = \
    /sys ln -sf %{logpath}/%{logfile} %{logpath}/last.log %;\
    /log -wdw %{logpath}/%{logfile}

The above logs you in with your credentials from vars.tf and starts logging on startup or when you you /connect later. It sets up some reasonable behaviour and creates a status bar that looks somewhat like this:

[ Junction north of the Mermaid Bridge                                            o c918da25 ]
[ h:1908/1908  g:595/595  x: 4665075  r: 421771  b:22%  a:e                            x r c ]

It tells you the room name, whether it's inside, outside or underwater, and the first ten characters of the room ID. It tells you your vitals, with alignment being shortened to one or two letters. It tells you whether you're in combat (x), at full GPs (r) or idle (z), and if you have some activity mode set (in this case, c; shows the initial letter of the activity name).

What's an activity, you ask? Well, the next section should explain it.

main.tf

This is where all the fun happens. Keybindings, triggers, aliases, GMCP handling, heartbeat emulation and anything else you can think of. The file is meant to be reloaded when you change something. A handy /reload macro is provided for it.

; Sets up basic hotkeys
/def key_tag = /completion
; Keypad
/set keypad=on
; Kill alias; attacks with minions
/def key_nkp/ = ok
; Burial alias
/def key_nkp* = b
/def key_nkp- = u
/def key_nkp+ = d
; Heal me!
/def key_nkp0 = h
/def key_nkp1 = sw
/def key_nkp2 = s
/def key_nkp3 = se
/def key_nkp4 = w
; Mapping
/def key_nkp5 = m
/def key_nkp6 = e
/def key_nkp7 = nw
/def key_nkp8 = n
/def key_nkp9 = ne
; Shielding alias
/def key_nkp. = mshi
; Activity!  The /activity macro decides what happens when you press Enter
/def key_nkpEnt = /activity
; Same as above with Alt
; A different kill alias; attacks directly
/def -b'^[O3o' = k
; A different burial alias (ritual burial)
/def -b'^[O3j' = ee
; Glancing in the directions
/def -b'^[O3m' = g u
/def -b'^[O3k' = g d
; Group status
/def -b'^[O3p' = gst
/def -b'^[O3q' = g sw
/def -b'^[O3r' = g s
/def -b'^[O3s' = g se
/def -b'^[O3t' = g w
/def -b'^[O3u' = l
/def -b'^[O3v' = g e
/def -b'^[O3w' = g nw
/def -b'^[O3x' = g n
/def -b'^[O3y' = g ne
; Group shields
/def -b'^[O3n' = gsh
; Prays
/def -b'^[O3M' = pp

You may want to bind some more keys or combinations, for example I also use:

; Stop!
/def -b"^[s" bind_stop = stop
; Display the queue
/def -b"^[q" bind_queue = queue
; Queue a frimble saying "Queue processed."; add a highlight or alarm and queue this to notify you when queues finish.
/def -b"^[d" bind_queuedone = frimble Queue processed.

You can come up with more.

Let's add some more connecting/disconnecting hooks.

; Disable LP mode once we connect so that we don't wait for newlines and don't put incomplete lines in the input area, assuming they're prompts
; Also enable GMCP; see below
/def -msimple -hCONNECT -F connected = \
    /set lp=0 %;\
    gmcp on

; Set idle mode when we disconnect
/def -msimple -hDISCONNECT -F disconnected = \
    /test heartbeat:=0

I like to use vim as an editor, so when MUD throws me into the editor, I just invoke vim and write my text in that.

; @ sends commands verbatim, bypassing the SEND hooks (see below)
/alias vim \
    @i %;\
    /sh -q rm -rf /tmp/tf.vim.txt; vim /tmp/tf.vim.txt %;\
    /quote -S -dexec @ '/tmp/tf.vim.txt %;\
    @.

These hooks trigger on any input, careful with the generic one. You probably don't want this but I like it to save some processing time on the MUD side.

; Prefix everything we send with noalias
; Lowest priority so that aliases are processed first (priority 1), along with other hooks
/def -p0 -msimple -hSEND sendhook = \
  @noalias %{*}

; If the input begins with @, _, >, ` or #, not followed by a space, call the alias of such name and pass the rest as arguments
; If there is a space, the standard alias processing catches it
; This emulates various MUD-side shorthands like ' or :
/def -pmaxpri -h"SEND ^([@_>`#])[^ ]" shorthand = \
    %{P1} $[substr({(*}, 1)]

; If it begins with a number, call the _ macro, for repetitions
/def -pmaxpri -h"SEND ^(\\d+)(.+)$" repshorthand = \
    _ %{P1} %{P2}

And the macros these call...

; Send stuff verbatim, bypassing the generic SEND hook
/alias @ \
    /send %{*}

; Repeat the command; just sends the number if no command is given (useful in the Shades)
/alias _ \
    /if ({#} > 1) \
        /for i 1 %{1} %{-1} %;\
    /else \
        %{1} %;\
    /endif

; A shorthand for sayto, like ">womble You're great!"
/alias > \
    sayto %{*}

; A shorthand for whispering, like "`womble You're great!"
/alias ` \
    whisper %{*}

; A shorthand for querying bug reports; Playtesters might use something different here
/alias # \
    bugs %{*}

You may want to use audio bell, so let's add a macro for it. You can call it from some triggers. You could control that with a variable, for instance /set audiobell=0 in your vars.tf, then toggle it as you go.

; Plays the argument file, or beep.ogg if none supplied
/def audiobell = \
    /sys mpv ~/tf/%{*-beep.ogg} >/dev/null 2>&1

Let's handle GMCP toggling and processing.

; Handles GMCP messages, called from the GMCP hook
; If gmcp_echo is set, it prints out some human-readable information
; If gmcp_debug is set, it prints out everything it receives, as is
; Also resets heartbeat; if we're receiving GMCP, we're active
; Calls /xp to store xps for rates and for xp difference notifications
; Finally calls /status to update the status bar
/def gmcp = \
    /test heartbeat:=default_heartbeat %;\
    /if (gmcp_debug) \
        /echo -aCgreen GMCP debug: %{*} %;\
    /endif %;\
    /if ({1} =~ "Char.Vitals") \
        /test regmatch('(?-i)^{"alignment":"([^"]+)","maxhp":(\\\\d+),"hp":(\\\\d+),"xp":(\\\\d+),"maxgp":(\\\\d+),"burden":(\\\\d+),"gp":(\\\\d+)}$$', {-1}) %;\
        /xp %{xp} %{P4} %;\
        /test alignment:={P1} %;\
        /test maxhp:={P2} %;\
        /test hp:={P3} %;\
        /test xp:={P4} %;\
        /test maxgp:={P5} %;\
        /test burden:={P6} %;\
        /test gp:={P7} %;\
        /if (gmcpecho) \
            /_echo GMCP vitals: %{hp} / %{maxhp} hp  %{gp} / %{maxgp} gp  %{xp} xp  %{burden}% burden  %{alignment} %;\
        /endif %;\
    /elseif ({1} =~ "room.info") \
        /test identifier:=(regmatch('(?-i)"identifier":"([^"]+)"', {-1}) ? {P1} : "unknown") %;\
        /test name:=(regmatch('(?-i)"name":"([^"]+)"', {-1}) ? {P1} : "unknown") %;\
        /test visibility:=(regmatch('(?-i)"visibility":(\d+?)', {-1}) ? {P1} : "unknown") %;\
        /test kind:=(regmatch('(?-i)"kind":"([^"]+)"', {-1}) ? {P1} : "unknown") %;\
        /if (gmcp_echo) \
            /echo -aCgreen GMCP room information: "%{name}"  %{identifier}  %{kind} %;\
        /endif %;\
    /else \
        /_echo Unrecognized GMCP data packet! %;\
        /return %;\
    /endif %;\
    /status

; Toggles GMCP, telling the MUD we support just vitals and room information.
; Defines a GMCP hook pointing to the previous macro
/alias gmcp \
    /if (!{#}) \
        /_echo $[gmcp ? "GMCP is enabled." : "GMCP is disabled."] %;\
        /return %;\
    /endif %;\
    /test gmcp:=(regmatch("(?i)^(?>0|1|on|off)$$", {*}) ? {*} : 1) %;\
    /test gmcp('core.hello { "client" : "tinyfugue", "version" : "5.0" }') %;\
    /if (gmcp) \
        /def -msimple -hGMCP gmcptrig = /gmcp %%{*} %;\
        /test gmcp('core.supports.set [ "Char.Vitals", "room.info" ]') %;\
    /else \
        /undef gmcptrig %;\
        /test gmcp("core.supports.set [ ]") %;\
    /endif

Besides GMCP, we want to gather updates from score brief and the monitor. If you modifier your monitor format as suggested earlier, you will be getting alignment updates. If not, you will only get alignment updates via GMCP. Without it we also can't distinguish monitor from score brief, so combat detection won't work well.

; Catches monitor and score brief to update vitals passively
; If it sees alignment, it assumes monitor and sets the combat mode
; Also calls /xp for xp difference notifications
; Calls /status at the end to redraw the status bar
/def -t"(?-i)^Hp: (\\d+)\\((\\d+)\\)  Gp: (\\d+)\\((\\d+)\\)  Xp: (\\d+)  Burden: (\\d+)%(?>  Alignment: (.+))?" -ag vitals = \
    /xp %{xp} %{P5} %;\
    /test hp:=strip_attr({P1}) %;\
    /test maxhp:=strip_attr({P2}) %;\
    /test gp:=strip_attr({P3}) %;\
    /test maxgp:=strip_attr({P4}) %;\
    /test xp:=strip_attr({P5}) %;\
    /test burden:=strip_attr({P6}) %;\
    /if ({P7} !~ "") \
        /test monitor:=default_monitor %;\
        /test alignment:=strip_attr({P7}) %;\
    /endif %;\
    /status

This records our stored xps and when it occured. It also notifies you if the gained xp is above xpthreshold.

/def xp = \
    /set xp_$[trunc(time())]=%{2} %;\
    /let diff=$[{2} - {1}] %;\
    /if (diff >= xpthreshold) \
        /_echo You have gained $[trunc(diff)] experience points. %;\
    /endif

Redraws the status bar in green, yellow or red, or no colour, based on how hurt you are. Also forces redrawing even if nothing has changed by assigning to status_attr.

/def status = \
    /let extra_attr %;\
    /if (hp >= maxhp) \
        /test status_attr:=default_status_attr %;\
        /return %;\
    /elseif (hp >= maxhp * 0.8) \
        /test extra_attr:=",Cgreen" %;\
    /elseif (hp >= maxhp * 0.5) \
        /test extra_attr:=",Cyellow" %;\
    /else \
        /test extra_attr:=",Cred" %;\
    /endif %;\
    /test status_attr:=strcat(default_status_attr, extra_attr)

This emulates the MUD heartbeats. Designed to run every two seconds, it increments HPs, GPs, experience (based on rate), checks for memos (more on that below), recalculates the rate, sets the combat status. This is a basic version that can be extended with alarms when GPs are full or with various other effects such as the dire seaweed extract. Besides the idle, combat monitor and rate counters it can also time your shields or other things. Extend it as you like.

At the end it schedules itself to run again in two seconds.

/def heartbeat = \
    /if (heartbeat) \
        /test --heartbeat %;\
        /test hp:=(hp+=hpinc, hp > maxhp ? maxhp : hp) %;\
        /test gp:=(gp+=gpinc, gp > maxgp ? maxgp : gp) %;\
        /test xp+=(rate > 800000 ? 1 : (rate > 50000 ? 2 : 3)) %;\
    /endif %;\
    /if (monitor) \
        /test --monitor %;\
    /endif %;\
    /if (xp & !ratecounter) \
        /test ratecounter:=default_ratecounter %;\
        /rate %;\
    /else \
        /test --ratecounter %;\
    /endif %;\
    memo quiet %;\
    /status %;\
    /repeat -2 1 /heartbeat

Recalculates the rate. Don't call this too often. I find calling it every five heartbeats is perfectly sufficient. Xp is stored in variables named xp_time.

It calculates the rate from the data available, or an hour at most. Data older than an hour is removed. It only considers positive increments, so spending xps doesn't mess it up.

/def rate = \
    /let ts=$[trunc(time())] %;\
    /let vars=$(/listvar -s xp_) %;\
    /let first=0 %;\
    /let last=0 %;\
    /let sum=0 %;\
    /let i=1 %;\
    /while (1) \
        /let var=$(/nth %{i} %{vars}) %;\
        /if (var =~ "") \
            /break %;\
        /endif %;\
        /if (substr(var, 3) < ts - 3600) \
            /unset %{var} %;\
        /else \
            /if (!first) \
                /test first:=substr(var, 3) %;\
            /else \
                /test sum+=(%{var} - last > 0 ? %{var} - last : 0) %;\
            /endif %;\
            /test last:=%{var} %;\
        /endif %;\
        /test ++i %;\
    /done %;\
    /test rate:=(sum * trunc(3600 / (ts - first == 0 ? 1 : ts - first)))

; If needed, you can call resetrate to clear all xp data
/alias resetrate \
    /let vars=$(/listvar -s xp_) %;\
    /let i=1 %;\
    /while (1) \
        /let var=$(/nth %{i} %{vars}) %;\
        /if (var =~ "") \
            /break %;\
        /endif %;\
        /unset %{var} %;\
        /test ++i %;\
    /done %;\
    /test rate:=0

You may also want client-side memos. Yes, you can use the in-game memos but client-side memos can be set automatically by triggers without sending anything to the MUD and breaking the rules. For instance when you start or finish a mission, a trigger could detect that and set a memo for you.

Memos are checked by the heartbeat macro. If there are any pending memos, they're displayed. Calling memo quiet does exactly this. Calling memo displays all pending macros (you don't want to do that every heartbeat). Adding new memos can be done with memo 7200 This will be displayed in two hours!. Memos are being removed once they're processed.

Memos are stored in variables named memo_time.

/alias memo \
    /let i=1 %;\
    /let when %;\
    /let what %;\
    /let anything=0 %;\
    /if ({#} >= 2) \
        /test when:={1} %;\
        /test what:={-1} %;\
        /if (regmatch("(?-i)^\\d+$", when)) \
            /set memo_$[trunc(time()) + when]=%{what} %;\
        /else \
            /_echo Usage: memo 

If you followed the initial recommendation for MUD-side settings, you can use the following to rewrite the output into something prettier.

; shortinline, notifications and timestamps all look the same, so rewrite them so that they look different... and better
; Room names when you look or glance will be printed without [] and in bold, also with the first letter capitalized
; Informs will be printed with the "Information:" prefix and [] stripped
; Timestamps will be printed with the "Timestamp:" prefix and [] stripped
; The timestamp detection should use your timezone name
/def -t"(?-i)^\\[([^\\[\\]]*)\\]$" shortinlong = \
    /if ( \
            {P1} =/ "* is now roleplaying" | \
            {P1} =/ "* has updated * roleplaying details" | \
            {P1} =/ "* has gained the * achievement *" | \
            {P1} =/ "* places a grouping request." | \
            {P1} =/ "* alters * grouping request." | \
            {P1} =/ "* withdraws * grouping request." | \
            {P1} =/ "* enters Discworld *" | \
            {P1} =/ "* leaves Discworld *" | \
            {P1} =/ "* has a birthday today" | \
            {P1} =/ "* arrives on Discworld for the first time!" | \
            {P1} =/ "* is now worshipping *" | \
            {P1} =/ "* is now a member of *" | \
            {P1} =/ "* joined the Unseen University in Ankh-Morpork" | \
            {P1} =/ "* joined the Wizards' Guild at the Sto Lat Academy" | \
            {P1} =/ "* has specialised as *" | \
            {P1} =/ "Recent Developments blog: *" | \
            {P1} =/ "Event * is due to start *" \
        ) \
        /substitute -aBCmagenta Information: %{P1} %;\
    /elseif ({P1} =/ "* in London*") \
        /substitute -aBCblue Timestamp: %{P1} %;\
    /else \
        /substitute -aB $[strcat(toupper(substr({P1}, 0, 1)), substr({P1}, 1))] %;\
    /endif

; Exits are also ugly and look similar to the above but begin with a space, even
; Let's rewrite them into something like "@ n, e, d"
/def -t"(?-i)^ \\[([a-z0-9-, ]+)\\]$" exits = \
    /substitute @ $[replace(",", ", ", {P1})]

; I run "who" and "qwho" often and don't like the machine-like line, so let's change it
/def -t"(?-i)^Unable to find any members of " qwhofail = \
    /substitute No one like that is currently online.

; Ever tried to run your kill alias in a room with nothing that matched?  Let's rewrite that
; The mobs variable is passed to kill aliases by default, we'll define it later
; It could happen that the line is too long if you have too many things in mobs, in which case this will not work
/def -t"(?-i)^Cannot find \"([^\"]+?)\", no match\\.$" notfound = \
    /if ({P1} =~ tolower(mobs)) \
        /substitute There is nothing you could attack. %;\
    /endif

I use "map door text" quite extensively to know what's around me. I also queue it after queued movements so that it notifies me when the queue finished. This could be done with GMCP updates if you're creative but I prefer to map manually.

The following trigger attempts to recognize "map door text" lines. These can often span across multiple lines as they tend to be longer than the maximum number of cols the MUD allows, so this needs to do multiline scanning. If the line looks like a map line and doesn't end with a fullstop, it eats all the following lines until it sees a line that does. It saves it all in a variable named map and when it's happy, it calls the /map macro that rewrites the map into something more readable.

This attempts to be fast, not pretty. You have been warned. It relies on the maximum visibility being six rooms. It realies on all exits always being the standard exits. it works around the Medina exits bug. It is designed for "map door text", not "map text", even though it can probably work with it.

/def -t"(?-i)^[^:]*\\b(?>[Th]he limit of your vision is |(?>is|are|of) (?>one|two|three|four|five) (?>north(?>west|east)?|south(?>west|east)?|west|east))\\b" -ag mdt = \
    /test map:={*} %;\
    /if (substr(map, -1, 1) !~ '.') \
        /def -pmaxpri -t -ag mdtappend = \
            /test map:=strcat(map, " ", {*}) %%;\
            /if (substr(map, -1, 1) =~ '.') \
                /undef mdtappend %%;\
                /map %%;\
            /endif %;\
    /else \
        /map %;\
    /endif

/def map = \
    /let nothing=Nothing, nowhere. %;\
    /if (map =~ "The limit of your vision is here.") \
        /_echo %{nothing} %;\
    /else \
        /let smap $[substr(map, 0, -1)] %;\
        /if (regmatch("(?-i)^(?>An|A|The|Two|Three|Four|Five|Six|Seven|Eight|Nine|Ten|Eleven|Twelve) ", smap)) \
             /test smap:=strcat(tolower(substr(smap, 0, 1)), substr(smap, 1)) %;\
        /endif %;\
        /test smap:=replace(" from here", "", smap) %;\
        /test smap:=replace(" is ", "!", smap) %;\
        /test smap:=replace(" are ", "!", smap) %;\
        /test smap:=replace(" and ", "!", smap) %;\
        /test smap:=replace(", ", "!", smap) %;\
        /test smap:=replace("one northwest", "1nw", smap) %;\
        /test smap:=replace("one northeast", "1ne", smap) %;\
        /test smap:=replace("one southwest", "1sw", smap) %;\
        /test smap:=replace("one southeast", "1se", smap) %;\
        /test smap:=replace("one north", "1n", smap) %;\
        /test smap:=replace("one south", "1s", smap) %;\
        /test smap:=replace("one west", "1w", smap) %;\
        /test smap:=replace("one east", "1e", smap) %;\
        /test smap:=replace("two northwest", "2nw", smap) %;\
        /test smap:=replace("two northeast", "2ne", smap) %;\
        /test smap:=replace("two southwest", "2sw", smap) %;\
        /test smap:=replace("two southeast", "2se", smap) %;\
        /test smap:=replace("two north", "2n", smap) %;\
        /test smap:=replace("two south", "2s", smap) %;\
        /test smap:=replace("two west", "2w", smap) %;\
        /test smap:=replace("two east", "2e", smap) %;\
        /test smap:=replace("three northwest", "3nw", smap) %;\
        /test smap:=replace("three northeast", "3ne", smap) %;\
        /test smap:=replace("three southwest", "3sw", smap) %;\
        /test smap:=replace("three southeast", "3se", smap) %;\
        /test smap:=replace("three north", "3n", smap) %;\
        /test smap:=replace("three south", "3s", smap) %;\
        /test smap:=replace("three west", "3w", smap) %;\
        /test smap:=replace("three east", "3e", smap) %;\
        /test smap:=replace("four northwest", "4nw", smap) %;\
        /test smap:=replace("four northeast", "4ne", smap) %;\
        /test smap:=replace("four southwest", "4sw", smap) %;\
        /test smap:=replace("four southeast", "4se", smap) %;\
        /test smap:=replace("four north", "4n", smap) %;\
        /test smap:=replace("four south", "4s", smap) %;\
        /test smap:=replace("four west", "4w", smap) %;\
        /test smap:=replace("four east", "4e", smap) %;\
        /test smap:=replace("five northwest", "5nw", smap) %;\
        /test smap:=replace("five northeast", "5ne", smap) %;\
        /test smap:=replace("five southwest", "5sw", smap) %;\
        /test smap:=replace("five southeast", "5se", smap) %;\
        /test smap:=replace("five north", "5n", smap) %;\
        /test smap:=replace("five south", "5s", smap) %;\
        /test smap:=replace("five west", "5w", smap) %;\
        /test smap:=replace("five east", "5e", smap) %;\
        /test smap:=replace(" of here", "!here", smap) %;\
        /test smap:=replace(" of 1", "!1", smap) %;\
        /test smap:=replace(" of 2", "!2", smap) %;\
        /test smap:=replace(" of 3", "!3", smap) %;\
        /test smap:=replace(" of 4", "!4", smap) %;\
        /test smap:=replace(" of 5", "!5", smap) %;\
        /test smap:=strcat(smap, "!") %;\
        /let pos 0 %;\
        /let what %;\
        /let where %;\
        /let mode 0 %;\
        /let anything 0 %;\
        /while ((pos:=strchr(smap, "!")) != -1) \
            /let obj $[substr(smap, 0, pos)] %;\
            /test smap:=substr(smap, pos + 1) %;\
            /if (!mode) \
                /if (regmatch("(?-i)(?:^\\d(?>n(?>w|e)?|s(?>w|e)?|w|e)|here)$", obj)) \
                    /test where:=obj %;\
                    /test mode:=1 %;\
                /else \
                    /test what:=(what !~ "" ? strcat(what, ', ', obj) : obj) %;\
                /endif %;\
            /else \
                /if (regmatch("(?-i)(?:^\\d(?>n(?>w|e)?|s(?>w|e)?|w|e)|here)$", obj)) \
                    /test where:=strcat(where, ' ', obj) %;\
                /else \
                    /test what:=replace("the limit of your vision", "", what) %;\
                    /test what:=replace("a hard to see through exit southeast", "", what) %;\
                    /test what:=replace("a hard to see through exit northwest", "", what) %;\
                    /if (what !~ "") \
                        /_echo > $[strcat(pad(replace("1", "", where), -10), "  ", what)] %;\
                        /test anything:=1 %;\
                    /endif %;\
                    /test what:=obj %;\
                    /test mode:=0 %;\
                /endif %;\
            /endif %;\
        /done %;\
        /if (!anything) \
            /_echo %{nothing} %;\
        /endif %;\
    /endif

Now, let's cover the activity mentioned in the beginning.

Activity is displayed in the status bar, at least its initial letter is. All it does is performs an action associated with the given activity when you hit the keypad Enter key.

For example, when I hunt, I set the activity to combat, displaying the letter c in the status bar. When I hit Enter, /activity is called, it sees the activity is combat and perform my combat action, i.e. feeds my minions.

When I idlechase, it runs my idlechasing alias. When I'm praying to charge rods or the pool, it prays. When I'm trying to TM things actively, it runs a TM'ing alias. And so on. You define your activities, you set them and then you just mash one key whenever it's time.

/def activity = \
    /if (activity =~ "") \
        /_echo There's nothing for you to do.  You feel helpless. %;\
    /elseif (activity =~ "combat") \
        ff %;\
    /elseif (activity =~ "idlechase") \
        manymanythings %;\
    /else \
        /_echo You don't know what to do because you didn't define this activity yet. %;\
    /endif %;\

; Just reset activities when you're done with them
; Usually not needed
/def reset = \
    /test activity:=""
; An example alias setting an activity
/alias idlechase \
    /test activity:="idlechase"

Earlier I mentioned matching combat messages and recommending to colour them on the MUD side. Well, you can match them like this.

Note my combat handling is fairly minimal and doesn't do much. I only highlight messages that involve me defending myself and rewrite them to add a message indicating success. Most people want more. Implementing this more is an exercise for the reader.

You cannot match on attributes so this needs to match everything, the encode the attributes into a string and see if it includes the colour code.

/def -t -F combatmsg = \
    /if (strstr(encode_attr({*}), "@{Cgray13}")) \
        /return %;\
    /endif %;\
    /if (regmatch("(?-i)\\byour?\\b", {*})) \
        /test monitor:=default_monitor %;\
        /status %;\
        /if (regmatch("(?-i)\\b(?>shoves|trips|reaches for|feint)\\b", {*})) \
            /return %;\
        /endif %;\
        /if (regmatch("(?-i)\\bbut(?> despite the surprise)?(?>, although unable to defend,)? you (?>(?>just|deftly|easily) )?(?>block|dodge|parry|somehow avoid)\\b", {*})) \
            /substitute -aCbg237,Cgreen %{*}  (Hit successfully defended.) %;\
        /elseif (regmatch("(?-i)\\bbut your [A-Za-z-'\\(\\) ]+? absorbs? all of the blow\\.$", {*})) \
            /substitute -aCbg237,Cyellow %{*}  (Hit fully absorbed.) %;\
        /elseif (regmatch("(?-i)\\bbut your [A-Za-z-'\\(\\) ]+? absorbs? (?>some|most) of the blow\\.$", {*})) \
            /substitute -aCbg237,Cyellow %{*}  (Hit partially absorbed.) %;\
        /elseif (regmatch("(?-i)\\bbut [A-Za-z-'\\(\\) ]+ parries\\b", {*})) \
            /substitute -aCbg237,Cyellow %{*}  (Hit parried on your behalf.) %;\
        /else \
            /substitute -aCbg237,Cred %{*}  (Hit lands in full force!) %;\
        /endif %;\
    /endif

I also want to see all specials...

/def -t"(?-i)\\bpowerful attack\\b" -F -PC16 specials

Now, from time to time I want to highlight something. Temporarily and visibly. And add an alarm. This set of aliases creates temporary triggers. And removes them. The arguments are a regex. You can call these like bell toys? and unbell toys?.

/alias bell \
    /if (!{#}) \
        /_echo Usage: bell <regex> %;\
        /return %;\
    /endif %;\
    /def -t"(?-i)\\\\b%{*}\\\\b" -F -PbBCyellow bell_$[textencode({*})]

/alias unbell \
    /if (!{#}) \
        /_echo Usage: unbell <regex> %;\
        /return %;\
    /endif %;\
    /undef bell_$[textencode({*})]

In the keybindings section I listed a frimble with "Queue processed." Don't forget to match it.

/def -msimple -t"Queue processed." -ahrb queue

Do you want to be alarmed when someone mentions your name? Or maybe you have alts you want to be notified about?

/def -pmaxpri -t"(?i)\\b(?>Yourname|Youralt|Yourotheralt)\\b" -F -PbB self

I also like to be notified when I get a tell. Or a say directed at me, same thing. But I don't want to be notified when it's minions or Ryattenoki talking. You may want to call /audiobell here, if you like that sort of thing. Perhaps conditionally.

It applies the attribute via substitution so that I can blacklist some.

/def -t"(?-i)(^[A-Za-z-',\\(\\) ]+) (?>tells|asks?|exclaims? to|says? to|whispers? to)[A-Za-z-',\\(\\) ]*? you: *" direct = \
    /if ({P1} =~ "Ryattenoki" | regmatch("\\bspectres?\\b", {P1})) \
        /return %;\
    /endif %;\
    /substitute -ab %{*}

Waiting for a t-shop? Make sure it lets you know.

/def -t"(?-i)^(?>Greasy sparks crawl over the .+, and a door silently materialises|The door .+ flickers, and vanishes\\.  A few lazy worms of fire remain .+ in its place, but they quickly wink out)\\.$" -abB tshop

Are you a teacher? Don't like figuring out why are you stuck and nothing is happening because someone is unexpectedly learning from you?

/def -t"(?-i)^(?>[A-Za-z-',\\(\\) ]+ tried to learn from you, but you were too idle to teach (?>him|her|it)|You (?>offer|start) to teach [A-Za-z-',\\(\\) ]+ \\d+ levels? (?>in|of) [a-z-\\.]+ for \\d+ xp)\\.$" -abB teaching

Do you want to know when you're being scried?

/def -t"(?-i)^(?>You sense someone breaking through your divine defences!$|You feel a pricking in the back of your mind that tells you )" -abB scrying

Or when you or others are successfully being snatched from?

/def -t"(?-i)^(?>[A-Za-z-'\\(\\), ]+? grabs your [A-Za-z-'\\(\\), ]+\\.  You struggle briefly but (?>he|she|it) wrests it from your grip and makes for a hasty retreat|[A-Za-z-'\\(\\), ]+? and [A-Za-z-'\\(\\), ]+? struggle briefly over [A-Za-z-' ]+? before [A-Za-z-'\\(\\), ]+? wrests it free and makes for a hasty retreat)\\.$" -abB snatching

Or when someone invites you to a group or you become the leader?

/def -t"(?-i)^(?>\\^\\+\\] You are now the leader of the group\\.$|You have been invited by [A-Za-z-',\\(\\)]+? to join (?>his|her|its) group called )" -abB groups

If you use faith rods, looking at them can be quite spammy. This helps you see what rituals it has and at what stage at a glance.

/def -t"(?-i)^The (?>balls|base|chains|coral section|emblem|etchings|feathers|grip|handle|head|hook|ironwood section|orb|point|rings|shaft|spikes|tape|tassels|tip|webbing) (?>is|are) (?>imbued|impressed|imprinted) with the ritual [a-z ]+? and (?>is|are) (?>not dependent on any other part of [A-Za-z'\\(\\) ]+|linked to the (?>chains|feathers|handle|hook|ironwood section|rings|shaft|spikes|tape))?\\.(?>  (?>It|They) glows? with a (?>brightly pulsing|bright) light\\.)?$" rods = \
    /substitute -p $[replace("imbued", "@{B}imbued@{n}", replace("impressed", "@{B}impressed@{n}", replace("imprinted", "@{B}imprinted@{n}", {*})))]

I also like to highlight some ritual messages. This covers Dark Sight, all faith shields, Weft Warping and Find.

/def -t"(?-i)^(?>Your vision returns to normal|Your vision suddenly fades to a dull blue shade|The higher being no longer protects you|Your divine protection is weakening|Your divine protection expires|Your protective aura is weakening|The protective aura shielding you expires|Hat is slowly starting to get tired of seeing your blood|Hat is tired of your blood|The weave on [A-Za-z-'\\(\\) ]+ suddenly returns to normal|Your set of prayer beads (?>hangs perfectly still|((?>rapidly|hastily|hurriedly|swiftly|quickly|briskly|deliberately|steadily|ponderously|gradually|slowly|indolently|sluggishly|almost imperceptibly) )?swings (?>\\d+ degrees? (?>north|south|west|east) from )?due (?>(?>north-|south-|west-|east-)?(?>north(?>west|east)?|south(?>west|east)?|west|east)(?> and(?> up| down)?(?>, and)?(?> yanks you \\d+\\.\\d inches in that direction)?)?)))\\.$" -aB rituals

If you use Devout Inquisition or meditate on obsecration, the following adds a coloured line to the ouput informing you about the state at a glance, with numbers.

The 5500 is an estimate but it's close for all churches.

/def -t"(?-i)^(?:Hat's pool ([a-z- ]+)|You understand that Hat's pool of available belief power ([a-z-,\\(\\) ]+)\\.  So far this week, you have (?>been (?>filling|draining) the deity pool|had no effect on the deity pool))\\.$" deitypool = \
    /let pool=$[{P1} !~ "" ? {P1} : {P2}] %;\
    /let poolmax=5500 %;\
    /let f %;\
    /let poolattr %;\
    /if (pool =~ "is at its maximum capacity") \
        /test f:=1 %;\
    /elseif (pool =~ "is nearly totally full") \
        /test f:=7./8 %;\
    /elseif (pool =~ "is only slightly depleted") \
        /test f:=6./8 %;\
    /elseif (pool =~ "is in good supply") \
        /test f:=5./8 %;\
    /elseif (pool =/ "is roughly half-full*") \
        /test f:=4./8 %;\
    /elseif (pool =~ "is less than half-full") \
        /test f:=3./8 %;\
    /elseif (pool =~ "is running dangerously low") \
        /test f:=2./8 %;\
    /elseif (pool =~ "has almost run out") \
        /test f:=1./8 %;\
    /elseif (pool =~ "has been totally exhausted") \
        /test f:=0 %;\
    /else \
        /_echo It appears the deity pool macro was triggered with an unknown state: %{pool} %;\
        /return %;\
    /endif %;\
    /if (f >= 0.75) \
        /test poolattr:="Cgreen" %;\
    /elseif (f >= 0.375) \
        /test poolattr:="Cyellow" %;\
    /else \
        /test poolattr:="Cred" %;\
    /endif %;\
    /echo -p @{%{poolattr}}The amount of available belief power is currently around $[trunc(poolmax * f)] points (or $[trunc(f * 100)]%%).@{n}

Don't bleed out when performing Holy Sacrifice. Don't let others bleed out. Alarms!

/def -t"(?-i)^(?>Some more of your life force slips between your fingers|[A-Za-z-',\\(\\) ]+? continues? to bleed profusely)\\.$" -ahb holysacrifice

Do you play games? They can also be spammy.

/def -t"(?-i)^(?>Starting game\\.  It is your turn|It is now your turn|The game has ended ended)\\.$" -aB games

Are you a high entity of some deity and hate the obsecrate command? Replace with pool, assistance, reductions and tithes

This hardoces Hat. Change it to yours. Maybe use a variable.

; High Entity's obsecration helper
; Inspects the pool, sets assistance, reductions or tithes
/def obsecrate = \
    /let req %;\
    /if ({1} =~ "pool") \
        /if ({-1}) \
            obsecrate hat for information on deity points regarding %{-1} %;\
        /else \
            obsecrate hat for information on deity points %;\
        /endif %;\
        /return %;\
    /elseif ({1} =~ "assistance") \
        /test req:="ritual assistance" %;\
    /elseif ({1} =~ "reductions") \
        /test req:="reduced ritual costs" %;\
    /elseif ({1} =~ "tithes") \
        /test req:="ritual tithes" %;\
    /else \
        /_echo Usage: obsecrate {pool|assistance|reductions|tithes} [arg] %;\
        /return %;\
    /endif %;\
    /if ({-1}) \
        obsecrate hat to set %{req} to %{-1} %;\
    /else \
        obsecrate hat for more information on %{req} %;\
    /endif

; Obsecrates the deity to provide pool information
/alias pool \
    /obsecrate pool %{*}

; Obsecrates the deity to query or set ritual assistance
/alias assistance \
    /obsecrate assistance %{*}

; Obsecrates the deity to query or set ritual cost reductions
/alias reductions \
    /obsecrate reductions %{*}

; Obsecrates the deity to query or set ritual tithes
/alias tithes \
    /obsecrate tithes %{*}

Do you hunt with minions like I do? Feed them with this. Put a feed variable in your vars.tf, defining the default amount to feed per minion. Redefine on the fly.

Can specify the amount as an argument. I only feed spectres but you want to extend this.

/alias ff \
    feed %{*-%{feed}} gps to my spectres

I also want to know when spectres are about to leave me to do this. The following highlight helps. It expects at most eight spectres leaving at once (yes, it's an overkill). It can detect multiple spectres with varied adjectives and it works when you can't see.

/def -t"(?-i)^(?>(?>The|One of the|Two|Three|Four|Five|Six|Seven|Eight) (?>groaning|moaning|pained|shivering|sobbing|translucent|wailing|whining|weeping|wretched) spectres?((?>,| and) (?>the|one of the|two|three|four|five|six|seven|eight) (?>groaning|moaning|pained|shivering|sobbing|translucent|wailing|whining|weeping|wretched) spectres?)*|Someone|The someone|(?>One of the|Two|Three|Four|Five|Six|Seven|Eight) people) fades? nearly to nothingness as (?>it|they) prepares? to depart\\.$" -aC16 minions

If you sail, you may want to remap your directions so that the standard ones work, especially with the keypad. These remap at the beginning and at the end, successful or not. They also set a memo when you start sailing.

; Starts a sail
; Calls sailstart and sets up a memo two hours from now
/def -t"(?-i)^The loading of the ship complete, (?>Captain Smith|Chidder) wishes you a safe and profitable trip as you climb aboard the SS Unsinkable\\.$" embark = \
    sailstart %;\
    memo 7200 Time to sail again!
; Ends the sail
; Triggered by both success and failure
; Calls sailend
/def -t"(?-i)^(?>The ship pulls into port\\.  You've arrived|As the ship sinks slowly beneath the waves, you dive off the side and manage to grab onto a couple of floating planks that used to be part of the ship\\.  You drift for what seems like an eternity before finally catching sight of shore\\.  Battered and bruised, you manage to struggle onto land)\\.$" disembark = \
    sailend
; Remaps directions for the ship
/alias dirsail \
    /alias n @f %;\
    /alias s @a %;\
    /alias w @p %;\
    /alias e @s %;\
    /alias nw @pf %;\
    /alias ne @sf %;\
    /alias sw @pa %;\
    /alias se @sa
; Remaps directions back to normal
/alias dirnorm \
    /unalias n %;\
    /unalias s %;\
    /unalias w %;\
    /unalias e %;\
    /unalias nw %;\
    /unalias ne %;\
    /unalias sw %;\
    /unalias se
; Pre-sail stuff
/alias sailstart \
    dirsail
; Post-sail stuff
/alias sailend \
    dirnorm

Do you ever send "history" to talker channels because you keep getting that wrong? Aliases like this show you the history if you don't pass an argument, otherwise they just send the argument.

Consider a generic talker alias and then just pass the channel name and the arguments, though. I have yet to do that myself.

I don't do this for clubs as they have too many options but it could be done.

/alias igame \
    /if ({#}) \
        @igame %{*} %;\
    /else \
        talker history igame %;\
    /endif

I store lists of things in *.list files. This includes the list of things to kill, a list of mobs to highlight, and various completion lists of commands, remembers or player names. The /loadlist loads a list of things from a file, ignoring empty lines and lines starting with #, and joins the items with the provided separator. You can then make the resulting string a part of a regular expression, too.

/def loadlist = \
    /let fh %;\
    /let file %{1} %;\
    /let sep %{2- } %;\
    /let out %;\
    /let line %;\
    /test fh:=tfopen(file, "r") %;\
    /while (tfread(fh, line) != -1) \
        /if (line !~ "" & substr(line, 0, 1) !~ '#') \
            /if (!strlen(out)) \
                /test out:=line %;\
            /else \
                /test out:=strcat(out, sep, line) %;\
            /endif %;\
        /endif %;\
    /done %;\
    /test tfclose(fh) %;\
    /_echo %{out}

I typically call this at the end, where I have a /start and /reload macros. These reload the main configuration and populate certain lists, including completion. They check for the /heartbeat process and only start it if it's not already running.

Lists I use include:

  • completion.list, a generic list of words I want in my completion
  • players.list, a list of player names updated whenever I run qwho (see below)
  • alts.list, a CSV-like list of alts I know about, because my memory is bad; not used here
  • kill.list, an inclusive list of things I kill by default, generally just all
  • killexcept.list, a list of things I want to exclude from the kill list

And indirectly:

  • bookmarks.json, a structured file with my remembered locations, processed with the jq utility; bookmark names are added to completion
/def start = \
    /let proc=$(/ps) %;\
    /if (!regmatch("(?-i)\\b\\d+ +\\d+\\.\\d+ r +1\\.000 +1 /heartbeat\\b", proc)) \
        /heartbeat %;\
    /endif %;\
    /set completion_list \
        $(/loadlist ~/tf/completion.list) \
        $(/loadlist ~/tf/players.list) \
        $(/quote -S -decho !jq -r "keys[]|@text" ~/tf/bookmarks.json | rg -Fv " ") %;\
    /set mobs=$(/loadlist ~/tf/kill.list &) except $(/loadlist ~/tf/killexcept.list &) %;\
    /def -pmaxpri -t"(?-i)\\\\b(?>$(/loadlist ~/tf/mobs.list |))\\\\b" -F -PCbrightblue mobs %;\

/def reload = \
    /load ~/tf/main.tf

/start

To update the list of players, I have the following trigger defined:

/def -t"(?-i)^\\d+ (?>Creator|Playtester|Friend|Player)s?: (.+)$" parseplayers = \
    /let fh %;\
    /let pos %;\
    /let players=%{P1} %;\
    /test players:=replace('(T)', , players) %;\
    /test players:=replace('(I)', , players) %;\
    /test players:=replace('(D)', , players) %;\
    /test players:=replace('(S)', , players) %;\
    /test players:=replace('(C)', , players) %;\
    /test players:=replace('(l)', , players) %;\
    /test players:=replace('(e)', , players) %;\
    /test players:=replace('(p)', , players) %;\
    /test players:=replace('(F)', , players) %;\
    /test players:=replace('[', , players) %;\
    /test players:=replace(']', , players) %;\
    /test players:=strcat(tolower(players), ' ') %;\
    /test fh:=tfopen('~/tf/players.list', 'a') %;\
    /while ((pos:=strchr(players, ' ')) != -1) \
        /let player=$[substr(players, 0, pos)] %;\
        /if (!regmatch(strcat("\\b", player, "\\b"), completion_list)) \
            /test tfwrite(fh, player) %;\
            /test completion_list:=(completion_list !~ "" ? strcat(completion_list, ' ', player) : player) %;\
        /endif %;\
        /test players:=substr(players, strlen(player) + 1) %;\
    /done %;\
    /test tfclose(fh)

And here's an alias working with the alts.list. I edit that file manually when I learn about new alts.

Calling alts playername runs qwho on all the known alts of that person to see if they are online, and prints to list of alts to refresh my memory.

/alias alts \
    /if (!{#}) \
        /_echo Usage: alts <player> %;\
        /return %;\
    /endif %;\
    /let q=%{1} %;\
    /let alts=$(/loadlist ~/tf/alts.list !) %;\
    /test alts:=strcat(alts, "!") %;\
    /let pos %;\
    /while ((pos:=strchr(alts, '!')) != -1) \
        /let rec $[substr(alts, 0, pos)] %;\
        /if (regmatch(strcat("\\b", q, "\\b"), rec)) \
            /_echo Known alts of %{q} include $[replace(",", ", ", strcat(substr(rec, 0, strrchr(rec, ",")), " and ", substr(rec, strrchr(rec, ",") + 1)))]. %;\
            q %{rec} %;\
            /return %;\
        /else \
            /test alts:=substr(alts, strlen(rec) + 1) %;\
        /endif %;\
    /done %;\
    /_echo We don't know about any alts of %{q}. %;\
    q %{q}

And finally, bookmarks.json is yet another file I maintain manually. It looks like this:

{
    "drum": {
        "description", "Outside the Mended Drum, Ankh-Morpork",
        "id": "",
        "item": "kept brass amphora charm",
        "pre": [
            "get kept silver charm bracelet from kept black leather bag",
            "remove _ from kept silver charm bracelet"
        ],
        "post": [
            "add _ to kept silver charm bracelet",
            "put kept silver charm bracelet in kept black leather bag"
        ]
    }
}

Where everything is optional. The keys are loaded into completion, the rest is processed by a helper script that can handle Divine Hand to a bookmark or speedwalking to it, using Quow's database as a backend. It can also perform Far Sight.

Divine Hand and Far Sight require an item; pre and post lists are executed before and after and provide commands to fetch the item and put it away again. The underscore is replaced by the item itself. If those are not present, the item must be in the inventory.

Speedwalking requires an ID.

The bookmarks and database helper script could be called like this, for instance:

/def db = \

   /if ({#} < 2) \
       /_echo Usage: /db <command> <arguments> %;\
       /return %;\
   /endif %;\
   /if ({1} =~ "walk") \
       /let destination %;\
       /test destination:=regmatch("(?-i)^\\\\d+$$", {-1}) ? room_%{-1} : {-1} %;\
       /quote -S -dexec !~/tf/helper.py %{1} "%{identifier}" "$(/escape " %{destination})" %;\
   /elseif (regmatch("(?-i)^(?>item|npc|room)$", {1})) \
       /quote -S -dexec !~/tf/helper.py %{1} "$(/escape " %{-1})" %;\
   /elseif (regmatch("(?-i)^(?>go|peek)$", {1})) \
       /quote -S -dexec !~/tf/helper.py %{1} "$(/escape " %{-1})" %;\
   /else \
       /_echo Unknown command passed to /db. %;\
   /endif

Besides walking, going (with Divine Hand), peeking (with Far Sight) it can also query the database for items, NPCs or rooms. The output from the helper script also set room_number variables, so that you can then walk 5 to go to the fifth listed item from the last query.

Some of the aliases conflict with certain commands. If you want to use the command, use the @ shorthand, like @go crazy.

/alias walk \
    /db walk %{*}

/alias go \
    /db go %{*}
/alias peek \
    /db peek %{*}
/alias room \
    /db room %{*}

/alias npc \
    /db npc %{*}

/alias item \
    /db item %{*}

For the time being, I'll let you implement the helper script but if you go with these, you will need to parse JSON and construct and search the graph for the shortest path. The database is standard SQLite, so you will have to work with that, too.