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.
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:
OS | Config dir |
---|---|
Linux | $HOME/.config/mudpuppy/config.toml |
MacOS | /Users/$USERNAME/Library/Application Support/mudpuppy/config.toml |
Windows | C:\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:
Field | Optional | Type | Default | Examples |
---|---|---|---|---|
name | No | String | N/A | "DuneMUD" |
host | No | String | N/A | "dunemud.net", "10.10.10.10" |
port | No | int | N/A | 4000, 5999 |
tls | No | String | None | "Enabled","InsecureSkipVerify", "Disabled", |
echo_input | Yes | bool | true | |
no_line_wrap | Yes | bool | false | |
hold_prompt | Yes | bool | true | |
command_separator | Yes | String | ";" | "!", ";;" |
splitview_percentage | Yes | int | 70 | |
splitview_margin_horizontal | Yes | int | 6 | |
splitview_margin_vertical | Yes | int | 0 | |
no_tcp_keepalive | Yes | bool | false | |
debug_gmcp | Yes | bool | false |
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:
Field | Optional | Type | Default | Examples |
---|---|---|---|---|
mode | True | String | "mudsession" | "mudsession", "mudlist" |
keys | No | String | N/A | "ctrl-q", "shift-up", "f4" |
action | No | String | N/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
throughf12
(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 sessionTabNext
- Move to the next tabTabPrev
- Move to the previous tabTabClose
- Close the current tabTabSwapLeft
- Swap the current tab with the one to the leftTabSwapRight
- Swap the current tab with the one to the rightMudListNext
- Move to the next MUD on the MUD list tabMudListPrev
- Move to the previous MUD on the MUD list tabMudListConnect
- Connect to the currently selected MUD on the MUD list tabToggleLineWrap
- Toggle line wrapping config for the output bufferToggleEchoInput
- Toggle echo input config for the output bufferHistoryNext
- Move to the next input history entryHistoryPrev
- Move to the previous input history entryScrollUp
- Scroll up in the output bufferScrollDown
- Scroll down in the output bufferScrollTop
- Scroll to the top of the output bufferScrollBottom
- 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.
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:
say hello
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 frommudpuppy_core.config()
.session
- the current session ID.session_info
- the current SessionInfo.cformat
- thecformat.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:
OS | Logfile |
---|---|
Linux | $HOME/.local/share/mudpuppy/mudpuppy.log |
MacOS | /Users/$USERNAME/Library/Application Support/mudpuppy/mudpuppy.log |
Windows | C:\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:
- error
- warn
- info
- debug
- 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:
OS | Config dir |
---|---|
Linux | $HOME/.config/mudpuppy/ |
MacOS | /Users/$USERNAME/Library/Application Support/mudpuppy/ |
Windows | C:\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.
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:
- The command's name.
- The session ID.
- The command's main func.
- 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():
-
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.
-
OutputItem.failed_command_result() - similar to above, but for operations that failed and should be displayed as an error result.
-
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.
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)
- 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):
- Negotiating support for the telnet "EOR" option, and expecting prompts to be terminated with EOR.
- Seeing lines that end with telnet "GA", and assuming they are prompts.
- 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:
- Click on
File
->Options
->Settings
- Search for the
python.languageServer
option; select 'Pylance'. - 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:
- Click on
File
->Options
->Settings
- Search for the
python.analysis.diagnosticSeverityOverrides
option. We want to suppress'reportMissingModuleSource'
. - 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:
- Right-click the directory in the project source tree view.
- Select "Mark directory as"
- 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.
- Install
pyright
withpip install pyright
- 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!