Where commands live, and how to add your own.
A verb is a command, stored as a short Python file attached to an object.
Everyday commands — look, go, get,
open — are attached to the room prototypes, not to
players. That is the key idea: a verb on a room is offered to everyone standing
in that room. Walk into a room and you can use its commands; walk out and you
cannot.
The two room prototypes are #16 for out-of-character rooms and
#17 for in-character ones. Put a verb on #17 and every
IC room in your world has it. Put it on one room, and only that room does.
The room is what decides what is possible inside it. A lobby should not
offer attack; an underwater room might offer
swim. Attaching commands to the place rather than the person
makes that natural instead of a special case.
Staff commands are the exception. All 75 of them live on #3, the
base character object, so every character inherits them — and they are kept in
check by an authority level rather than by where you are standing. They are
listed in full in the Command Reference.
| Level | They are | And can |
|---|---|---|
gm1 | Trusted player | Teleport, hold things, inspect objects. |
gm2 | Builder | Dig rooms, make exits, name and describe things, move and delete them. |
gm3 | Programmer | Make objects, write verbs, set properties, reserve numbers, run code. |
gm4 | Wizard | Remove player accounts. |
gm5 | Owner | Grant levels, reserve number blocks, restart and shut down. |
> @auth Ana = add gm2
> @auth Ana = remove gm2
Only gm5 may grant levels — the power to hand out authority is
itself the top of the ladder. Levels are cumulative: gm3 can do
everything gm1 and gm2 can.
Each staff verb begins by checking the level and, if the player is short, simply
answers Do what? — the same reply as a command that does not exist.
Staff commands do not advertise themselves to people who cannot use them.
Your own verbs can require a level too. Set it when you create the verb and the engine refuses the command before your code runs:
> @adverb #3.@mycmd with rx auth=3
> @verbauth #3.@mycmd = 3 # or afterwardsVerb code is ordinary Python with ordinary Python privileges: it can
import anything, open files, and reach the interpreter's
own builtins. That is the point of the language choice, and it is why
the whole standard library is genuinely available to you.
It also means that anyone you give gm3 can run
any code they like as the account your server runs under.
Grant it to people you would trust with the machine, not merely with
the game.
Classic MOO let ordinary players program, because the MOO language was itself the sandbox. MegaMOO has no equivalent boundary, so a world where untrusted players write code is not something the auth levels can give you today.
This is how one object behaves unlike every other object of its kind, and it is the most useful pattern in MegaMOO.
A shared verb does its normal job, but first it offers the object a chance to
handle the command itself — by calling a verb of the same name with a trailing
underscore. If that exception verb exists and returns
True, the shared verb stops. If the object has no such verb,
nothing happens and the normal behaviour continues.
Say you want a pull command. Normally it just reports that nothing
happens, with slightly different wording depending on what was pulled. Put this
on #17 and every IC room has it:
# the shared `pull` verb on #17
if not dobj:
pobj.msg("Pull what?")
return
target = pmatch(dobj, pobj, list(pobj.location.contents) + list(pobj.contents))
if not target:
pobj.msg("You don't see that here.")
return
# Does this particular object know how to be pulled?
try:
if call_verb(target, 'pull_'):
return
except KeyError:
pass
# Default behaviour, varying by what was pulled.
if getattr(target, 'is_exit', False):
pobj.msg("You tug at &d, but it does not budge.", dob=target)
else:
pobj.msg("You pull &d. Nothing happens.", dob=target)
pobj.location.msg_room("&S pulls &d.", exclude=[pobj], sub=pobj, dob=target)
Now the exception. A marble statue in the library gets its own
pull_, and pulling its arm swings a bookcase aside:
# the `pull_` exception verb on the statue, #204
bookcase = pmatch("bookcase", pobj, list(this.location.contents))
if not bookcase:
return False
if not getattr(bookcase, 'closed', 0):
pobj.msg("The statue's arm is already down.")
return True
pobj.msg("The statue's arm swings down with a grinding of stone.")
pobj.location.msg_room("&S pulls the statue's outstretched arm.",
exclude=[pobj], sub=pobj)
call_verb(bookcase, 'open_')
pobj.location.msg_room("The bookcase swings away from the wall, revealing a passage.")
return TrueAnd in play:
> pull statue
The statue's arm swings down with a grinding of stone.
You open a bookcase.
The bookcase swings away from the wall, revealing a passage.
> pull statue
The statue's arm is already down.
> pull bookcase
You tug at a bookcase, but it does not budge.
> go bookcase
Hidden Passage
Notice what did not happen. Nothing in pull knows the
statue exists. The bookcase has no special code either — it is an ordinary door.
The exception lives on the object that is exceptional, and everything else stays
simple.
| Inside an exception verb | Is |
|---|---|
pobj | The person who typed the command. |
this | The object the exception is attached to — the statue. |
return True | "I handled it" — the shared verb stops. |
| Return nothing | The shared verb carries on with its normal behaviour. |
Shared verbs pass extra information when the exception needs it — the shipped
give passes the recipient as call_verb(item, 'give_',
dobj=recipient), and lock passes the key. The exception
reads them as dobj and iobj, exactly as a normal verb
would.
The engine ships exceptions for most everyday actions:
go_, look_, get_, drop_,
eat_, drink_, open_, close_,
lock_, wear_, remove_, and the
in_/on_/under_/behind_
container variants.
Before your verb runs, the typed line is broken into pieces. That job belongs to the verb's type, and almost every verb uses the standard one, MasterVerb.
MasterVerb splits the line at the first preposition it finds:
put the blue sword in the oak chest
`--------------' `-------------'
dobj iobj
prep = "in"| You get | Holding |
|---|---|
dobj | The direct object — what the command acts on. |
prep | The preposition found: in, from, to, on… |
iobj | The indirect object — what follows the preposition. |
prep2 / dobj2 | A second preposition and everything after it. This is how @move ball to #13 with A ball flies in! keeps its message. |
lhs / rhs | Everything left and right of the preposition. |
args | The whole line after the command word. |
switches | Options typed as look/brief. |
These are text, not objects. Turning "blue sword"
into an actual object is pmatch's job, and it stays your verb's
decision — which is why a verb can choose to search the room, your hands, or
inside a container.
Single-character prepositions work without spaces, which is why
@desc #14=A room parses the same as
@desc #14 = A room.
That convenience has a consequence worth knowing before it surprises
you: because = needs no surrounding spaces, it is found
inside tokens too, and it wins over a later word preposition.
@adverb #3.mycmd with rx auth=3 does not parse as you would
read it aloud — the split happens at the =:
dobj = '#3.mycmd with rx auth'
prep = '='
iobj = '3'This is deliberate and it is not going to change: it is what makes
@name #10=Sword work. What it means for you is that a verb
whose arguments may contain = — free text, or options
written as key=value — should read argstr, the
whole unsplit argument string, and split it itself. Everything else
should use dobj/prep/iobj, which
are right for the grammar they model and save you the work.
The rule of thumb: if your usage line contains a literal
=, or free text a player might type an = into,
parse argstr. Otherwise trust the slots.
Write your own type when the standard split gets in the way. The usual reason is
a command whose argument is free text that must not be broken at a preposition:
a whisper that would otherwise split "meet me at the gate" at
at, or a command taking name: value pairs.
A custom type is a small Python class in the engine's source that overrides one
method — parse() — and writes its results into the same named
slots. Because it is engine code rather than a verb, adding one needs a server
restart, and it is a rare thing to need. Full details are in
Writing Verbs in docs/manual/.
A verb type can also wrap the verbs that use it. Two methods fire around
every use of such a verb — whether typed by a player, reached through
call_verb, or triggered as an exception:
at_pre_cmd() before the verb; return True to cancel it
at_post_cmd() after the verb, even if it failedThe use for this is a check that many verbs share. Rather than opening thirty combat verbs with the same "are you still recovering?" guard, give them a type that does it once:
# in the engine source
class TimedVerb(MasterVerb):
def at_pre_cmd(self):
rt = getattr(self.pobj, 'rt', 0) or 0
if rt > 0:
self.pobj.msg(f"You must wait {rt} more seconds.")
return True # the verb never runsThe verb body does not start at all, so there is no path through your code that can forget the check — and it applies to verbs reached from other verbs, which a guard pasted at the top of each one would miss. Use it for facts about the actor: recovering, stunned, seated. A check that needs to know what was targeted belongs in the verb itself.
Exception verbs are machinery, not commands. Nobody should be able to type
pull_ and fire one directly — it would run without the checks the
shared verb does first, and it makes no sense as a command in its own right.
Hiding is separate from authority levels: a hidden verb cannot be typed by anyone, whatever their level, and can only be reached by other code.
> @adverb/hidden #204.pull_ # create it hidden — the right way
> @hideverb #204.pull_ # or hide it afterwards
> @unhideverb #204.pull_
The trailing underscore is a naming convention that reminds you; it is
@hideverb that actually enforces it. A hook you forget to hide is a
command your players can type.
Two commands, in this order. @adverb creates the verb;
@program opens an editor for its code.
> @adverb #17.pull
> @program #17.pullA verb can answer to several names, and each name can be abbreviated. The number in brackets is the fewest letters that will do:
> @adverb #17.examine(3),look(1),l
examine, exa, look, l all work — but 'ex' does not
A minimum of 0 — the default — means the name must be typed in full. Set one
later with @min #17.examine = 3. Abbreviations count the
@: @article with a minimum of 4 answers to
@art.
| Command | Does |
|---|---|
+verbs <obj> | List the verbs on an object. |
+decompile <obj>.<verb> | Show the code exactly as stored — the ground truth. |
@rmverb <obj>.<verb> | Delete a verb. |
@min <obj>.<verb> = <n> | Set the abbreviation minimum. |
A verb attached to an object beats one inherited from further up, with no
warning. That is the intended mechanism — it is how one object overrides
shared behaviour — but it is also the thing that actually catches people
out. If a command has stopped behaving the way it does elsewhere, look
for a same-named verb closer to hand with +verbs.
Every verb carries a short permission string, and the default —
rx — is what you want almost every time: anyone may
read its code, anyone may execute it. Add
w only if others should be able to rewrite it.
> @adverb #17.pull with rx # the default, spelled out
Permissions are about the verb's code. Whether a player may use the
command is decided by auth, above, and by
hiding.
One verb runs at a time. That is deliberate: verb code freely reads a property, changes it, and writes it back, and two verbs interleaving would silently lose one of the changes. Everything you write can assume it has the world to itself.
The catch is that anything a verb waits for, the whole game waits for — every player, every ticker. So there are three ways to wait, and picking the wrong one freezes your world.
| Use | When | What happens |
|---|---|---|
suspend(n) | You want to carry on after a wait | Steps aside for n seconds. Other verbs run. Yours resumes on the next line. |
request(...) | You are calling something outside the server | Returns at once. The answer arrives later by calling a verb. |
pause(n) | Almost never | Freezes the entire game for n seconds. |
suspend(n) hands your turn back. Other verbs run while you are
parked, and execution picks up on the very next line with your variables
exactly as you left them:
pobj.msg("You begin meditating...")
suspend(5)
pobj.msg("You feel refreshed.")
That makes patrols, slow rituals and staged events straightforward — a guard can walk its round without anyone else noticing:
for _step in ('north', 'east', 'south', 'west'):
call_verb(this, 'gmove', args=_step)
suspend(10)
Nothing else runs while your verb is executing, but plenty can run
while it is parked. An object you looked at before the suspend
may have moved, changed hands, or been recycled by the time you wake. Re-read
what matters afterwards rather than trusting what you read before — the same
rule MOO has always had.
A single suspend is capped at 300 seconds. For anything longer,
schedule a fresh task with delay(n, code) instead of holding a
parked verb open for an hour.
request() is for reaching something outside the server — a web
service, a local model, anything that answers over HTTP. It returns
immediately, and the answer comes back by calling a verb of your choosing:
# in a verb on the NPC
request('http://127.0.0.1:11434/api/generate',
reply='npc_said', on=this, method='POST',
json={'model': 'llama3.2', 'prompt': argstr, 'stream': False},
tag=pobj.objnum)
The reply verb receives what happened as ordinary variables — ok,
status, body, error, and whatever you
passed as tag:
# npc_said, on the same object, some time later
if not ok:
this.msg_room("&S looks momentarily vacant.", sub=this)
return
import json as _j
this.msg_room(_j.loads(body).get('response', ''), sub=this)
tag comes back untouched, which is how you match an answer to the
question that prompted it — the player who spoke, say, when several
conversations are in flight at once.
By the time a request fails, your verb has already returned, so there is
nowhere for an exception to go. A timeout, a refused connection or an HTTP
500 all arrive at the reply verb with ok false and
error saying why. Check ok first.
The response body is never run as code. It arrives as a value, so a model that happens to emit something resembling Python — or an endpoint that does so on purpose — cannot get it executed.
The in-game editor is fine for a few lines. For anything longer you will want your own editor, and verbs can live as ordinary files:
verbs/17/pull.py # the shared pull verb
verbs/204/pull_.py # the statue's exception
The folder is named after the object's number, the file after the verb. When
you start a world with megamoo --dev, MegaMOO watches that tree and loads
changes within about two seconds — save the file and the next player to type
the command runs your new code. No reload, no restart.
To get an object's verbs onto disk in the first place, use @reload
once. If the folder does not exist it is created and the object's existing
verbs are written into it:
| Command | Does |
|---|---|
@reload #204 | Creates the folder if needed and exports the object's verbs, then loads what is there. |
@reload #204.pull_ | Loads one verb from its file. |
@reload all | Sweeps every folder and loads everything. |
You only need @reload when a file changed by some means other than
the watcher — a git pull, say. After an ordinary save, the watcher
has already done it.
Two copies of a verb exist — the file and the one loaded in the running world — and the rule is that the file wins. It is what your editor opens and what git tracks, so it is the one that has to be authoritative for anything else to make sense.
Everything follows from that. @program and
@port write the file before the database, so
a failed write leaves nothing changed rather than a live verb with
no file behind it, and they ask once rather than twice — answering
yes to the verb and no to the file used to leave the two
disagreeing. At startup the server reads the tree and reconciles:
a file with no verb behind it becomes one, and a file that changed
while the server was down replaces what the database is holding.
Edit verbs with the world stopped, git checkout a
branch, rename something in bulk — it all lands.
A syntax error still cannot take the game down: the broken file is refused and the previous working version keeps serving until you fix it.
Each world remembers its own verb folder in
#8.moo_verb_path, and megamoo init points
it at the verbs/ directory beside your world file.
Move a world to a differently-named folder without updating that
and your edits quietly stop landing, because the world is still
watching the old path.
game/ package
Verbs are the world's behaviour. Some things are not behaviour — combat
tables, chargen rules, price lists, anything that is data or shared
helpers — and those do not want to be a verb on an object. They go in
game/, the Python package megamoo init creates
beside your world:
mygame/
world.db
verbs/
game/
__init__.py
combat.py # yoursA verb imports from it the ordinary way:
from game.combat import swing
pobj.msg(swing("axe"))
That is the same spelling verbs already use for engine modules like
moo.objects. There is no plugin registry, no hook to
register, nothing new to learn — the engine puts your game directory on
sys.path when it finds a game package beside the
world file, and normal Python does the rest.
Before there was a game/, the only place to put such
code was inside the engine — and that is how an engine gets forked.
The development world this guide was written alongside grew four
modules under moo/, with nine verbs importing them,
and could no longer run on a stock MegaMOO at all. Putting your
code in game/ is what keeps
pip install --upgrade megamoo from being a merge.
Verbs suit this unusually well. Each one is a short, self-contained file with a fixed set of variables available and no imports to resolve — a small, bounded problem. With the tooling connection open, an assistant holds the live world: it can write the verb, run it as a test character, read what the game printed back, and fix it, without you retyping anything.
Three things make it work in practice:
megamoo --dev scratch.db and have the assistant confirm which world it
is on before it writes.pull_ to #204 that opens the bookcase and reveals
the passage" is a complete request. "Make the statue do something" is
not.