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
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.
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
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_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
|
required |
Returns:
| Type | Description |
|---|---|
ContentPack
|
The reconstructed pack. |
Raises:
| Type | Description |
|---|---|
ContentValidationError
|
If the envelope is malformed or is not a |
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 |
id
class-attribute
instance-attribute
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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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
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 |
required |
equipment
|
EquipmentCatalog
|
The shipped equipment catalog, usually
|
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'