Introduction

Mudpuppy is a terminal MUD client with a customizable interface and Python scripting.

This work-in-progress user manual aims to help you understand and use Mudpuppy. While Mudpuppy is a prototype, you may ask questions in our Discord server provided you understand the project is deeply in-flux.

Documentation is sparse and there is NO stability guarantee. Everything is subject to change without notice.

Mudpuppy should work for a variety of MUD/MUSH/MUX/MUCK games, but it has primarily been tested with LP-style MUDs.

Here be dragons^H^H^H^H^H^H^Hrabid saber-toothed mudpuppys.

Command line

Mudpuppy offers a number of command line flags to customize its behavior.

Help

You can run mudpuppy --help to see the available options:

Usage: mudpuppy [OPTIONS]

Options:
  -f, --frame-rate <FLOAT>  Frame rate, i.e. number of frames per second [default: 60]
  -c, --connect <MUD_NAME>  MUD name to auto-connect to at startup. Can be specified multiple times
  -l, --log-level <LEVEL>   Log level filter. Default is INFO [default: INFO]
  -h, --help                Print help
  -V, --version             Print version

Connect

By default mudpuppy opens to a MUD list screen where you can select which MUD to connect to based on the ones listed in your [Config]. However, if you know which MUD(s) you want to connect to at startup, you can use the --connect option to specify them. This option can be used multiple times to specify multiple MUDs. Mudpuppy will open new tabs for each of the --conect arguments and immediately connect. The <MUD_NAME> argument must match the name field of a MUD in your MUD Config.

Log Level

Controls the verbosity of the log output. The --log-level option lets you specify the minimum log level to display. See Logging for more information on the available log levels.

Frame Rate

The --frame-rate option lets you customize the client frame rate.

Mudpuppy uses an immediate mode (IM) terminal user interface (TUI). This means that each frame, the portions of the interface that have changed are redrawn. The frame rate argument specifies how many frames per second Mudpuppy should aim for. The default is 60 frames per second, giving a nice smooth interface.

You may find (especially since Mudpuppy is an unoptimized prototype!) that drawing at this frame rate uses excessive CPU. First confirm you're running a --release build (debug builds are significantly slower). After that, try experimenting with lowering the frame rate. This will reduce the CPU usage, but may increase interface lag (e.g. when responding to your keystrokes).

Configuration

Where is my config file?

This depends on your OS, but will generally be where applications keep their configuration:

OSConfig dir
Linux$HOME/.config/mudpuppy/config.toml
MacOS/Users/$USERNAME/Library/Application Support/mudpuppy/config.toml
WindowsC:\Users\$USER\AppData\Roaming\mudpuppy\config.toml

You can also find this directory from within Mudpuppy by running:

/py mudpuppy_core.config_dir()

Or from a Python script with:

from mudpuppy_core import mudpuppy_core
path = mudpuppy_core.config_dir()

Customizing config/data directories

You can also set the MUDPUPPY_CONFIG and MUDPUPPY_DATA environment variables to customize the config and data dir that Mudpuppy will use. For example, on a UNIX-like operating system you could run:

MUDPUPPY_CONFIG=$HOME/mudpuppy-test/config MUDPUPPY_DATA=$HOME/mudpuppy-test/data mudpuppy

Example Config

mouse_enabled = false

[[muds]]
name = "DuneMUD (TLS)"
host = "dunemud.net"
port = 6788
tls = "Enabled"

[[binding]]
keys = "shift-up"
action = "scrolltop"

See Mouse support for more information on mouse_enabled.

See MUDs for more information on the MUD config fields.

See Keybindings for more information on the keybinding config fields.

MUD configuration

Inside your config file, you can define multiple MUD profiles. Each profile can have a number of settings that customize the connection to the MUD.

Example

For example, here is a config file that sets up profiles for three MUDs:

[[muds]]
name = "DuneMUD (TLS)"
host = "dunemud.net"
port = 6788
tls = "Enabled"

[[muds]]
name = "DunemUD (Telnet)"
host = "dunemud.net"
port = 6789
tls = "Disabled"

[[muds]]
name = "Custom"
host = "dunemud.net"
no_tcp_keepalive = true
hold_prompt = false
echo_input = false
no_line_wrap = true
debug_gmcp = true
splitview_percentage = 50
splitview_margin_horizontal = 0
splitview_margin_vertical = 0
command_separator = ";;"

Fields

Each MUD profile is defined in a [[muds]] TOML table in your config file. The following fields are available for each MUD profile:

FieldOptionalTypeDefaultExamples
nameNoStringN/A"DuneMUD"
hostNoStringN/A"dunemud.net", "10.10.10.10"
portNointN/A4000, 5999
tlsNoStringNone"Enabled","InsecureSkipVerify", "Disabled",
echo_inputYesbooltrue
no_line_wrapYesboolfalse
hold_promptYesbooltrue
command_separatorYesString";""!", ";;"
splitview_percentageYesint70
splitview_margin_horizontalYesint6
splitview_margin_verticalYesint0
no_tcp_keepaliveYesboolfalse
debug_gmcpYesboolfalse

Name

The name of the MUD profile. This is used to identify the MUD in the MUD list screen and in the --connect command line option. It is also the title for your session tab when connected.

You can write triggers and aliases that only apply for connections to a MUD with a specific name matching this config field.

Host

The hostname of the MUD server. This can be a domain name, or an IP address (both IPv4 and IPv6 are supported).

When connecting to a domain name Mudpuppy uses the "happy eyeballs" algorithm, which means it will try both IPv4 and IPv6 connections in parallel and use whichever succeeds first.

Port

The port number of the MUD server. This is the port that the MUD server is listening on for connections. Make sure the TLS setting matches the port number you use. Some MUD servers only speak TLS on a specific port and assume telnet will be used for other ports.

TLS

The transport layer security setting for the connection. It's recommended to use TLS to connect to a MUD when possible to avoid sending your username/password and all other data in plaintext on the network.

The available option values are:

  • "Enabled": TLS will be used for the connection and the MUD server's certificate will be verified. If the certificate is not valid, the connection will be refused. This is the recommended setting when using TLS.
  • "InsecureSkipVerify": TLS will be used for the connection, but the MUD server's certificate will not be verified. This should only be used for testing purposes since it is insecure.
  • "Disabled": The connection will be made over plain text (telnet) without using TLS. This is not recommended unless you have no other choice.

echo_input

When set to true (the default) Mudpuppy will display your sent input in the output buffer. This is useful for seeing what you've sent to the game. If the input you sent matched an alias, both the original input you sent, and the expanded alias will be shown.

When set to false Mudpuppy will not display your sent input in the output buffer. This can be useful if you prefer not to clutter your output buffer with your own input history.

no_line_wrap

When set to false (the default) Mudpuppy will wrap long lines of text in the output buffer so they aren't truncated. This is useful for reading long lines of text that don't fit in the window.

When set to true Mudpuppy will not wrap long lines of text in the output buffer. This can be helpful if you prefer to preserve the layout of the text as sent by the MUD. It may mean that some parts of the text are not visible without resizing your terminal window to be wide enough to accommodate the full text.

hold_prompt

When set to true (the default) Mudpuppy will automatically "hold" the last received prompt line at the bottom of the screen. This is helpful if you want to see your prompt at all times.

See prompt detection for more information on how Mudpuppy determines what is/isn't a prompt.

You may wish to set this to false if:

  • You prefer to have your prompt printed as a normal line in the output buffer.
  • Mudpuppy fails to detect the prompt correctly.

command_separator

The command separator is a string that Mudpuppy uses to split input into multiple commands. By default, this is ;. This means that if you type say hello;wave and hit enter, Mudpuppy will send say hello and wave as separate commands to the MUD.

See command splitting for more information.

splitview_percentage

The percentage of the screen that the scrollback history window should take up. This is a number between 0 and 100. The default is 70%.

splitview_margin_horizontal

The number of columns of space to use as margin on the left/right side of the scrollback history window. The default is 6. If you set this to 0 the scrollback history window will not have any margin and will cover the output buffer completely on the left/right.

splitview_margin_vertical

The number of rows of space to use as margin on the top/bottom of the scrollback history window. The default is 0. If you set this to 10 the scrollback history window will show 10 rows of the output buffer above/below the scrollback window.

no_tcp_keepalive

When set to false (the default) Mudpuppy will send TCP keepalive packets to the MUD server periodically. This is useful because Telnet has no built-in keepalive mechanism.

By adding no_tcp_keepalive = true to a MUD configuration Mudpuppy will not send keepalives. You may find this makes your connections drop after a period of inactivity.

debug_gmcp

When set to true Mudpuppy will print received GMCP messages to the output buffer as debug output. This can be useful for debugging GMCP issues with a MUD, but is also very verbose!

Key Bindings

Key bindings are a way to map keyboard keys to Mudpuppy shortcut actions. The bindings are configured based on which tab is currently focused: the Mud list, or a connected MUD session.

Example

# Quit a MUD session with 'ctrl-x'
[[keybinding]]
keys = "ctrl-x"
action = "quit"

# Move to the previous MUD on the MUD list tab with 'j'
[[keybinding]]
mode = "mudlist"
keys = "j"
action = "mudlistprev"

# Move to the next MUD on the MUD list tab with 'k'
[[keybinding]]
mode = "mudlist"
keys = "k"
action = "mudlistnext"

Fields

Each key binding is defined in a [[keybinding]] TOML table in your config file. The following fields are available for each key binding:

FieldOptionalTypeDefaultExamples
modeTrueString"mudsession""mudsession", "mudlist"
keysNoStringN/A"ctrl-q", "shift-up", "f4"
actionNoStringN/A"quit", "scrolltop", "toggle"

mode

The tab type that must be in-focus for the key binding to be active. The default is mudsession, meaning the key binding is only active when you're on a MUD's session tab.

If you want a key binding to be active on the MUD list tab, set mode = "mudlist" instead.

keys

The key or key combination that triggers the action. This can include modifiers by separating them with a -. For example, ctrl-x, shift-up, or f4.

modifiers

The available modifiers are:

  • ctrl
  • alt
  • shift

keys

The available keys are:

  • space (space bar)
  • enter (return key)
  • esc (escape key)
  • tab (tab key)
  • backspace (backspace key)
  • delete (delete key)
  • insert (insert key)
  • home (home key)
  • end (end key)
  • pageup (page up key)
  • pagedown (page down key)
  • up (up arrow key)
  • down (down arrow key)
  • left (left arrow key)
  • right (right arrow key)
  • f1 through f12 (function keys)
  • all other normal singular keys, e.g. 'a-z', '0-9', punctuation, etc.

action

The shortcut action that will be taken when the keys are input. For example, quit, scrolltop, or toggle.

Shortcuts

The available shortcuts (case insensitive) are:

  • Quit - Quit the current MUD session
  • TabNext - Move to the next tab
  • TabPrev - Move to the previous tab
  • TabClose - Close the current tab
  • TabSwapLeft - Swap the current tab with the one to the left
  • TabSwapRight - Swap the current tab with the one to the right
  • MudListNext - Move to the next MUD on the MUD list tab
  • MudListPrev - Move to the previous MUD on the MUD list tab
  • MudListConnect - Connect to the currently selected MUD on the MUD list tab
  • ToggleLineWrap - Toggle line wrapping config for the output buffer
  • ToggleEchoInput - Toggle echo input config for the output buffer
  • HistoryNext - Move to the next input history entry
  • HistoryPrev - Move to the previous input history entry
  • ScrollUp - Scroll up in the output buffer
  • ScrollDown - Scroll down in the output buffer
  • ScrollTop - Scroll to the top of the output buffer
  • ScrollBottom - Scroll to the bottom of the output buffer

Mouse support

You can optionally enable mouse support in Mudpuppy in your config.toml with the global setting mouse_enabled. For example:

# Enable mouse support
mouse_enabled = true

Make sure this configuration is outside of any MUD or Keybinding stanzas in your config TOML.

Important: mouse mode often interferes with selecting text to copy/paste. Support for mouse mode varies by terminal.

Mouse Scrolling

When mouse_enabled is true you can choose whether or not mouse scroll events are used to scroll the output history scrollback buffer using the global setting mouse_scroll. For example:

# Enable mouse support, and mouse scrolling of output history
mouse_enabled = true
mouse_scroll = true

Input

Command splitting

Often it's useful to be able to enter several commands all in one go. For this situation Mudpuppy supports input that's split into multiple commands based on a special command splitting delimiter.

By default this delimiter is ;; but it can be adjusted by setting the command_separator field of a MUD config in your MUD Config.

Example

For example, if you typed the following input and hit enter:

say hello;;wave;;/status -v

Then Mudpuppy would send these commands to the MUD:

  1. say hello
  2. wave

and then it would run the /status -v command.

If you've defined an alias for the pattern ^wave$ (for example) it would be evaluated just like if you typed wave without using the ;; splitter.

Remember if you've changed the command_separator you'll have to adjust the example above.

Commands

Mudpuppy has several built-in commands you can run from within the client. By default the command prefix is "/". The choice of prefix can be changed in your config file.

/status

Shows the current connection status. Use /status --verbose for more information like the IP address of the MUD and any relevant TLS details.

/connect

Connects the current session if it isn't already connected.

/disconnect

Disconnects the current session if it isn't already disconnected.

/quit

Exits Mudpuppy.

/reload

Reloads user python scripts. Scripts can define a handler to be called before reloading occurs if any clean-up needs to be done:

# Called before /reload completes and the module is re-imported.
def __reload__():
    pass

/alias, /trigger, /timer

These commands allow creating simple aliases/triggers/timers that last only for the duration of the session. To create durable versions pref Python scripting.

/bindings

View the configured key bindings. You can show only bindings for a specific input mode by providing --mode to the list sub command, e.g.:

/bindings list --mode=mudsession

See Key Bindings for more information.

/py

Allows running Python expressions or statements. If an expression returns an awaitable, it will be awaited automatically. You are free to import other modules as needed, and define your own functions/variables/etc.

By default several helpful items are provided in-scope:

  • mudpuppy - the mudpuppy module.
  • commands - the commands module.
  • config - the result from mudpuppy_core.config().
  • session - the current session ID.
  • session_info - the current SessionInfo.
  • cformat - the cformat.cformat() function.
  • history - the history module (documentation TBD).

Logging

Mudpuppy can log at a variety of different verbosity levels. This is a very helpful mechanism when you're troubleshooting scripts, or trying to learn how Mudpuppy works.

Log location

This depends on your OS, but will generally be where applications keep their non-configuration data:

OSLogfile
Linux$HOME/.local/share/mudpuppy/mudpuppy.log
MacOS/Users/$USERNAME/Library/Application Support/mudpuppy/mudpuppy.log
WindowsC:\Users\$USER\AppData\Roaming\mudpuppy\mudpuppy.log

You can also find this directory from within Mudpuppy by running:

/py mudpuppy_core.data_dir()

Or from a Python script with:

from mudpuppy_core import mudpuppy_core
path = mudpuppy_core.data_dir()

Customizing config/data directories

You can also set the MUDPUPPY_CONFIG and MUDPUPPY_DATA environment variables to customize the config and data dir that Mudpuppy will use. For example, on a UNIX-like operating system you could run:

MUDPUPPY_CONFIG=$HOME/mudpuppy-test/config MUDPUPPY_DATA=$HOME/mudpuppy-test/data mudpuppy

Log Level

By default Mudpuppy logs at the "info" level. You can change the log level by setting an environment variable, or using the --log-level command line argument:

# Via env var:
RUST_LOG=mudpuppy=trace mudpuppy

# Or, via the CLI:
mudpuppy --log-level=trace

The available log levels (in increasing level of verbosity/spam) are:

  1. error
  2. warn
  3. info
  4. debug
  5. trace

Python logging

Your Python code can use the normal logging library and the log information will be sent to the same place as Mudpuppy's own logs.

import logging
logging.warning("hello from Python code!")

Scripting

Python scripts placed in the Mudpuppy config directory are automatically loaded when Mudpuppy is started. This is the principle mechanism of scripting: putting Python code that interacts with Mudpuppy through the mudpuppy and mudpuppy_core packages in your config dir.

Where is my config directory?

This depends on your OS, but will generally be where applications keep their configuration:

OSConfig dir
Linux$HOME/.config/mudpuppy/
MacOS/Users/$USERNAME/Library/Application Support/mudpuppy/
WindowsC:\Users\$USER\AppData\Roaming\mudpuppy\

You can also find this directory from within Mudpuppy by running:

/py mudpuppy_core.config_dir()

Or from a Python script with:

from mudpuppy_core import mudpuppy_core
path = mudpuppy_core.config_dir()

Customizing config/data directories

You can also set the MUDPUPPY_CONFIG and MUDPUPPY_DATA environment variables to customize the config and data dir that Mudpuppy will use. For example, on a UNIX-like operating system you could run:

MUDPUPPY_CONFIG=$HOME/mudpuppy-test/config MUDPUPPY_DATA=$HOME/mudpuppy-test/data mudpuppy

Mudpuppy packages

Your Python scripts can import mudpuppy and import mudpuppy_core to get access to helpful interfaces for interacting with Mudpuppy and MUDs.

In general mudpuppy_core has low-level APIs, and helpful type definitions. On the other hand mudpuppy has higher-level APIs, such as decorators for making triggers/aliases/etc.

For full documentation on the available packages and APIs, reference the API documentation. This guide aims to provide helpful overviews while the API documentation aims for complete detail.

Async

Remember that Mudpuppy is asynchronous: most callbacks will need to be defined async, and you will need to await most operations on the mudpuppy_core interface.

# Correct:
@trigger(
    mud_name="Dune",
    pattern="^Soldier died.$",
)
async def bloodgod(session_id: int, _trigger_id: int, _line: str, _groups):
    await mudpuppy_core.send(session_id, "say blood for the blood god!")

It is an error to forget to define your functions with the async keyword, or to forget to await async APIs like mudpuppy_core.send():

# INCORRECT (no 'async' on def, no 'await' for send):
@trigger(
    mud_name="Dune",
    pattern="^Soldier died.$",
)
def bloodgod(session_id: int, _trigger_id: int, _line: str, _groups):
    mudpuppy_core.send(session_id, "say blood for the blood god!")

Mudpuppy will do its best to catch these errors for you, but it's helpful to keep in mind.

Aliases

Aliases match input you send to the MUD. When the input matches an alias' pattern, the alias callback will be executed.

  • Aliases can be used for something as simple as expanding "e" to "east", or for more complex actions like making an HTTP request.

  • Aliases can be added so that they're only available for MUDs with a certain name, or so that they're available for all MUDs you connect to.

  • The line that matched the alias, as well as any regexp groups in the pattern are provided to the alias callback function alongside the session ID of the MUD.

Search the API documentation for Alias to learn more.

Basic global alias

To make a basic alias that would expand the input "e" to "east" for all MUDs you can use the @alias decorator. For example, adding this to a mudpuppy Python script:

from mudpuppy import alias
from mudpuppy_core import mudpuppy_core

@alias(pattern="^e$", expansion="east")
async def quick_east(_session_id: int, _alias_id: int, _line: str, _groups):
    pass

Providing expansion is a short-cut for "expanding" the input that was matched by the pattern, by replacing it with the expansion value. The above example is equivalent to using send_line() directly:

@alias(pattern="^e$", name="Quick East")
async def quick_east(session_id: int, _alias_id: int, _line: str, _groups):
    await mudpuppy_core.send_line(session_id, "east")

If you want to customize the name of the alias, provide a name="Custom Name" argument to the @alias decorator. Otherwise, the name of the decorated function is used.

Per-MUD alias

Here's an example of an alias that's only defined when you connect to a MUD named "Dune".

It also demonstrates how to use a match group and the convenience of async aliases for doing things like "waiting a little bit" without blocking the client, or needing to use a separate timer.

It will match input like "kill soldier", pass the command through to the game, and then also wait 5 seconds before issuing the command "headbutt soldier".

import logging
import asyncio
from mudpuppy import alias
from mudpuppy_core import mudpuppy_core


@alias(mud_name="Dune", pattern="^kill (.*)$")
async def kill_headbutt(session_id: int, _alias_id: int, line: str, groups):
    # Send through the original line so that we actually start combat in-game
    # with the 'kill' command.
    await mudpuppy_core.send_line(session_id, line)

    # Wait for a little bit, and then give them a headbutt!
    target = groups[0]
    logging.info(f"building up momentum for a headbutt attack on {target}")

    await asyncio.sleep(5)
    await mudpuppy_core.send_line(session_id, f"headbutt {target}")

If you wanted to have this alias also available on MUDs named "DevDune" and "Dune (Alt)" the mud_name can be changed to a list:

@alias(mud_name=["Dune","DevDune","Dune (alt)"], pattern="^kill (.*)$")
async def kill_headbutt(session_id: int, _alias_id: int, line: str, groups):
    ...

Alias info

You can use the alias ID passed to the alias handler to access information about the alias, like how many times it has matched. See the get_alias() and AliasConfig API references for more information.

This can be used for things like disabling an alias after a certain number of usages:

from mudpuppy import alias
from mudpuppy_core import mudpuppy_core, OutputItem

@alias(pattern="^backstab$")
async def backstab(session_id: int, alias_id: int, line: str, _groups):
    alias_info = await mudpuppy_core.get_alias(session_id, alias_id)

    # Too many backstabs this session!
    hits = alias_info.config.hit_count
    if hits > 10:
        msg = f"backstabbed {hits} times already. Ignoring cmd."
        logging.info(msg)
        await mudpuppy_core.add_output(
            session_id, OutputItem.failed_command_result(msg)
        )
        return

    # Do the backstab
    await mudpuppy_core.send_line(session_id, line)

Triggers

Triggers match a line of output sent by the MUD. When the output matches an trigger's pattern, the trigger callback will be executed.

  • Triggers can be used for something as simple as sending "light torch" whenever the game sends the line "It is too dark to see." or for more complex actions like automatically making an HTTP request to look up the name of an item in a database when you see it on the ground.

  • Triggers can be added so that they're only available for MUDs with a certain name, or so that they're available for all MUDs you connect to.

  • The line that matched the trigger, as well as any regexp groups in the pattern are provided to the trigger callback function alongside the session ID of the MUD.

Note that all triggers are matched a line at a time. Multi-line triggers are not yet supported.

Search the API documentation for Trigger to learn more.

Basic global trigger

To make a basic trigger that would match the line "Your ship has landed." and automatically send "enter ship" you can use the mudpuppy module's @trigger decorator.

@trigger(
    pattern=r"^Your ship has landed\.$"
    expansion="enter ship",
)
async def quick_ship(_session_id: int, _trigger_id: int, _line: str, _groups):
    pass

If you want to customize the name of the trigger, provide a name="Custom Name" argument to the @trigger decorator. Otherwise, the name of the decorated function is used.

Providing expansion is a short-cut for "expanding" the input that was matched by the pattern, by replacing it with the expansion value. The above example is equivalent to awaiting send_line() directly:

@trigger(
    pattern=r"^Your ship has landed\.$"
)
async def quick_ship(session_id: int, _trigger_id: int, _line: str, _groups):
    await mudpuppy_core.send_line(session_id, "enter ship")

Per-MUD triggers

Like aliases you can define triggers for only certain MUDs by providing a mud_name string, or list of strings as an argument to the @trigger decorator:

@trigger(
    mud_name=["Dune", "DevDune"],
    pattern=r"^Your ship has landed\.$",
    expansion="enter ship",
)
async def quick_ship(_session_id: int, _trigger_id: int, _line: str, _groups):
    pass
)

Output gags

If you want to silence, supress or "gag" lines of output you can write a trigger that matches the lines you wish to gag, setting gag=True in the @trigger decorator:

@trigger(
    pattern=r"^(?:Autosave)|(?:Your character has been saved safely)\.$",
    gag=True
)
async def quiet_saves(_session_id: int, _trigger_id: int, _line: str, _groups):
    pass
)

Matching prompt lines

You can also create triggers that only match prompt lines by specifying prompt=True in the @trigger decorator. This can also be combined with gag=True to gag matched prompts.

See prompts for more information on how prompts are detected.

import logging

@trigger(
    prompt=True,
    gag=True,
    pattern=r"(?:Enter your username: )|(?:Password: )"
)
async def gag_login(_session_id: int, _trigger_id: int, line: str, _groups: Any):
    logging.debug(f"gagged login prompt: {line}")

Matching ANSI

By default triggers are created with strip_ansi=True. Lines of text will have any ANSI colour codes removed before evaluating the trigger pattern.

If you want to write a trigger that matches on ANSI you need to specify strip_ansi=False in the @trigger decorator:

import logging

@trigger(
    strip_ansi=False,
    pattern=r"\033\[[\d]+;1m(.*)\033\[0m",
)
async def quiet_saves(_session_id: int, trigger_id: int, _line: str, groups):
    logging.info(f"quiet_saves({trigger_id}) matched bold text: {groups[0]}")
)

Timers

Timers run on a fixed interval. When the timer interval runs out, the timer callback is invoked and then the timer is reset to wait for another interval.

  • Timers are great for running an action on a regular cadence, like sending a "save" command every 15 minutes.

  • Timers can be configured to only run a certain number of times. This can be helpful for something like running a "heal" command 3 times, with a 10 second wait between them.

Search the API documentation for Timer to learn more.

Basic global timer

To make a basic timer that runs every 2 minutes, 10 seconds you can use the mudpuppy module's @timer decorator.

Since global timers run without being tied to a specific MUD they are provided the currently focused active [SessionID] (if there is one!) as an argument:

@timer(seconds=10, minutes=2)
async def party(timer_id: int, session_id: Optional[int]):
    logging.debug(f"2m10s timer fired: {timer_id}!")
    if session_id is not None:
        await mudpuppy_core.send_line(session_id, "say PARTY TIME!!!")

If you want to customize the name of the timer, provide a name="Custom Name" argument to the @timer decorator. Otherwise, the name of the decorated function is used.

Max ticks

Here's an example of a timer that's only defined when you connect to a MUD named "Dune", and that only runs 3 times total (with a 10s wait between each run).

@timer(mud_name="Dune", seconds=10, max_ticks=3)
async def heal_timer(_timer_id: int, session_id: int):
    await mudpuppy_core.send_line(session_id, "heal")

Like aliases and triggers you can also pass a list of names to the @timer decorator's mud_name parameter, like mud_name=["Dune", "OtherMUD"].

Custom Commands

Mudpuppy comes with a number of built-in commands. You can also have your Python scripts add new custom commands. This is an attractive alternative to aliases when you want to support parse command-line arguments and flags.

The default command prefix is / but can be altered in configuration.

For more information, consult the API reference for the commands module.

Simple command

Commands are created by extending the Command class and registering the command for a specific session with commands.add_command().

Your command's __init__() should call the super().__init__ with:

  1. The command's name.
  2. The session ID.
  3. The command's main func.
  4. A description of the command.

Here's a simple command that when /simple is run, will log a message.

import logging
from mudpuppy_core import Event
from commands import Command, add_command

@on_new_session()
async def setup(event: Event):
    add_command(event.id, SimpleCmd(event.id))


class SimpleCmd(Command):
    def __init__(self, session: int):
        super().__init__("simple", session, self.simple, "A simple command example")

    async def simple(self, session: int, _args: Namespace):
        logging.debug("Hello world!")

Command-line args

To define a command that takes command-line args and has sub-commands look at existing examples of built-in commands like /trigger.

Output

Mudpuppy displays outputs per-MUD in a special output buffer. Your Python code can add items to be displayed through the mudpuppy_core API, or for simple debugging, using print().

Presently only the low-level API/types are available. In the future there will be helpers to make this less painful :-)

See the API reference for OutputItem as well as the add_output() and add_outputs() functions for more information.

You may also want to use cformat for colouring output you create.

Debug Output

For simple debug output you can use print(). It will convert each line of what would have been written to stdout into OutputItem.debug() instances that get added to the currently active session. If called when there is no active session, nothing will be displayed - prefer logging for this use-case.

You can also use print() from /py but you must carefully escape the input:

/py print(\"this is a test\\nhello!\")

Adding Output

Other kinds of output can be added using mudpuppy_core.add_output() and providing both the session ID to add the output to, and an OutputItem to add. Remember this is an async operation so you'll need to await!

from mudpuppy_core import mudpuppy_core, OutputItem

await mudpuppy_core.add_output(
    sesh_id, OutputItem.command_result("This was a test")
)

Output Item Types

There are several OutputItem types you can construct to use with add_output():

  1. OutputItem.command_result() - for constructing output that should be rendered as separate from game output. Generally this is used when the operation being described was successful.

  2. OutputItem.failed_command_result() - similar to above, but for operations that failed and should be displayed as an error result.

  3. OutputItem.mud() - for displaying output as if it came from the MUD. You'll need to construct a MudLine as the argument. E.g.:

from mudpuppy_core import MudLine, OutputItem

item = output_item.mud_line(MudLine(b"Some fake MUD output!"))

There is also OutputItem.prompt() and OutputItem.held_prompt() that take a MudLine but treat it as a prompt, or held prompt.

  1. OutputItem.input(line) - for displaying input as if it came from the user. You'll need to construct a InputLine for the argument. E.g.:
from mudpuppy_core import InputLine, OutputItem

line = InputLine("some fake input!")
line.original = "FAKE!"
item = output_item.input(line)
  1. OutputItem.debug() - for displaying debug information.

Prompts

Mudpuppy attempts to detect what is/isn't a prompt in a few ways (listed in order of how reliable they are):

  1. Negotiating support for the telnet "EOR" option, and expecting prompts to be terminated with EOR.
  2. Seeing lines that end with telnet "GA", and assuming they are prompts.
  3. Seeing lines that end without \r\n, after a short timeout expires to ensure it wasn't a partial line.

It is not presently possible to set the prompt handling mode manually, it is determined based on whether the MUD supports the telnet options mentioned above. Similarlyh it isn't presently possible to change the prompt flushing timeout for unterminated prompt mode. In the future this will be more flexible.

Prompt Event Handlers

To write a handler that fires for every prompt line for any MUD, use the mudpuppy module's @on_event decorator:

from mudpuppy_core import EventType, Event, MudLine
from mudpuppy import on_event

@on_event(EventType.Prompt)
async def prompt_handler(event: Event):
    session_id: int = event.id
    prompt_line: MudLine = event.prompt
    logging.debug(f"session {session_id} got prompt line {prompt_line}")

Similar to aliases, triggers, and timers it's also possible to write a handler that only fires for prompt events for specifically named MUDs.

@on_event(EventType.Prompt)
async def prompt_handler(event: Event):
    session_id: int = event.id
    prompt_line: MudLine = event.prompt
    logging.debug(f"session {session_id} got prompt line {prompt_line}")

Similar to aliases, triggers, and timers it's also possible to write a handler that only fires for prompt events for specifically named MUDs using @on_mud_event.

@on_mud_event(["Dune", "OtherMud"], EventType.Prompt)
async def prompt_handler(event: Event):
    ...

Prompt Triggers

MudLine's that are detected as a prompt have the prompt field set to True. See triggers for more information on how to write triggers that only match prompt lines.

This is genereally more useful if you want to only match certain prompt patterns, or to gag prompts.

IDE Setup for Script Editing

Once your scripts reach a certain complexity level it's helpful to have a Python integrated development environment (IDE) set up that understand the Mudpuppy APIs.

Since the Mudpuppy APIs are only exposed from inside of Mudpuppy this requires a little bit of special configuration to tell your IDE where to find "stub files" describing the API. How you do this depends on the specific IDE or tool. This page describes doing it with VSCode, PyCharm and Pyright.

In both cases you'll need the .pyi stub files from the Mudpuppy GitHub repo. You can find them under the python-stubs directory.

Setup Stubs

First, make sure you've taken note of where you cloned Mudpuppy, and the location of the python-stubs directory inside.

On Linux, MacOS or in WSL, you can symlink the Mudpuppy stubs into your project directory. That way they're always up to date with your clone of Mudpuppy:

ln -s /path/to/mudpuppy/python-stubs ./typings

If you're on Windows, you can copy the files instead, but remember to update them as Mudpuppy changes!

xcopy /E /I \path\to\mudpuppy\python-stubs .\typings

Visual Studio Code

Python Extension

You'll want to use the Python VSCode extension for editing Mudpuppy python scripts. It comes with the Pylance extension that will be used for type checking.

After installing the extension:

  1. Click on File -> Options -> Settings
  2. Search for the python.languageServer option; select 'Pylance'.
  3. Restart VS Code.

Type Checking

If you've copied the stub files to a directory named typings in the root of your project, no further configuration is needed. If you want to use a folder name other than typings you'll need to customize the python.analysis.stubPath option. See the VSCode settings reference and VSCode python docs for more information.

Missing Module Source Warnings

Since the Mudpuppy stubs are just that, stubs, they don't have associated source code. To stop VSCode from warning you about that we need to customize it further:

  1. Click on File -> Options -> Settings
  2. Search for the python.analysis.diagnosticSeverityOverrides option. We want to suppress 'reportMissingModuleSource'.
  3. Restart VS Code.

This is optional, but will clear up any warnings you might see about a missing source module.

Here's an example of this section of VS Code's json settings after making the change:

"python.analysis.diagnosticSeverityOverrides": {
  "reportMissingModuleSource": "none"
}

PyCharm

After copying the stub files into the root of your project you can:

  1. Right-click the directory in the project source tree view.
  2. Select "Mark directory as"
  3. Select "Source root"

That's it! You're all set.

For more information see the PyCharm stubs documentation.

Pyright

You can also configure a static type checking tool like Pyright to use the Mudpuppy stubs. This can be helpful for command-line type checking, or CI integrations.

  1. Install pyright with pip install pyright
  2. Create (or update) a pyproject.toml file at the root of your project with contents:
[tool.pyright]
stubPath = "typings" # or whatever directory name you used for the stubs
reportMissingModuleSource = false

See the Pyright user manual for more information.

Tutorial

This section of the user guide is an end-to-end tutorial showing how one might get started writing scripts for a new character on Dune MUD.

TODO: Write the content!