Skip to content

osrlib.crawl.content_pack

Content packs: finished rooms you can carry from one adventure to another.

A ContentPack is room content with the geometry left out. Where an AreaSpec binds an encounter, a trap, treasure, and features to particular cells of a particular level, a pack entry carries the same content slots and no cells at all. That is what makes it portable: an authoring tool reads an entry and writes its content into whatever area the target adventure already has, so a pack never places geometry and never has to agree with a map it has not seen.

Sections group the entries by dungeon level, and each section can carry that level's WanderingSpec. Alongside them, the pack bundles the MonsterTemplates its encounters and wandering tables reference beyond the shipped catalog, which is the pack's closure: what it needs that the engine does not already ship. Item templates are deliberately outside that closure, because a bundled item id belongs to the one adventure that carries it and would arrive dangling anywhere else, so a pack's features reference the shipped equipment catalog only.

Ids are what an authoring tool addresses a pack by, so all three sets are checked when the pack is constructed: section ids, entry ids (unique across the whole pack, not only within a section), and bundled monster ids. A pack that breaks any of the three fails to construct rather than surviving as a warning.

A reference that resolves to nothing is legal here, the way it is in an Adventure you have not finished writing. An encounter may name a template neither the pack nor the shipped catalog holds, and validate_content_pack hands those back as PackFinding models instead of raising, so the tool that reads a pack can show you the gaps and let you decide.

Packs serialize as stamped "content_pack" documents (CONTENT_PACK_KIND) and have their own acceptance rules, because a pack is meant to be kept and passed around longer than a save file is. A document stamped by an older schema version loads, one stamped by a newer version fails with SaveVersionError, and every write re-stamps at the current schema and engine versions, so an older pack you load and save comes back current.

Typical usage:

from osrlib.crawl.content_pack import ContentPack, ContentPackEntry, PackSection, validate_content_pack
from osrlib.crawl.dungeon import KeyedEncounter, KeyedMonster
from osrlib.data import load_equipment, load_monsters

pack = ContentPack(
    name="The gnawing dark",
    sections=(
        PackSection(
            id="level-1",
            label="Level 1",
            entries=(
                ContentPackEntry(
                    id="guard-post",
                    name="Guard post",
                    encounter=KeyedEncounter(monsters=(KeyedMonster(template_id="orc", count_fixed=4),)),
                ),
            ),
        ),
    ),
)
document = pack.to_document()
assert document["kind"] == "content_pack"
assert ContentPack.from_document(document) == pack
assert validate_content_pack(pack, load_monsters(), load_equipment()) == ()

CONTENT_PACK_KIND module-attribute

CONTENT_PACK_KIND = 'content_pack'

The kind stamped on a serialized content pack.

Every document osrlib writes has a kind, and this is the pack's. ContentPack.to_document stamps it and from_document refuses anything else, so handing a save file to a pack loader fails with a message naming both kinds rather than producing a nonsense pack. Read it to route a document you have just parsed to the right loader.

ContentPack

Bases: BaseModel

A content pack: sections of geometry-free rooms, plus the monsters they need.

Build one by hand, or have an authoring tool capture it out of an adventure you have already written. Check it with validate_content_pack, write it out with to_document, and read it back with from_document. What you do with the entries is yours: nothing in osrlib applies a pack to an adventure, because only your tool has the mapping from an entry to the target area it belongs on.

A pack is frozen, and its three id rules are checked when you construct it rather than when you validate it, so a pack you have in hand is one whose ids are already sound.

Attributes:

Name Type Description
id str

The pack's own id, when it has one.

name str

The pack's name.

description str

Prose about the pack.

author str

Who wrote it.

sections tuple[PackSection, ...]

The level groupings holding the entries.

monsters tuple[MonsterTemplate, ...]

The monster templates the entries need.

Raises:

Type Description
ValueError

If two sections share an id, if two entries share an id anywhere in the pack, or if two bundled monsters share an id.

id class-attribute instance-attribute

id: str = ''

The pack's own id, empty by default. A pack derived on the fly takes its identity from whatever it was derived from, so only a pack somebody saved needs to mint one.

name class-attribute instance-attribute

name: str = ''

The pack's name, for a panel to show.

description class-attribute instance-attribute

description: str = ''

Prose about what the pack contains and what it is for.

author class-attribute instance-attribute

author: str = ''

Who wrote the pack. Nothing reads it, and it travels with the document as credit.

sections class-attribute instance-attribute

sections: tuple[PackSection, ...] = ()

The pack's level groupings. Section ids are unique, and entry ids are unique across all of them together.

monsters class-attribute instance-attribute

monsters: tuple[MonsterTemplate, ...] = ()

The MonsterTemplates the pack's encounters and wandering tables need beyond the shipped catalog. This is the pack's closure: bundle what your rooms reference and the pack arrives self-contained.

There is no matching item bundle, because a bundled item id belongs to the adventure that carries it. A pack's features reference the shipped equipment and magic-item catalogs, and an entry naming an adventure's bundled item is reported as a gap rather than carried along.

to_document

to_document() -> dict[str, object]

Serialize the pack to a stamped document you can write to a file.

The result is plain JSON-compatible data: an envelope containing the kind, the schema version, the engine version, and the pack itself as the payload. Hand it to json.dump, put it in a database, or send it over a wire. Read it back with from_document.

A write always stamps the current versions, so a pack you loaded from an older document saves as a current one.

Returns:

Type Description
dict[str, object]

The stamped document envelope wrapping the serialized pack.

Examples:

from osrlib.crawl.content_pack import ContentPack

document = ContentPack(name="The gnawing dark").to_document()
print(document["kind"])
# content_pack

from_document classmethod

from_document(document: Mapping[str, object]) -> ContentPack

Load a content pack from a stamped document.

This is the other half of to_document: parse your file however you like, then hand the resulting mapping here. The pack's identity rules are checked on the way in, so a document with duplicate ids fails here rather than later.

An older schema_version is accepted, because pack payloads only grow within a version. There has been one narrowing: schema 3 made TrapSpec refuse trigger="enter" on a treasure trap, and a pre-3 document carrying that combination is repaired in place on the way in, rewritten to "open" exactly as the save migration does. Payload fields this version doesn't recognize are ignored.

Parameters:

Name Type Description Default
document Mapping[str, object]

A document produced by to_document.

required

Returns:

Type Description
ContentPack

The reconstructed pack.

Raises:

Type Description
ContentValidationError

If the envelope is malformed or is not a content_pack document, or if the payload fails validation.

SaveVersionError

If the document's schema version is newer than this library understands, which means the pack was written by a later osrlib than the one reading it.

ContentPackEntry

Bases: BaseModel

One portable room: an AreaSpec with its geometry left out.

An entry carries the content slots an area has, which is prose, an encounter, a trap, treasure, and features, and nothing that binds to a grid. There are no cells. A tool applying a pack picks a target area the adventure already has and writes these slots onto it, so the same entry works on a corridor dead end in one adventure and a vaulted hall in another.

An entry's trap has to be a room trap, the same rule AreaSpec enforces. Treasure traps need no rule here, because they belong to a FeatureSpec and that model already enforces it.

Attributes:

Name Type Description
id str

The entry's id, unique across the pack.

name str

The room's name.

description str

Prose for your front end.

encounter KeyedEncounter | None

The monsters waiting in the room.

trap TrapSpec | None

A room trap over the whole room.

treasure AreaTreasureSpec | None

Generated treasure with nothing guarding it.

features tuple[FeatureSpec, ...]

The keyed things in the room.

Raises:

Type Description
ValueError

If trap is not a room trap.

id class-attribute instance-attribute

id: str = Field(min_length=1)

The entry's id, which has to be unique across the whole pack rather than only within its section. An authoring tool addresses an entry by this alone, so a pack with two entries of the same id fails to construct. It cannot be empty.

name class-attribute instance-attribute

name: str = ''

The room's name, for a panel to list and for the target area to take.

description class-attribute instance-attribute

description: str = ''

Prose describing the room, for the target area to take.

encounter class-attribute instance-attribute

encounter: KeyedEncounter | None = None

The monsters waiting in the room, or None. Any template id it names that neither the shipped catalog nor the pack's own monsters holds is what validate_content_pack reports as a gap.

trap class-attribute instance-attribute

trap: TrapSpec | None = None

A trap over the whole room, or None. It has to be a room trap.

treasure class-attribute instance-attribute

treasure: AreaTreasureSpec | None = None

Treasure the engine rolls on first entry, or None. It names treasure type letters or the unguarded band, so it carries across adventures without needing anything else.

features class-attribute instance-attribute

features: tuple[FeatureSpec, ...] = ()

The keyed things in the room: caches, tricks, and custom content. Their item_ids and magic_item_ids resolve against the shipped catalogs only, since a pack bundles no items of its own.

PackFinding

Bases: BaseModel

One reference a content pack does not cover: what is missing, and where.

validate_content_pack returns these. They are data rather than errors, so a panel can list them beside the pack and let the author decide whether a gap matters. To refuse a pack that has any gap, check for a non-empty result.

Attributes:

Name Type Description
code str

What kind of gap this is.

message str

The specifics, in English.

entry_id str | None

The entry the gap sits on, or None for a section-level one.

Raises:

Type Description
ValueError

If code is not two or more dot-separated snake_case segments.

code instance-attribute

code: str

What kind of gap this is, as a dotted snake_case code namespaced by subsystem, like "pack.encounter.unknown_monster". It is the part a program reads, and it follows the same rule Rejection codes do, so a front end can switch on it instead of parsing the message.

message instance-attribute

message: str

The specifics in English: which entry, which feature, which id. Written for a person reading a list of findings.

entry_id class-attribute instance-attribute

entry_id: str | None = None

The entry the gap sits on, or None when the gap belongs to a section rather than an entry, which today means a wandering table's monster. message names the section in that case.

PackSection

Bases: BaseModel

A pack's level grouping: the entries for one dungeon level, plus that level's wandering table.

Sections exist because three things work on a level at a time: a panel showing the pack level by level, the wandering-monster check, whose scope is a level, and a capture that pulls one level's rooms out of an adventure. Entry ids are unique across the pack, so an authoring tool never needs the section to address an entry. What the section adds is the grouping and the wandering slot.

Attributes:

Name Type Description
id str

The section's id, unique in the pack.

label str

The section's display name.

entries tuple[ContentPackEntry, ...]

The rooms in this section.

wandering WanderingSpec | None

The level's wandering-monster check.

id class-attribute instance-attribute

id: str = Field(min_length=1)

The section's id, unique within the pack. It cannot be empty.

label class-attribute instance-attribute

label: str = ''

What a panel calls this section: "Level 1", "The lower caves".

entries class-attribute instance-attribute

entries: tuple[ContentPackEntry, ...] = ()

The rooms in this section. Their ids are unique across the whole pack, not only here.

wandering class-attribute instance-attribute

wandering: WanderingSpec | None = None

The level's wandering-monster check, or None for a section that has none. An authoring tool writes it onto the target level's wandering. Monster ids in its table are part of what the pack's closure has to cover.

validate_content_pack

validate_content_pack(
    pack: ContentPack, monsters: MonsterCatalog, equipment: EquipmentCatalog
) -> tuple[PackFinding, ...]

Report the references a content pack does not cover on its own.

Call this before you share a pack, or whenever a panel needs to show what is still missing. It follows every id in the pack and reports the ones that resolve nowhere, so you can bundle the monster, change the id, or decide the gap is acceptable for the adventures you mean to apply the pack to.

It checks every monster reference, which is the keyed-encounter lines and the wandering-table rows, against the shipped catalog composed with the pack's own monsters. It checks every feature's item_ids against the shipped equipment catalog and every feature's magic_item_ids against the shipped magic-item catalog, which it loads itself with load_magic_items because packs bundle no magic items.

It never raises. A dangling reference in a pack is as legal as one in an adventure you are still writing, so the gaps come back as data. Compare it with validate_adventure, which does raise, because by then the content is about to be played.

Parameters:

Name Type Description Default
pack ContentPack

The pack to check.

required
monsters MonsterCatalog

The base monster catalog, usually load_monsters. The check composes it with pack.monsters itself.

required
equipment EquipmentCatalog

The shipped equipment catalog, usually load_equipment. Nothing is composed with it, because a pack carries no items of its own. An entry naming an item that some adventure bundles is reported as a gap, which is the right answer for content meant to travel.

required

Returns:

Type Description
tuple[PackFinding, ...]

One finding per gap, in section then entry order. Empty means the pack is self-contained.

Examples:

from osrlib.crawl.content_pack import ContentPack, ContentPackEntry, PackSection, validate_content_pack
from osrlib.crawl.dungeon import KeyedEncounter, KeyedMonster
from osrlib.data import load_equipment, load_monsters

entry = ContentPackEntry(
    id="guard-post",
    encounter=KeyedEncounter(monsters=(KeyedMonster(template_id="grue", count_fixed=1),)),
)
pack = ContentPack(name="The gnawing dark", sections=(PackSection(id="level-1", entries=(entry,)),))
for finding in validate_content_pack(pack, load_monsters(), load_equipment()):
    print(finding.code, finding.entry_id, finding.message)
# pack.encounter.unknown_monster guard-post entry 'guard-post' references unknown monster 'grue'