← 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 #12 for out-of-character rooms and #13 for in-character ones. Put a verb on #13 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 81 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.
gm3ProgrammerEverything a builder does, and write verb code: @program, @adverb, eval.
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.

That is a deliberate trade, and it is the one classic MOO made in reverse: there, ordinary players could program because the MOO language was itself the sandbox. Python is not, so MegaMOO puts the boundary at who may write a verb instead. A coder is staff.

What gm3 cannot do is reach anyone else's things. Ownership decides that, not level: a verb runs as its owner, so a builder's code writes their own objects and is refused on yours — and on auth, which #0 owns. That is enforcement, not etiquette. It is not a defence against a coder who means harm, and nothing in-process could be.

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 #13 and every IC room has it:

# the shared `pull` verb on #13 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: look_, go_, get_, put_, drop_, give_, examine_, open_, close_, latch_, unlatch_, lock_, unlock_, sit_, lay_, and the in_/on_/under_/behind_ container variants.

A world of your own adds its own. Shadowfall carries eat_, drink_, wear_ and remove_, for instance, because it has the commands to call them; the shipped world has neither.

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 #201 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 #201=A room parses the same as @desc #201 = 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 #201=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.

Verbs nobody types: Type: function

A hook or a utility verb is never typed. look_here, in_get, $string_utils:capitalise — these are reached from other code through call_verb, which hands them their arguments positionally. There is no command line behind them, so there is nothing for the parser to split.

Declaring one a function says so, in the verb file itself:

"""
name_match on $match_utils.

Type:    function
"""

That skips the whole command-parsing lifecycle. Measured against MasterVerb on the same verb, parsing was 2.06µs of a 6.02µs call — 34% of it, spent deciding that an empty argument string contains no direct object. It is not a new execution path: a verb with no type instance was always legal, and this reaches that state deliberately rather than by a verb type failing to build.

When not to use it. If the verb reads dobj, iobj, prep, lhs/rhs, switches or regex_match, it wants the parse and must stay a MasterVerb.

That includes a hook called as call_verb(npc, 'react', args='angry'): MasterVerb sets dobj to 'angry', and a function verb leaves it ''. The difference is silent, which is why it is worth stating twice.

at_pre_cmd() does not fire on a function verb either, and for the same reason: the veto exists to stop a typed command before its body runs, and there is no typed command here to stop.

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 #13.pull > @program #13.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 #13.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 #13.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 #13.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(seconds, code, context) instead of holding a parked verb open for an hour. All three arguments are required — context is the namespace the code runs in, usually globals().

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.

Your editor, and the names it cannot see

Verb code is ordinary Python, but the engine hands the verb its context — pobj, this, db, call_verb and about 330 others — instead of the file importing it. Your editor does not know that, so it marks every one of them undefined: dozens in a single verb, all of them wrong. Across the starter world that was 4,683 false errors.

Your game ships with a ruff.toml in the verb tree that declares those names, and a verb_context.pyi so a language server can complete them rather than merely tolerate them. Both are already there; you do not have to do anything. If you upgrade MegaMOO and want the list refreshed:

python tools/gen_verb_lint_config.py <your-game-dir>

The list is assembled from the places the engine actually defines those names, so it cannot drift out of step with the engine you are running.

Both files sit inside the verb tree rather than at the top of your game, and deliberately: a linter uses the nearest config to each file, so your own Python under game/ gets none of these declarations. An undefined name there is still a real mistake, and should still be reported.

Names your own game injects

If you call a verb with keyword arguments of your own — call_verb(target, 'hit', attacker=pobj) — those arrive as names too, and the shipped list has no way to know about them. Put them one per line in verb_lint_extra.txt beside the config and regenerate.

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 #39.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: