Creating MOO Verbs
This guide is the reference for writing verb code: the file format, the names injected into the verb’s scope, the parser API, the output mechanisms, error handling, and the patterns most verbs share. For the beginner walk-through, see Your First MOO Verb. For patterns beyond the basics — calling other verbs, time-aware continuation, placement, SDK helpers — see Advanced Verb Patterns.
The shebang line
Every verb file starts with a #!moo verb shebang that supplies the
verb’s name(s), the object it lives on, and how the parser should match
it:
#!moo verb take --on $thing --dspec this --ispec from:any
Grammar:
#!moo verb verb_name1 [verb_name2] ...
--on object_name
[--dspec this|any|none|either]
[--ispec PREP:SPEC [PREP:SPEC ...]]
Verb names — space-separated. Multiple names act as aliases; inside the verb body,
verb_nameis the specific alias the player invoked.--on— required. Accepts a player name (Wizard), an object ID (#5), or a system property reference ($thing,$room,$container).--dspec— direct object specifier:this— verb fires only when the parsed direct object resolves to the object the verb is on. (drop widgetmatches widget’sdrop.)any— a direct object must be present; any string is accepted.either— direct object is optional.thisis set correctly when one is given.none(the default) — verb only matches commands with no direct object.
--ispec— indirect object specifiers, one per preposition.--ispec on:this --ispec in:thiswould letput X on Yandput X in Yreach the same verb. Usenonewhen the preposition itself must be present but takes no object (e.g.crawl --dspec none --ispec under:anymatchescrawl under desk).
Examples from the default verbs:
#!moo verb accept --on $room
#!moo verb take --on $thing --dspec this --ispec from:any
#!moo verb put give --on $thing --dspec this --ispec on:this --ispec in:this
#!moo verb @reload reload_batch --on $programmer --dspec any --ispec on:any
Common --ispec choices
Interaction |
|
Sample command |
|---|---|---|
Talking to someone |
|
|
Sitting / lying down |
|
|
Putting items inside |
|
|
Taking / drinking from |
|
|
Examining via |
|
|
Attacking / aiming |
|
|
Words the parser treats as prepositions
The lexer scans every command for preposition words before it splits
the command into parts. Words like from, to, with, in, on,
at, and into are always preposition boundaries — even when they
appear inside what you intended to be a plain argument.
If a player is going to type something whose argument contains one of those words, they need to quote it:
@eval from moo.sdk import lookup ← parsed as prep boundary; verb won't match
@eval "from moo.sdk import lookup" ← preserved as a single argument
@eval pre-imports moo.sdk, so the example above is also avoidable
by writing @eval "lookup('Wizard').location.name". The interactive
shell expands a leading ; to @eval:
; lookup('Wizard').location.name
The full preposition list lives in settings.PREPOSITIONS and is
documented in Command Parser Reference. When a verb takes free-form
text (a code snippet, a description, a message), document the quoting
expectation in the verb’s help.
RestrictedPython execution
Verb code is compiled and run inside Zope’s RestrictedPython sandbox. Practical implications:
Only the modules in
settings.ALLOWED_MODULESmay be imported:moo.sdk,hashlib,re,datetime,time. Wizards additionally getmoo.core.models.{object,verb,property}.Only the builtins in
settings.ALLOWED_BUILTINSare available:all,any,dict,enumerate,getattr,hasattr,list,max,min,set,sorted,sum, andPermissionError. Other builtins (type,dir,eval,exec,open, …) are absent by design.Attribute names beginning with
_raiseAttributeError. The single exception is the global_reference to the System Object.str.formatandstr.format_mapare blocked on string instances — use f-strings orstr.replace()instead.returnmay appear at any level of the verb body, not just at function end (RestrictedPython rewrites the source).
For the full sandbox model and the security rationale, see Verb Sandbox Security Reference.
Names injected into the verb’s scope
Every verb is compiled into a function with the signature
def verb(this, passthrough, _, *args, **kwargs). Inside the body, the
following names are available without import:
Name |
Type |
Description |
|---|---|---|
|
|
The object the verb was matched on. With |
|
callable |
Calls the same verb on the parent class. Pass any arguments through: |
|
|
The System Object ( |
|
|
Positional arguments when the verb is invoked as a method. Empty when invoked from the command parser. |
|
|
Keyword arguments when invoked as a method. Empty from the parser. |
|
|
The exact alias the caller used. Do not assign to a local variable named |
Linters will complain about undefined references for this,
passthrough, _, args, kwargs, and verb_name. Add
# pylint: disable=undefined-variable at the top of every verb file.
The context object
from moo.sdk import context brings the per-task context proxy into
scope. Most non-trivial verbs need it. The two attributes you’ll
reach for most:
context.player— the Object that originated the command. Stays anchored to the session initiator across nested verb calls. Use this for “who is acting” logic.context.parser— the parsed command. See “Parser methods” below.
context.caller shifts as verbs invoke other verbs (it tracks the
verb’s owner for permission checks); context.player does not. They
are the same object at the start of a command.
For the full attribute list (writer, task_id, task_time,
caller_stack), see The DjangoMOO Runtime.
Parser methods
When a verb is dispatched from a command, context.parser exposes
methods to read the parsed arguments. The headline calls are
get_dobj() / get_dobj_str() for the direct object and
get_pobj(prep) / get_pobj_str(prep) for indirect objects, with
has_* predicates for each.
Use get_*_str() when the argument is plain text (a message, a name
to create). Use get_*() when you expect the argument to refer to an
existing game object — and let the exception propagate if it doesn’t.
For the full method reference and exception behaviour, see Command Parser Reference.
Sending output to players
Three mechanisms exist:
Mechanism |
Recipient |
Notes |
|---|---|---|
|
The player who ran the command |
Buffered until the verb finishes. The standard way for command verbs to show results. |
|
Any player Object |
Goes through |
|
Any player Object |
Low-level connection write, bypasses all filtering. Wizard-owned verbs only. |
print() is what most command verbs use. obj.tell() is for sending
to players other than the initiator (or when player preferences should
be respected). write() is rare — only for system notifications that
must skip filtering.
return "string" does not display anything in a command verb. The
return value goes back to whatever invoked the verb — discarded for
top-level player commands. Always print() for player-visible output;
use a bare return to exit early:
if not context.parser.has_dobj_str():
print(f"Usage: {verb_name} <target>")
return
print("Done.")
For tests where there is no live SSH connection, tell() and write()
emit RuntimeWarning(f"ConnectionError({obj}): {msg}"). Capture with
pytest.warns(RuntimeWarning) (see Writing Tests for Your Verbs).
Reading and writing properties
The Django ORM is available, but the helper methods on Object are
shorter and walk the inheritance chain:
description = obj.get_property("description")
print(description)
obj.set_property("description", "A dark room.")
__getattr__ on Object lets you write obj.description directly,
but it tries verb dispatch first and only falls through to property
lookup on miss — so it’s two queries. Use get_property() when you
know it’s a property.
Don’t pair has_property with get_property. That’s two database
queries for the same data. Catch the absence with try/except:
from moo.sdk import NoSuchPropertyError
try:
description = obj.get_property("description")
except NoSuchPropertyError:
description = "You see nothing special."
Multi-line description text is automatically reflowed by the description
verb in root_class/description.py via _.string_utils.rewrap():
single newlines collapse to spaces, double newlines become paragraph
breaks, and each paragraph wraps to 80 columns. To get the same
behaviour for a custom help verb or note’s read verb, call rewrap
explicitly:
text = obj.get_property("body")
print(_.string_utils.rewrap(text))
obj.save() is only required after changing intrinsic fields like
name, unique_name, obvious, or owner. set_property saves the
property row directly.
Error handling
Every exception in moo.core.exceptions inherits from UserError.
When a UserError propagates out of a verb, the task runner
(moo.core.tasks.parse_command) catches it and shows the message to
the player as a bold red line. No try/except boilerplate is needed
just to report errors.
The common exceptions are all importable from moo.sdk:
- exception moo.core.exceptions.UserError(message, data=None)
Superclass for any error that should be displayed to the player who triggered it. The task runner catches every
UserErrorraised by a verb and renders it as a bold red line; verbs do not need totry/exceptaround calls that may raise these. Subclasses customize the default message rendered to the player.
- exception moo.core.exceptions.UsageError(message, data=None)
Raise when the player invoked a verb with bad syntax or missing arguments. The constructor takes the message string verbatim — that string is what the player sees.
raise UsageError(f"Usage: {verb_name} <target>")is the conventional pattern.
- exception moo.core.exceptions.NoSuchObjectError(name)
Raised when a name does not resolve to any object in scope — typically by
Parser.get_dobj()ormoo.sdk.lookup(). Default message:There is no '<name>' here.
- exception moo.core.exceptions.NoSuchVerbError(name)
Raised by the parser when no verb on any candidate object matches the typed command. Default message:
I don't know how to do that.
- exception moo.core.exceptions.NoSuchPropertyError(name, origin=None)
Raised by
Object.get_property()when the named property does not exist on the object or any of its ancestors. Default message:There is no '<name>' property defined.
- exception moo.core.exceptions.AmbiguousObjectError(name, matches, message=None)
Raised when a name resolves to more than one object. Default message:
When you say, "<name>", do you mean <obj1>, <obj2>, or <obj3>?— the matching objects are listed by name and#idso the player can disambiguate.
- exception moo.core.exceptions.AmbiguousVerbError(name, matches)
Raised when verb dispatch finds more than one matching verb at the same object. Default message:
More than one object defines "<name>": <obj1>, <obj2>, and <obj3>.
- exception moo.core.exceptions.NoSuchPrepositionError(prep)
Raised by parser methods like
Parser.get_pobj_str()when the requested preposition was not present in the player’s command. Default message:I don't understand you.
- exception moo.core.exceptions.QuotaError(message, data=None)
Raised when
@createis invoked by a player whose object quota is exhausted. Default message:You don't have enough quota to create that.
- exception moo.core.exceptions.AccessError(accessor, access_str, subject)
Subclass of Python’s
PermissionErrorraised by model-layer permission checks (Object.save(),Verb.__call__(), etc.) when the caller lacks the required permission. Default message:<accessor> is not allowed to '<action>' on <subject>. The task runner catchesPermissionErrorautomatically.
Letting get_dobj() raise NoSuchObjectError is the right pattern when
the argument must resolve to a real object — the player sees the
canned message automatically. Catch only when you want a different
message or an alternative path:
from moo.sdk import NoSuchObjectError
try:
target = context.parser.get_dobj()
except NoSuchObjectError:
print("You'll need to be more specific.")
return
UsageError is the conventional way to signal bad syntax:
from moo.sdk import UsageError
if not context.parser.has_dobj_str():
raise UsageError(f"Usage: {verb_name} <target>")
Any uncaught exception that isn’t a UserError shows
"An error occurred while executing the command." to regular players
and a full traceback to wizards.
Permission checks
For verbs that mutate state, check before touching the database:
if not this.can_caller("write"):
print("Permission denied.")
return
can_caller(perm) consults the ACL on this against the current
context.caller. Common permission names: read, write, execute,
move, transmute, derive, develop, entrust, grant. See
How Permissions Work in Verbs for the full set.
Validating arguments
Validate early; report once with a single print() and an early
return:
if not context.parser.has_dobj_str():
print(f"Usage: {verb_name} <target>")
return
Or raise UsageError and let the task runner format it:
from moo.sdk import UsageError
if not context.parser.has_dobj_str():
raise UsageError(f"Usage: {verb_name} <target>")
A real verb, end to end
default/verbs/thing/take.py puts most of the patterns above into one
file:
#!moo verb take --on $thing --dspec this --ispec from:any
# pylint: disable=return-outside-function,undefined-variable
from moo.sdk import context, NoSuchObjectError
# If "from <target>" was given, verify the object is actually placed on/near that target.
if context.parser.has_pobj_str("from"):
try:
from_target = context.parser.get_pobj("from")
placement = this.placement
if placement is None or placement[1] != from_target:
tname = context.parser.get_pobj_str("from")
print(f"{this.title()} isn't on the {tname}.")
return
except NoSuchObjectError:
tname = context.parser.get_pobj_str("from")
print(f"There is no '{tname}' here.")
return
title = this.title()
if this.location == context.player:
print(f"You already have {title} in your inventory.")
elif this.moveto(context.player):
this.clear_placement()
print(this.take_succeeded_msg(title))
if msg := this.otake_succeeded_msg(title):
this.location.announce(msg)
else:
print(this.take_failed_msg(title))
if msg := this.otake_failed_msg(title):
this.location.announce(msg)
What’s going on:
The shebang fires this verb when
take widgetmatches a$thingand optionally accepts afrom <target>clause.context.parser.has_pobj_str("from")andget_pobj("from")extract the optionalfromargument, treating “object not found” as a user-facing error.this.title()is a helper verb on$thingthat returns the formatted name.this.moveto(context.player)calls another verb on the object — see Advanced Verb Patterns for how that works.this.take_succeeded_msg(title)returns a pronoun-substituted string. Helper verbs like this do return values (because they’re invoked as methods, not as parser commands). The pattern is covered in Advanced Verb Patterns.this.location.announce(msg)broadcasts to everyone else in the room.
Where to go next
Advanced Verb Patterns — calling other verbs,
passthrough(), helper verbs that return values, time-aware continuation, placement, SDK helpers.Command Parser Reference — full parser reference, preposition synonyms, verb search order.
Verb Sandbox Security Reference — what RestrictedPython blocks and why.
The DjangoMOO Runtime — full
contextreference.SDK Functions — every callable exposed by
moo.sdk.