Skip to content

osrlib.crawl.content_pack

Content packs: portable, geometry-free keyed content an editor can carry between adventures.

A ContentPack is finished room content with the geometry left behind: sections of entries that mirror AreaSpec minus its cells, each section optionally carrying a level's WanderingSpec, plus the bundled MonsterTemplates the entries' encounters and the sections' wandering tables reference — the pack's closure. Item templates are deliberately not part of that closure: a pack's features reference the shipped equipment catalog only, since a bundled item id belongs to the one adventure that carries it and would arrive dangling in any other. Packs are how an authoring tool moves stocked rooms from one adventure to another: the consumer writes an entry's content into a target area it already has, so a pack never places cells, transitions, or any other geometry.

Identity is the contract consumers stand on: section ids, entry ids (pack-wide), and bundled monster ids are each unique, enforced at construction — a pack that breaks them refuses to load rather than lingering as a diagnostic. Dangling references are legal by the same posture as Adventure: an encounter may name a template neither the pack nor the shipped catalog carries, and validate_content_pack reports such gaps as structured PackFindings instead of raising — the first consumer is a panel that lists findings, not a gate.

Packs serialize as stamped "content_pack" documents (CONTENT_PACK_KIND), the longest-lived artifacts in the document family, and own their acceptance rules: a document stamped by an older schema version is accepted on load, one stamped by a newer version fails with SaveVersionError, and any write re-stamps at the current schema and engine versions — a loaded older pack saves as a current one.

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 stamped-document kind for serialized content packs.

ContentPack

Bases: BaseModel

A content pack: sections of geometry-free entries plus their monster closure.

monsters bundles the MonsterTemplates the pack's encounters and wandering tables reference beyond the shipped catalog. id defaults empty: a pack derived on the fly takes its identity from its source, and only a persisted pack mints an id of its own.

id class-attribute instance-attribute

id: str = ''

name class-attribute instance-attribute

name: str = ''

description class-attribute instance-attribute

description: str = ''

author class-attribute instance-attribute

author: str = ''

sections class-attribute instance-attribute

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

monsters class-attribute instance-attribute

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

to_document

to_document() -> dict[str, object]

Serialize to a stamped document with schema and engine versions.

A write always stamps the current versions: re-serializing a pack loaded from an older document re-stamps it as a current one.

Returns:

Type Description
dict[str, object]

The stamped document envelope wrapping the serialized pack.

from_document classmethod

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

Load a content pack from a stamped document.

A schema_version older than the current one is accepted: pack payload changes are additive within a version, and the one narrowing — schema 3 made TrapSpec reject trigger="enter" on a treasure trap — is repaired in place, rewriting the dead value to "open" exactly as the save migration does. Unknown payload fields are ignored, per the additive-schema contract.

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 or payload is malformed or of the wrong kind.

SaveVersionError

If the document's schema version is newer than this library understands.

ContentPackEntry

Bases: BaseModel

One portable room: AreaSpec minus its geometry.

The entry carries the content slots an area has — prose, encounter, trap, treasure, features — and nothing that binds to a grid: there are no cells, and a consumer writes the carried slots into a target area it already has. An entry trap must be a room trap, the same rule AreaSpec enforces; the treasure-trap coupling needs no restatement because it lives on FeatureSpec itself.

id class-attribute instance-attribute

id: str = Field(min_length=1)

name class-attribute instance-attribute

name: str = ''

description class-attribute instance-attribute

description: str = ''

encounter class-attribute instance-attribute

encounter: KeyedEncounter | None = None

trap class-attribute instance-attribute

trap: TrapSpec | None = None

treasure class-attribute instance-attribute

treasure: AreaTreasureSpec | None = None

features class-attribute instance-attribute

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

PackFinding

Bases: BaseModel

A structured self-containment gap in a content pack.

code is dotted snake_case namespaced by subsystem, the Rejection discipline. entry_id names the entry the gap sits on, or None for a section-scoped gap (a wandering table's); message carries the specifics either way.

code instance-attribute

code: str

message instance-attribute

message: str

entry_id class-attribute instance-attribute

entry_id: str | None = None

PackSection

Bases: BaseModel

A pack's level grouping: entries plus the level's optional wandering table.

Sections are structural because three consumers operate on a level grouping — a panel's level rows, wandering's level scope, and a level-scoped capture. Entry ids are unique pack-wide, so consumers address entries by id alone; the section contributes presentation grouping and the wandering slot.

id class-attribute instance-attribute

id: str = Field(min_length=1)

label class-attribute instance-attribute

label: str = ''

entries class-attribute instance-attribute

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

wandering class-attribute instance-attribute

wandering: WanderingSpec | None = None

validate_content_pack

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

Report a pack's self-containment gaps — references its closure does not cover.

Checks every monster reference (keyed-encounter lines and wandering-table rows) against the union of the shipped catalog and the pack's bundled monsters, every feature's item_ids against the shipped equipment catalog, and every feature's magic_item_ids against the shipped magic-item catalog (load_magic_items — packs bundle no magic items, so the check loads it itself). Findings are data, not errors: a dangling reference is legal in a pack exactly as it is while editing an adventure, and a caller wanting a gate checks for a non-empty result.

Parameters:

Name Type Description Default
pack ContentPack

The pack to check.

required
monsters MonsterCatalog

The base monster catalog — the check unions it with pack.monsters internally.

required
equipment EquipmentCatalog

The shipped equipment catalog feature contents resolve against. Packs carry no item bundle of their own, so there is nothing to union here: an entry naming an item some adventure bundles reports as a gap, which is the honest answer for a portable pack.

required

Returns:

Type Description
PackFinding

One finding per gap, in section and entry order; empty means the pack is

...

self-contained.