The verbs nobody types, and how to write your own.
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.
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.
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 by | Written | If the verb does not exist |
|---|---|---|
| Another verb | call_verb(obj, 'in_get') | raises KeyError |
| Another verb, method style | obj.in_get() | raises AttributeError — not KeyError |
| The engine, at a hook point | fire_hook('after_move', obj) | nothing happens, silently |
| A ticker | ticker_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 |
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 caseThe 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.
Every verb — hidden or not — is handed the same core names:
| Name | What it is |
|---|---|
this | the object the verb was found on |
pobj, player | the character who caused this to happen |
caller | the object whose verb called this one |
location | where pobj is |
db | the database |
verb | the name this was invoked as — the alias, not the filename |
args, argstr, argv | whatever 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.
<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 hereReturn 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.
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.
| Verb | On | What it does |
|---|---|---|
open_ | #17, #21 | Opens the exit or container. Refuses if locked; announces through to the far side. |
close_ | #17, #21 | Closes it, and closes the matching exit on the other side. |
lock_ | #17 | Locks a closed exit. Checks lockable, and the key against this.key. |
unlock_ | #17 | The reverse, with the same key check. |
latch_, unlatch_ | #17 | As lock, for a latch — no key involved. |
look_ | #20, #23 | Replaces the ordinary description. The container lists contents; the furniture lists who is sitting on it. |
sit_, lay_ | #23 | Seats or beds a character. Checks capacity and sets their position. |
go_ | #43, #44 | Runs when a character walks into the arch (chargen) or the portal (entering the game). |
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| Face | Verbs | On |
|---|---|---|
| inside | in_get, in_put, in_look | #20 BaseContainer |
| on top of | on_get, on_put, on_look | #9 object |
| underneath | under_get, under_put, under_look | #9 object |
| behind | behind_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.
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.
| Hook | On | Fires when |
|---|---|---|
at_post_move | #3 | a character has finished moving |
on_puppet | #5 | a player takes control of the character — adds them to the room's list |
on_unpuppet | #5 | they disconnect or leave the game |
enter_func | #13, #20 | something enters the room, or climbs into the container |
exit_func | #13, #20 | something leaves it |
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.
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.
| Name | Meaning |
|---|---|
pobj | the character the effect is on |
tick | which firing this is, counting from 1 |
remaining | firings left after this one — zero means this is the last |
effect_args, effect_kwargs | anything 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 = _dPlain 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.
| Handler | Sets | Also reachable as |
|---|---|---|
do_stun | status['stunned'] | pobj._stun(n) |
do_blind | status['blind'] | pobj._blind(n) |
do_sleep | status['sleeping'] | pobj._sleep(n) |
do_unconscious | status['unconscious'] | pobj._unconscious(n) |
do_paralyze | status['paralyzed'] | pobj._paralyze(n) |
do_intoxicate | status['intoxicated'] | pobj._intoxicate(n) |
do_no_parry | status['no_parry'] | pobj._no_parry(n) |
do_must_parry | status['must_parry'] | pobj._must_parry(n) |
do_web | condition['webbed'] | pobj._web(n) |
do_bind | condition['bound'] | pobj._bind(n) |
do_entangle | condition['entangled'] | pobj._entangle(n) |
do_immobilize | condition['immobilized'] | pobj._immobilize(n) |
do_imprison | condition['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.
| Verb | Call it as | What 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. |
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.
| Verb | Answers to | What it does |
|---|---|---|
_td_rt | _tick_down | Counts roundtime down to zero, then stops itself. |
_tick_up | _tu_hits, _tu_stamina, _tu_mana, _tu_focus, _tu_adrenalin, _tu_fabric | Regenerates a resource back up to its maximum. |
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.
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.
| Verb | On | What it does |
|---|---|---|
do_wait | #3 | The gate on every action. Returns true when the character cannot act — unconscious, asleep, paralysed, webbed, or still in roundtime. |
hands_free | #3 | Which hands are empty, for anything that needs one. |
time_ok | #3 | Roundtime alone. Superseded by do_wait, which checks more. |
move_to_hand | #5 | Puts an item in a hand. Handles two-handed items. |
clear_hand | #5 | Takes it out again. |
get_status | #5 | Which statuses are active — stunned, asleep and the rest. |
get_condition | #5 | Which conditions are — webbed, bound, immobilised. |
get_position | #5 | Standing, sitting or lying, as a number. |
postring | #5 | That number as words — "sitting", "lying down". |
make_postatus | #5 | Position and status combined, as the room sees it. |
look_self | #5 | What a character looks like to somebody else. |
rlook | #5, #11 | The same, for staff, with the numbers shown. |
_afflict | #1 | Applies an affliction by name. Answers to thirteen: _stun, _blind, _sleep, _web and the rest — each one hands off to $eu. |
_resource | #1 | Drains a resource. Answers to _hits, _stamina, _mana, _focus, _adrenalin, _fabric. |
_rt | #1 | Applies roundtime and starts it counting down. |
_title | #1 | Rebuilds an object's display name from its article, adjectives and noun. |
tell | #1 | The MOO spelling of msg(), for pasted MOO code. |
_allow | #3 | Reserved, and empty. Nothing calls it. |
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.
| Verb | On | What it does |
|---|---|---|
gmove | #11, #14 | The generic move: takes the traveller, the destination and the four messages, and performs the move. |
move | #15, #16, #18, #19 | One per exit kind. Each checks what its kind cares about — closed, locked, climbable, jumpable — then delegates to gmove. |
vmove | #15 | The same for virtual exits, which have no object of their own. |
invoke | #14 | Checks whether an exit will let you through, and says why not. |
match_exit | #11 | Turns "north" or "2 door" into an actual exit. |
look_here | #11 | Builds the room description a player sees. |
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:
get_ is called by
get; do_poison is called for the effect named
poison. The name is the wiring.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.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.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.+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.