← MegaMOO Guide

Verbs

Where commands live, and how to add your own.

Where verbs live

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.

Why rooms and not players

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 verbs and the auth system

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.

LevelThey areAnd can
gm1Trusted playerTeleport, hold things, inspect objects.
gm2BuilderDig rooms, make exits, name and describe things, move and delete them.
gm3ProgrammerMake objects, write verbs, set properties, reserve numbers, run code.
gm4WizardRemove player accounts.
gm5OwnerGrant 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 afterwards
gm3 is the security boundary

Verb 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.

Exception verbs

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.

A worked example: the statue and the bookcase

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 True

And 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 verbIs
pobjThe person who typed the command.
thisThe object the exception is attached to — the statue.
return True"I handled it" — the shared verb stops.
Return nothingThe 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.

Verb types and parsing

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 getHolding
dobjThe direct object — what the command acts on.
prepThe preposition found: in, from, to, on
iobjThe indirect object — what follows the preposition.
prep2 / dobj2A second preposition and everything after it. This is how @move ball to #13 with A ball flies in! keeps its message.
lhs / rhsEverything left and right of the preposition.
argsThe whole line after the command word.
switchesOptions 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.

= is matched anywhere, including inside a word

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.

When you need a custom type

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/.

Running something before and after every use

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 failed

The 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 runs

The 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.

Hidden verbs

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.

Adding a verb

Two commands, in this order. @adverb creates the verb; @program opens an editor for its code.

> @adverb #17.pull > @program #17.pull

Aliases and minimums

A 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.

CommandDoes
+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.
Name collisions are silent

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.

Who may read and run it

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.

Waiting, and talking to the outside

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.

UseWhenWhat happens
suspend(n)You want to carry on after a waitSteps aside for n seconds. Other verbs run. Yours resumes on the next line.
request(...)You are calling something outside the serverReturns at once. The answer arrives later by calling a verb.
pause(n)Almost neverFreezes the entire game for n seconds.

suspend

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)
A suspend is a place where the world can change

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

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.

Failures arrive as data, not exceptions

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.

Editing verbs on disk

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:

CommandDoes
@reload #204Creates 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 allSweeps 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.

The file is the source of truth

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.

Where the world looks

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.

Your own Python: the 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 # yours

A 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.

Why this exists

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.

Writing verbs with an AI assistant

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: