A Python library to parse MUD area files.
This project reads area files from old MUDs and presents them as Python objects. The returned objects all use the attrs package, so it is very easy to do things like render the entire tree of objects out as JSON or similar.
| Format | Reader class | Native writer | Source shape |
|---|---|---|---|
| ROM | RomAreaFile |
dumps() / write() |
single tilde-delimited .are file |
| Merc | MercAreaFile |
dumps() / write() |
single tilde-delimited area file |
| GodWars Deluxe | GodWarsAreaFile |
dumps() / write() |
single .are file (#AREA or #AREADATA) |
| Devil's Silence (DSA) | DsaAreaFile |
dumps() / write() |
single .are file whose #AREA header starts DSA Format~ |
| ACK!MUD | AckAreaFile |
dumps() / write() |
single .are file with a letter-keyed #AREA header (K/L/N/I/V...) |
| SMAUG | SmaugAreaFile |
dumps() / write() |
single tilde-delimited area file |
| SWR / FUSS | SwrAreaFile |
dumps() / write() |
single tilde-delimited area file |
| CircleMUD | CircleAreaFile |
dumps() / write() |
indexed world tree (wld/mob/obj/zon/shp) directory |
| tbaMUD | TbaAreaFile |
dumps() / write() |
indexed world tree (zon/trg/wld/mob/obj/shp/qst) directory |
| Medievia | MedieviaAreaFile |
dumps() / write() |
game tree with monolithic lib/medievia.* files and per-zone lib/wld room files |
| CoffeeMud | CoffeeMudAreaFile |
dumps() / write() |
.cmare XML export (areas, item/mob catalogs, nested boardable areas) |
Every reader exposes the same shape: construct it with a path, call
load_sections(), then read the parsed tree off .area.
>>> import area_reader
>>> area_file = area_reader.RomAreaFile('midgaard.are')
>>> area_file.load_sections()
>>> area_file.area
RomArea(name='Midgaard', metadata='{ All } Diku Midgaard', original_filename='midgaard.are', first_vnum=3000, last_vnum=3399, ... )ROM, Merc, SMAUG, and SWR/FUSS areas can be rendered to a canonical native form or written directly:
text = area_file.dumps()
area_file.write("midgaard-canonical.are")The native writers preserve the parsed semantic model and reach a canonical fixed point: parsing their output and rendering again produces the same model and text. They do not claim byte-for-byte reproduction of source whitespace or flag spelling. Native field order and codecs live on the existing attrs models, and unrecognized source sections are retained as native sections.
CircleMUD splits a world across an indexed file tree, so the reader takes the world root directory rather than a single file.
>>> import area_reader
>>> world = area_reader.CircleAreaFile('/path/to/circlemud')
>>> world.load_sections()
>>> world.area
>>> tree = world.dumps()
>>> tree['wld/index']
>>> world.write('/path/to/canonical-circlemud')For this multi-file format, dumps() returns an ordered mapping from paths
relative to lib/world to canonical native text. write() creates that tree,
including every family index. Index order, record-to-file membership, mobile
type, and shop version headers are part of the attrs model and survive a
semantic round trip.
tbaMUD retains CircleMUD's indexed tree but has its own native signature. Use
TbaAreaFile to preserve the four 32-bit flag banks, zone builder and level
metadata, DG triggers and attachments, string-valued zone variables, quests,
object levels and timers, and hidden exits.
>>> import area_reader
>>> world = area_reader.TbaAreaFile('/path/to/tbamud')
>>> world.load_sections()
>>> world.area.triggers
>>> world.area.quests
>>> world.write('/path/to/canonical-tbamud')Directory auto-detection distinguishes a tbaMUD tree from a CircleMUD tree by
its indexed trg or qst family.
Medievia uses one zone file to name its room files, monolithic tagged mobile
and object files, and a shop stream whose numeric headers are not unique.
Construct the reader with either the game root or its lib directory:
>>> world = area_reader.MedieviaAreaFile('/path/to/Medievia')
>>> world.load_sections()
>>> world.area.rooms[100]
>>> tree = world.dumps()
>>> tree['medievia.zon']
>>> world.write('/path/to/canonical-medievia')dumps() returns an ordered mapping relative to lib. Medievia-only room
restrictions, exit messages, stochastic mobile values, object deterioration,
variable reset arity, ignored historical tokens, terminal vnums, and duplicate
shop headers remain represented in the native model and survive a semantic
round trip.
CoffeeMud .cmare files are XML exports. They may be full areas, item or mob
catalogs, or items (ships, caravans, castles) that embed nested boardable areas.
Native CoffeeMud identifiers (string room IDs such as Coffee Grounds#73, class
IDs such as GenMob) are preserved. Modeled tags live on typed attrs fields;
unknown or repeated tags remain ordered XML residuals on the owning object.
>>> import area_reader
>>> coffee = area_reader.CoffeeMudAreaFile('monsters.cmare')
>>> coffee.load_sections()
>>> coffee.area.top_level
'MOBS'
>>> coffee.area.mobs[0].class_id
'GenMob'
>>> canonical = coffee.dumps()
>>> coffee.write('monsters-canonical.cmare')CoffeeMud payload boundaries (MTEXT, ITEXT, RTEXT, and EXDAT) are
escaped once at the document boundary, including nested boardable areas. Typed
field edits are authoritative over the read-side raw_text and raw_data
views. The six upstream example and skill catalogs have semantic and canonical
fixed points and are accepted by CoffeeMud's native MOB/item loaders.
A new ROM 2.4 area is written as a directory of YAML files and compiled to one
.are file. Nobody writes vnums, bit values, reset lines or wear-location
numbers: records have ids, flags have names, and what stands in a room is
written on the room. This needs PyYAML (pip install area-reader[authoring]).
area-reader build SRC [-o OUT] [--format rom|json] [--set DIR ...] [--show-defaults]
area-reader unbuild AREA.are -o SRC
test/authoring/saltworks is a complete ten-room example that uses every
feature; read it first. In outline:
# area.yaml: the header. Every other *.yaml, *.yml or *.json file below the
# directory may hold rooms, mobs, objects and helps, and they are merged.
area:
name: The Salt Works
filename: saltworks.are
levels: [5, 15]
builders: Claude
vnums: {first: 12000, size: 100}
rooms:
gatehouse:
name: The Gatehouse
description: |
Two stumpy towers of tarred timber flank a tunnel.
sector: city
flags: [indoors, law]
exits:
north: yard # makes yard's south exit too
east: {to: tally-office, door: closed, keyword: door oak}
west: 3001 # a vnum in another area
up: {look: The towers go up into the smoke.}
mobs:
- mob: gate-warden
wears: {wield: boat-hook}
carries: [store-key]
objects:
- object: salt-chest
contains: [coin-purse]
mobs:
gate-warden:
keywords: warden gate guard
short: the gate warden
long: The gate warden leans on a boat-hook, counting carts.
race: human
level: 12 # hit, mana, damage and ac follow from the level
act: [sentinel, stay_area]
special: spec_guard
objects:
boat-hook:
keywords: hook boat-hook
short: a long boat-hook
long: A boat-hook as long as a man lies here.
type: weapon
wear: [take, wield]
weapon: {class: polearm, dice: 2d5, attack: pierce, flags: [two_hands]}- Ids and vnums. An id is a lowercase slug, unique among the area's rooms,
mobs or objects.
buildnumbers ids from the header's block in the order they are first defined and records the numbers invnums.lock.yaml, so a later record never renumbers an earlier one.vnum:on a record pins its number. A reference is an id, a bare vnum (any area), orDIRNAME:idfor an id of another source directory named with--set. - Exits. An exit to a room of the same area also makes the opposite exit,
with the same door, keyword and key;
back:overrides its description.oneway: truemakes one side only. When both rooms write the exit they must agree on destination, door and key.doorisopen,closed,locked, orreset_open(opened again at each reset); closed and locked doors get theirDresets on both sides. - Placements. A room's
mobsandobjectsbecome itsM,G,E,OandPresets.countloads a mob several times;limit,max_in_roomandmax_in_worldset the reset limits, which otherwise allow everything written.random_exits: Nis anRreset. - Mobs. A shop, a special procedure and mob programs are written on the
mob. Omitted
hit,damageandacare the values recommended for the level in ROM's builder guide (Rom2.4.doc, Appendix A, with its adjustments for the four class flags); omittedmanais the median of the stock mobiles of that level; omittedformandpartsare the race's.--show-defaultsprints each filled value. In program code@room:id,@mob:idand@object:idbecome vnums. - Objects. Each item type with meaningful values has a named form
(
weapon,armor,container,drink,fountain,food,light,money,potion,pill,scroll,wand,staff,portal,furniture);values: [..five..]is accepted for any type. - Raw escapes. A flag list may be a number or a ROM letter string, and may
hold numbers beside names. Top-level
resets,shops,specialsandmobprogstake raw records, emitted after the compiled ones. - Errors. A fault raises
SourceErrornaming the file and the key path, such asrooms.gatehouse.exits.east.key.buildparses its own output with the ROM reader and writes nothing unless the result equals what it compiled.
build --format json writes the area as JSON in place of the .are file: the
same JSON area-reader AREA.are prints for the built area, so a program that
reads one reads the other. The JSON can also carry what a ROM area file has no
place for, for a world that is not a ROM server:
area:
name: The Castle
vnums: {first: 12000, size: 100}
properties: {gravity: 0.9}
rooms:
landing:
name: The Upper Landing
kind: echoing # what the room is, to the world that loads it
with: {decay: 3} # the kind's parameters
properties: {ceiling: 12} # set on the room as written
exits:
north: {to: corridor, kind: guarded, with: {phrase: open sesame}}
hatch: {to: loft, keyword: hatch, kind: guarded}kind,with,propertiesmay be written on the header, a room, a mob, an object and an exit.kindis a slug;withneeds akind. They appear under the same names on the record in the JSON.builddoes not know which kinds a world has: any slug is accepted.- Named exits. An exit whose name is not one of the six directions is any
slug. It has one side and takes
to,keyword,descriptionand the three keys above, with no door or key: what stands in its way is its kind's business. In the JSON it is one more entry of the room'sexits, with the name as itsdoor. - The ROM part of the area is still rendered, parsed back and compared before
anything is written.
--format romrefuses a source that uses any of this, naming the first place it does.lint,walkandunbuildread area files, not this JSON.
A top-level quests key maps a quest id to a quest, in the fields of the
Mongoose MOO's $quest. Rooms, mobs and objects are named by id, as anywhere
else in the source:
quests:
ledger:
name: The Lost Ledger
aliases: [ledger]
giver: clerk # a mob; credits are paid from its wallet
offer: | # also accept, done, abandon, fail
I have lost the ledger somewhere in the cellar.
eligible: [{state: level, args: [1, 5]}]
time_limit: 0 # seconds, 0 for none
repeatable: false
roles: {item: {object: ledger}} # {room: id}, {mob: id} or {object: id}
stages:
- name: find
objectives: [{event: moved, match: [item, '*', '@party'], text: ledger found}]
- name: return
after: [find] # opens when `needs` (default: all) of these are done
objectives: [{event: moved, match: [item, '@party', giver]}]
rewards: [{kind: credits, amount: 5}]
solution: [quest accept ledger, down, get ledger, up, give ledger to clerk]An objective is an event that must happen count times, its data lined up
with match from the left (* is anything, @party a participant, a role
name that role's thing), or a state that must hold (holds, at, near,
gone, level), whose first argument names a role. solution is required: it
is the list of commands that completes the quest, for a character to type in a
loaded world. build refuses an unknown id, a stage after that names no stage
or makes stages wait on each other, credits with no giver, and a moved
objective whose object cannot be taken. In the JSON, quests maps each id to
the record, with each reference as {"mob": vnum} and so on, and each text as
a list of lines (offer_text, …). --format rom refuses a quest.
Stage dependencies follow the live MOO: needs: 1 opens after any one listed
predecessor finishes; needs: 0 is unconditional. A cycle is accepted only when
the thresholds allow every stage to open from an initial stage. Duplicate or
unknown predecessors are errors. A local giver must have a mob placement, and
both moved and holds objectives require takeable local objects. State arguments
are checked before output: holds takes a role and optional positive quantity,
at/near take one role, gone takes a role and optional bound room role
(default place), and level takes low/high integers and an optional default.
These are structural checks, not proof that the solution commands work: play the
clue and item handoffs in the actual destination engine, including wrong actions,
repeat visits, and another player taking a shared key first.
unbuild writes any ROM area the reader parses in this form: ids slugged from
names, every vnum pinned, two-way exits collapsed, resets hung on their rooms.
For the 48 stock areas with rooms, building the unbuilt directory gives back
the parsed area up to area_reader.authoring.NORMALIZATIONS (reset order,
line comments, and a few values the engine never reads), and unbuilding that
gives the same files again. Resets the form cannot express stay in raw.yaml.
ROM keeps vnums in 16 bits, so a block for a ROM server stays below 32768
(build itself sets no limit). Stock ROM 2.4b6 reads neither #AREADATA nor
mob programs; they come with the OLC and MOBprogram patches, as in QuickMUD.
An area for the stock engine uses header: rom and no programs.
scripts/verify_rom_writer.py UPSTREAM AREA.are --new builds either engine
from its source tree, boots it with the new area, and fails on any bug the
engine logs that it does not log without the area.
area-reader walk [--from VNUM] [--anonymous] [--with PATH ...] AREA.are
walk writes one ROM area out room by room, in the order a walk from the way
in meets them: each room's text, its exits with where they lead and how each
door is left by the resets, its extra descriptions, who the resets put there
with what they wear, carry, sell and say, and what lies on the floor with what
is inside it. Rooms are numbered in walk order, not by vnum; rooms the walk
never reaches are listed last. The walk starts at the first room with an exit
out of the area, or at --from. --with loads other areas so their rooms and
keys can be named; --anonymous leaves out the credits line.
area-reader atlas loads several ROM or Merc area files together and answers
one question about the set. Each path is an area file, or a directory whose
*.are files are all loaded. Answers are text; --json prints the same
answer as JSON. A file that does not parse stops the command with the parser's
error.
area-reader atlas summary test/rom
area-reader atlas reach --from 3001 [--rooms] [--with-portals] [--with-progs] test/rom
area-reader atlas links [--rooms] test/rom
area-reader atlas dangling test/rom
area-reader atlas depends test/rom
area-reader atlas path 3001 3472 test/rom
area-reader atlas find mota test/rom
summary: counts per file, totals, and vnum ranges that overlap between files.reach: what a walk over exits from one room reaches, and what can walk back.--with-portalsadds portals that anOreset places in a room;--with-progsaddsmob transferandmob gtransferfrom the room anMreset loads the mob in.links: which areas have exits to which, and the links with no exit back.dangling: references to vnums no file defines (exits, keys, resets, shop keepers, specials, mob programs, portals, and vnums inmob transfer,gtransfer,otransfer,goto,at,mloadandoloadlines), and the exits whose destination is 0 or less.depends: the other areas each area refers to, by kind of reference.path: the shortest path between two rooms.find: rooms, mobs and objects by name or short description.
The same questions are functions of area_reader.atlas, each returning the
dictionary that --json prints.
area-reader lint judges ROM 2.4 area files: it lists their defects and
measures them. Each path is an area file or a directory of *.are files.
--with paths are loaded only so references into other areas resolve; they
get no findings. The files are read as ROM, and one that does not parse stops
the command with the parser's error.
area-reader lint [--json] [--with PATH ...] [--min-severity error|warning|info] PATH ...
area-reader lint new.are --with test/rom
A finding has a rule (a stable id such as dangling-exit-key), a severity
(error, warning or info), the file, a kind (room, mob, object,
reset, shop, special, program or area), a vnum or, for a reset, its
index among the file's resets counted from 0 without comment lines, and a
message. Text output is one line per finding under each file's name, then
the count per severity. --json prints {"findings": [...], "counts": {...}, "metrics": {file: {...}}}. --min-severity shortens the list of findings;
the counts always cover all of them. The exit status is 1 when there is any
error finding, else 0.
- Errors are what stops ROM booting or leaves a record that cannot work: a reference to a vnum nothing defines, a vnum outside the area's range or defined twice, resets out of order, an unknown spec function, item type, position, sex, size, liquid or section, a broken mob program, and an empty name, short description, room description or mob long description.
- Warnings are defects ROM runs with: one-way exits, doors that disagree between their two sides, rooms cut off from the rest, mobs, objects, keys and programs nothing loads or uses, shops without stock, mob hit points, damage and armor class far from the table in ROM's builder guide, vnums above 32767 (ROM holds a vnum in a short), equipment reset into a slot its wear flags do not allow, doors without a keyword, and prose that is short, repeated, too wide, miscapitalized or tells the reader what they feel.
- Info notes levels outside the area's
{low high}range, exits to nowhere, descriptions that say a way leads where the room has no exit, door sides with noDreset or reset to a different state than the far side, extra descriptions the room text never names, mobs both aggressive and wimpy, shopkeepers that can be killed, andoldstyleconversion leftovers. - Metrics, per file: record counts, exits per room, room description length, the shares of rooms with an extra description, of distinct room names and descriptions, of room descriptions that say "you", and of mobs with a program or spec function, doors, locked doors and keys, item type and sector counts, the mob level spread, and the type-token ratio of all description words.
area_reader.lint.RULES maps each rule id to its severity and its function;
area_reader.lint.lint(atlas, targets) returns the dictionary that --json
prints, for the entries of an atlas given as targets.
PROJECT.md— the larger goal: a shared "virtual world algebra" across MUD dialects.circlemud.md— CircleMUD support plan.coffeemud.md— CoffeeMud support plan.