Bringing an old world across, and the commands you already know.
MegaMOO reimplements the MOO model — numbered objects, single inheritance, properties and verbs living on the objects themselves, edited while the world runs. It does not implement the MOO language. Verbs here are Python.
That is the whole of the incompatibility, and it is worth being blunt about it: a LambdaMOO core will not run on MegaMOO. What this chapter covers is what you can do — bring the world across, keep the code where you can read it, and port it verb by verb.
Use mooR. It is a mature reimplementation in Rust that reads LambdaMOO databases and executes MOO verbs directly, so an existing core keeps working. MegaMOO is the right choice when you would rather write in Python; mooR is the right choice when you have a core you want to keep running.
Importing a whole LambdaMOO database is not part of the engine. It lives in
a separate tool, mooport, and that is deliberate: reading a
1994 textdump and deciding what its verbs mean is a different job from
running a game, and MegaMOO is meant to stay a lean Python world-building
platform rather than become a MOO compatibility layer.
It is also, usually, the wrong thing to want. Measured against stock LambdaCore, seven verbs in ten are redundant before translation starts — the utility objects, the editors, both quota systems, login, the help databases, the option packages. MegaMOO does all of that in engine code, and a world carrying its own copy would be carrying a second implementation of its host. What is worth reclaiming from an old MOO is the world: the rooms, the things, the characters, the behaviour somebody wrote by hand.
mooport is not published yet — it is being written alongside
the engine, and the measurements above come from running it against stock
LambdaCore and a sixteen-year-old Inferno database. If you have a world
you want to bring across, say so: knowing there is somebody waiting is
the thing most likely to get it released.
They change, and they have to. A LambdaMOO #10 cannot become a
MegaMOO #10 — that number is already the base object here, and
a core landing on top of the shipped hierarchy would destroy both.
Every imported object is created fresh, and every object reference stored
inside a property is rewritten to match. Each object records where it came
from in moo_import_id, so you can always find the thing that
used to be #57:
@find 57 isa #1
@ex #412
A reference pointing at something that was not imported — a skipped player, or a number the source database never had — is left visibly as a number and counted in the report. Pointing it at whatever now holds that number would be worse than leaving it obviously broken.
Most survive untouched. Two kinds cannot:
flags,
owner, location, contents and
friends. Assigning to those sets the real thing, not a property.
Both are prefixed rather than dropped: _mail_task becomes
moo_mail_task, flags becomes
moo_flags. Every rename is listed in the import report, so
nothing changes name quietly.
aliases is the one happy exception. MOO means the same thing by
it that MegaMOO does, so it maps straight onto the native attribute.
This is the part worth understanding properly. Verb code comes across, but nothing imported can run — it is MOO source, and this engine executes Python.
Rather than discard it, the importer keeps every verb's original source
exactly as it was, stored as comments beneath a docstring that records where
it came from and how to port it. The verb itself is stored
hidden, without the execute permission, and at
gm3, so no player can reach it by typing its name, and its body
ends in return None so that even a contrived call does nothing.
The effect is that your old code is sitting on the right objects, under the right names, waiting to be rewritten — instead of in a tarball you never open. To see what is left:
@grep UNPORTED MOO SOURCE
To port one, read the source in the verb, write the Python in its place, and
give the verb rx permissions and an auth level. Delete the
docstring when you do — it is what marks the verb as un-ported.
Player objects are left out unless you pass /players. A
player object carries a password hash and a connection history that mean
nothing here, and importing one creates an account nobody can log into.
Most lines fall to a small substitution table. The shape of the code — what
is on this, what the verb is called with, how it talks to the
player — does not change.
| MOO | MegaMOO |
|---|---|
player:tell("You have ", n) | pobj.msg(f"You have {n}") |
this:verbname(x) | call_verb(this, 'verbname', x) |
pass(@args) | pass_(*args) |
suspend(n) | suspend(n) (same meaning: others run, you resume on the next line) |
$string_utils:foo(x) | su.foo(x) |
$object_utils:foo(x) | ou.foo(x) |
player | pobj |
caller_perms() | caller |
length(x) | len(x) |
tostr(a, b) | f"{a}{b}" |
typeof(x) == LIST | isinstance(x, list) |
{1, 2, 3} | [1, 2, 3] |
x[1] (1-based) | x[0] (0-based) |
this.location | this.location (unchanged) |
args, argstr, dobj | args, argstr, dobj (unchanged) |
#123 | #123 (unchanged) |
E_PERM | E_PERM (a value here too) |
A few things are worth knowing beyond the table:
result.
Both work here: assign result, or use a plain
return.tell exists. If ported code reads better as
tell(pobj, "a", b), that is provided and does the same as
pobj.msg.E_PERM and the rest can be
returned, stored in a property and compared with ==, as
well as raised — the same as in MOO.E_PROPNF — a PropertyNotFound,
which is also an AttributeError — so
if this.rt > 0 fails on an object with no
rt. Because it is an AttributeError,
getattr(this, 'rt', 0) and hasattr both behave
as you would expect; use those, or declare the property with
@adprop.@port
The table above is what you do by hand. For a whole verb,
@port does the mechanical part for you: paste MOO source,
read the Python it produces, and save it only if you agree.
> @port #92.buy
Porting MOO code into 'buy' on a merchant (#92).
Paste MOO source. '.' alone to finish, '@abort' to cancel.
-----
player:tell("That costs ", price, " coins.");
return 1;
.
-- translated --
tell(pobj, f"That costs {price} coins.")
return 1
Save this to buy on #92? [y/N]
Nothing is saved without a yes. Anything the translator would not guess
at is left in the code as a # PORT: line naming what
needs a person, and a verb saved with marks still in it is stored
hidden, so a half-ported command cannot be reached by a player typing
its name.
What @port saves is the translation with the source
commented underneath it. That matters more than it sounds. A
translation that runs but means the wrong thing is the
expensive failure — it does not raise, it just quietly does
something else — and the only way to see it is to read the
original beside the Python.
It is also what @port/again reads. The translator
improves; when it learns something, a verb that still carries its
source can simply be redone rather than dug out of the textdump
again:
> @port/again #92.buy
Re-translating 'buy' on a merchant (#92) from its kept source (2 lines).It shows the result and asks, exactly as pasting does — it is the same code path. A verb with no kept source says so and stops, rather than opening an empty editor.
"Translated clean" means the translator recognised everything, not
that the verb does what it did. Where MOO and MegaMOO differ
quietly — this binds differently, verbs()
includes inherited names here, verb_info() returns a
dict rather than a 1-based list — the output compiles, runs, and
is wrong. Marked lines are the ones the translator knows about.
The rest is on you, and the way to check is to run the verb.
These exist because a MOO programmer reaches for them without thinking. They behave as you would expect, with the differences noted.
| Command | Level | Does |
|---|---|---|
@find <text> | gm2 | Find objects anywhere by name. in <room> and isa <parent> narrow it; /exact and /count are available. |
@audit [player] | gm2 | Everything a player owns, grouped by location. /rooms, /nowhere for orphans, /count. |
@kids <object> | gm2 | What inherits from an object. /all draws the whole descendant tree. |
@quota [player] | gm2 | How much of the database someone has built. See the note below. |
@grep <text> | gm3 | Search verb code across the database. |
@copy <object> | gm3 | Duplicate an object's own properties. Verbs and contents are deliberately not copied. |
@ps / @kill | gm3 | List running tasks, and stop one. |
@dump <object> | gm3 | Write an object out as text, into dumps/. |
@load <file> | gm3 | Build a new object from a dump. |
MegaMOO has no quota system. @quota tells you what somebody
has actually built — objects, split into rooms, exits and everything else
— and shows a limit only if you have set a quota property
yourself, clearly marked as advisory. Object creation here is not gated
on it.
@dump and @load are the small version of the
importer, and the everyday one: they move a single object between two
MegaMOO databases — a development world and a live one, say.
@dump/verbs #92
@load 92-BaseMerchant
What travels is what makes the object itself: parent, owner, flags, noun,
aliases, properties, and with /verbs its verb code. What does
not travel is where it happened to be — location, contents and children are
relationships to other objects, and those numbers mean something else in the
database you are loading into.
@load always creates a new object. It never overwrites,
it refuses a dump whose parent does not exist locally, and it lists any
object references pointing at numbers this database does not have.
/dry shows you all of that without building anything.