← MegaMOO Guide

Hooks & Internal Verbs

The verbs nobody types, and how to write your own.

What a hidden verb is

A hidden verb is an ordinary verb that a player cannot type. It lives on an object like any other, it is written in Python like any other, and it can be called by other verbs like any other — the only thing it cannot be is the first word of a command.

You mark one by putting a line in its docstring:

""" in_get -- take an item out of this container. Hidden: yes """

The starter world ships 81 of them, answering to 107 names once aliases are counted, spread across 18 objects. They are the seams of the world: the places the engine and the shipped verbs stop and ask this object what it wants to do. Learning them is how you stop editing shared verbs and start attaching behaviour to individual things.

Hidden means untypeable, not unreachable

The decision is made in the parser, at the point where a typed command is matched to a verb — not in find_verb, which is what call_verb uses. So a hidden verb is fully reachable from code and completely unreachable from the prompt. help does not list them either. To see them in-game, staff can use +verbs, which does not filter them out — add /all to include the ones inherited from parents — or @vfind, which marks each match it finds as hidden.

The five ways one gets called

This matters more than it sounds, because the five paths behave differently when the verb is missing, and a missing hook is the normal case rather than an error — most of these are optional by design.

Called byWrittenIf the verb does not exist
Another verbcall_verb(obj, 'in_get')raises KeyError
Another verb, method styleobj.in_get()raises AttributeError — not KeyError
The engine, at a hook pointfire_hook('after_move', obj)nothing happens, silently
A tickerticker_add(1, '_td_rt', obj, id)swallowed; the ticker keeps running
The effects system$eu.trigger(who, 'stun', 5, 1)the effect is dropped on its first fire
The two exception types are not interchangeable

If you guard an optional hook, guard the right thing. This is correct for the call_verb form:

try: if call_verb(item, 'get_'): return except KeyError: pass # no get_ on this item, which is the usual case

The method form item.get_() raises AttributeError instead, and sails straight through that except. Pick one style per call site and match the guard to it.

What arrives inside it

Every verb — hidden or not — is handed the same core names:

NameWhat it is
thisthe object the verb was found on
pobj, playerthe character who caused this to happen
callerthe object whose verb called this one
locationwhere pobj is
dbthe database
verbthe name this was invoked as — the alias, not the filename
args, argstr, argvwhatever the caller passed as text

On top of that, each family gets its own extras, passed as keyword arguments by whatever called it. Those are listed with each family below, and they are the part worth reading before you write a hook — a handler that ignores remaining will misbehave in a way nothing warns you about.

verb tells you which name was typed

Several of these verbs answer to many names and switch on which one was used. #1:_afflict answers to thirteen; _stun and _blind are the same file, reading verb to decide what to do. If you add an alias, remember the body has to know about it.

Exception verbs — <verb>_

This is the pattern the whole design rests on, so it is worth stating plainly: every typeable command should have a verb_ exception after it has matched its target. The shared verb does the parsing, the matching and the "you can't see that here" — then it asks the object whether it would like to handle the rest.

That is how you attach behaviour to one thing without editing the verb everybody uses. The cursed sword that refuses to be picked up is a get_ on the sword, not an if in #13:get.

# get_ on the one cursed sword pobj.msg("The hilt burns. You snatch your hand back.") result = True # handled -- get stops here

Return a true value and the shared verb stops. Return nothing and it carries on as normal, so a hook that only wants to watch can emit a message and fall through.

Most exceptions do not ship, and that is the point

get_, put_, drop_, give_ and examine_ are called by the shipped verbs and defined by nothing. They are the contract, waiting for you. An object without one is the ordinary case, which is why every caller wraps the call in try/except KeyError.

VerbOnWhat it does
open_#17, #21Opens the exit or container. Refuses if locked; announces through to the far side.
close_#17, #21Closes it, and closes the matching exit on the other side.
lock_#17Locks a closed exit. Checks lockable, and the key against this.key.
unlock_#17The reverse, with the same key check.
latch_, unlatch_#17As lock, for a latch — no key involved.
look_#20, #23Replaces the ordinary description. The container lists contents; the furniture lists who is sitting on it.
sit_, lay_#23Seats or beds a character. Checks capacity and sets their position.
go_#43, #44Runs when a character walks into the arch (chargen) or the portal (entering the game).

Container faces — in, on, under, behind

An object has four faces a thing can be put, and each face has three verbs. They are the exception pattern again, one step more specific: #13:put works out which face you meant from the preposition you typed, then calls that face's hook.

> put coin in chest → in_put on the chest > put coin on table → on_put on the table > put coin under rug → under_put on the rug > look behind painting → behind_look on the painting
FaceVerbsOn
insidein_get, in_put, in_look#20 BaseContainer
on top ofon_get, on_put, on_look#9 object
underneathunder_get, under_put, under_look#9 object
behindbehind_get, behind_put, behind_look#9 object

Every object in the world inherits the on, under and behind faces from #9, so you can already put a coin under any rock. Only containers have an inside.

Lifecycle hooks

These are fired by the engine, not by another verb, at moments in an object's life. They are optional everywhere: no verb, nothing happens, no error.

HookOnFires when
at_post_move#3a character has finished moving
on_puppet#5a player takes control of the character — adds them to the room's list
on_unpuppet#5they disconnect or leave the game
enter_func#13, #20something enters the room, or climbs into the container
exit_func#13, #20something leaves it
Some hooks can veto

The engine's before_ hooks — before a move, a recycle, a reparent — read a False return as "do not do this", and stop. A hook that raises is also read as False, deliberately: a broken guard fails closed rather than letting the thing through. The starter ships no before_ verbs; the hook points are there for you.

Effects — do_<name> and $eu

Anything that should happen repeatedly and then stop — a poison, a stun, a spell that burns down — is an effect. You start one and the effects object ($eu, which is #33) does the rest: the scheduling, the counting down, the expiry, the stacking.

_effects.trigger(pobj, 'stun', 5, 1) # 'stun', 5 times, one second apart

Every second, $eu calls do_stun on itself — the effect's name is the verb's name, which is the whole binding. There is no table to register in. Adding a file called do_poison is all it takes to have a poison effect.

What a handler receives

NameMeaning
pobjthe character the effect is on
tickwhich firing this is, counting from 1
remainingfirings left after this one — zero means this is the last
effect_args, effect_kwargsanything extra passed to trigger

The shipped handlers all do the same two things: keep a countdown in the character's status or condition, and clear it on the last tick. That number is what the rest of the world reads — do_wait consults it to decide whether you can act at all.

# do_stun, in full _d = dict(pobj.status or {}) if remaining > 0: _d['stunned'] = remaining else: _d.pop('stunned', None) pobj.status = _d
Do not use setattr

Plain assignment is required here: setattr goes through the permission check, and the effect is running as the character, who does not own their own status. It would be refused.

Building a fresh dict, as above, is no longer necessary — a list or dict read from a property comes back bound to where it came from, and mutating it writes the whole container back. It is still a clear way to write it, and on an inherited property the write lands as a local copy either way.

The thirteen shipped effects

HandlerSetsAlso reachable as
do_stunstatus['stunned']pobj._stun(n)
do_blindstatus['blind']pobj._blind(n)
do_sleepstatus['sleeping']pobj._sleep(n)
do_unconsciousstatus['unconscious']pobj._unconscious(n)
do_paralyzestatus['paralyzed']pobj._paralyze(n)
do_intoxicatestatus['intoxicated']pobj._intoxicate(n)
do_no_parrystatus['no_parry']pobj._no_parry(n)
do_must_parrystatus['must_parry']pobj._must_parry(n)
do_webcondition['webbed']pobj._web(n)
do_bindcondition['bound']pobj._bind(n)
do_entanglecondition['entangled']pobj._entangle(n)
do_immobilizecondition['immobilized']pobj._immobilize(n)
do_imprisoncondition['imprisoned']pobj._imprison(n)

do_intoxicate is the one to read first. It does the state half like all the others, then adds what a game actually wants — an onset line, a line each tick, and a recovery line. Copy its shape, not its prose.

Managing effects

VerbCall it asWhat it does
trigger$eu.trigger(who, name, ticks, interval)Starts one. Triggering the same effect again stacks — the remaining count goes up.
trigger_all$eu.trigger_all(who, [...])Several at once.
cancel$eu.cancel(who[, name])Ends one, or all of them. Calls the handler once more with remaining=0, so the state clears and the recovery message plays.
list_active$eu.list_active(who)What is running, with ticks left.
_tick—The dispatcher. Runs once a second for the whole world, not once per effect.

Ticker callbacks

Below the effects system sit two raw tickers on #1, for numbers that count on their own. They are older and simpler than $eu: no registry, one ticker per thing per character.

VerbAnswers toWhat it does
_td_rt_tick_downCounts roundtime down to zero, then stops itself.
_tick_up_tu_hits, _tu_stamina, _tu_mana, _tu_focus, _tu_adrenalin, _tu_fabricRegenerates a resource back up to its maximum.
A ticker runs as the character, and cannot use setattr

The same trap as the effect handlers, for the same reason. rt and the resource dictionaries are not writable by the character who owns them, so setattr is refused and the timer silently never ticks. Assign directly: this.rt = value.

Character state helpers

These answer questions about a character. Most are called by other verbs to build what a player sees, and they are the ones to override if your game measures people differently.

VerbOnWhat it does
do_wait#3The gate on every action. Returns true when the character cannot act — unconscious, asleep, paralysed, webbed, or still in roundtime.
hands_free#3Which hands are empty, for anything that needs one.
time_ok#3Roundtime alone. Superseded by do_wait, which checks more.
move_to_hand#5Puts an item in a hand. Handles two-handed items.
clear_hand#5Takes it out again.
get_status#5Which statuses are active — stunned, asleep and the rest.
get_condition#5Which conditions are — webbed, bound, immobilised.
get_position#5Standing, sitting or lying, as a number.
postring#5That number as words — "sitting", "lying down".
make_postatus#5Position and status combined, as the room sees it.
look_self#5What a character looks like to somebody else.
rlook#5, #11The same, for staff, with the numbers shown.
_afflict#1Applies an affliction by name. Answers to thirteen: _stun, _blind, _sleep, _web and the rest — each one hands off to $eu.
_resource#1Drains a resource. Answers to _hits, _stamina, _mana, _focus, _adrenalin, _fabric.
_rt#1Applies roundtime and starts it counting down.
_title#1Rebuilds an object's display name from its article, adjectives and noun.
tell#1The MOO spelling of msg(), for pasted MOO code.
_allow#3Reserved, and empty. Nothing calls it.

Movement & rendering internals

The machinery under go and look. You are unlikely to call these, but overriding one is how you change how movement or room descriptions work everywhere at once.

VerbOnWhat it does
gmove#11, #14The generic move: takes the traveller, the destination and the four messages, and performs the move.
move#15, #16, #18, #19One per exit kind. Each checks what its kind cares about — closed, locked, climbable, jumpable — then delegates to gmove.
vmove#15The same for virtual exits, which have no object of their own.
invoke#14Checks whether an exit will let you through, and says why not.
match_exit#11Turns "north" or "2 door" into an actual exit.
look_here#11Builds the room description a player sees.

Writing your own

Nothing special is required. A hidden verb is made the same way as any other — the only difference is the line in its docstring.

> @adverb #412.get_ > @program #412.get_

Then write it, ending the docstring with Hidden:  yes. Four things are worth keeping in mind:

  1. Name it for the verb it serves. A trailing underscore means "the exception for this command". get_ is called by get; do_poison is called for the effect named poison. The name is the wiring.
  2. Return True only if you handled it. For an exception verb that stops the shared command. Return nothing and it continues, which is what you want if you only added a message.
  3. Assign, do not setattr. Mutating a list or dict read from a property now writes it back, so obj.prop.append(x) persists. What still matters is how you assign: setattr is permission-checked, plain assignment is not.
  4. Put the name in the file, not just the database. If you give your verb an alias, add it to the Aliases: line — @alias does this for you. A name that exists only in the database is lost the next time the verb is loaded from disk.
Finding them in a running world

+verbs #20 lists every verb on an object, hidden ones included; /all adds the ones it inherits. And @vfind in_get finds which objects define a given hook. +decompile #20.in_get prints the source exactly as stored, which is the ground truth for what is actually running.