Skip to content

osrforge.overrides

Overrides application: the human correction channel, applied inside assembly.

Contract models live in contracts/overrides.py; this module is assembly-side application code — the same split as report contracts vs. report production.

The anti-silent-no-op rule, pinned: every entry in a correction file must take effect or application fails with OverrideError naming the entry — a typo'd monster name or area address that silently did nothing would let a human believe a correction landed when it didn't. The division of labor: addressing errors are loud; content validity flows to the report. An override whose template_id doesn't exist in the catalog does take effect — the dangling id lands in the draft and validate_adventure reports it in report.json, which is exactly the loop the human is already in.

Every function here is pure: application is part of assembly, and assembly's artifacts stay a deterministic function of the stage caches plus the overrides file.

AREA_OVERRIDE_FIELDS module-attribute

AREA_OVERRIDE_FIELDS = ('name', 'description', 'encounter', 'trap', 'treasure', 'features')

The replaceable per-area fields, in the pinned overridden vocabulary order.

LevelOverridePlan dataclass

LevelOverridePlan(
    area_overrides: dict[str, AreaOverride] = dict[str, AreaOverride](),
    adds: tuple[tuple[str, AreaOverride], ...] = (),
    removed: frozenset[str] = frozenset(),
    cells: dict[str, tuple[Position, ...]] = dict[str, tuple[Position, ...]](),
    edges: dict[str, Edge] = dict[str, Edge](),
    entrance_set: bool = False,
    entrance: Position | None = None,
    transitions_set: bool = False,
    transitions: tuple[TransitionSpec, ...] = (),
)

One survey level's resolved override application, addressing-validated.

Attributes:

Name Type Description
area_overrides dict[str, AreaOverride]

Survey-area key → its field-replacement entry.

adds tuple[tuple[str, AreaOverride], ...]

(key, entry) pairs for added areas, in correction-file order.

removed frozenset[str]

Survey-area keys whose AreaSpec is skipped (their placed cells become corridor — removal deletes content, not floor plan).

cells dict[str, tuple[Position, ...]]

Area key → overridden cell cluster (survey areas and adds).

edges dict[str, Edge]

The override edge map, canonically keyed.

entrance_set bool

Whether the entrance was overridden (a None value with this set clears it).

entrance Position | None

The overridden entrance.

transitions_set bool

Whether the transitions tuple was overridden.

transitions tuple[TransitionSpec, ...]

The overridden transitions.

area_overrides class-attribute instance-attribute

area_overrides: dict[str, AreaOverride] = field(default_factory=dict[str, AreaOverride])

adds class-attribute instance-attribute

adds: tuple[tuple[str, AreaOverride], ...] = ()

removed class-attribute instance-attribute

removed: frozenset[str] = frozenset()

cells class-attribute instance-attribute

cells: dict[str, tuple[Position, ...]] = field(default_factory=dict[str, tuple[Position, ...]])

edges class-attribute instance-attribute

edges: dict[str, Edge] = field(default_factory=dict[str, Edge])

entrance_set class-attribute instance-attribute

entrance_set: bool = False

entrance class-attribute instance-attribute

entrance: Position | None = None

transitions_set class-attribute instance-attribute

transitions_set: bool = False

transitions class-attribute instance-attribute

transitions: tuple[TransitionSpec, ...] = ()

OverridePlan dataclass

OverridePlan(
    levels: dict[tuple[str, int], LevelOverridePlan] = dict[tuple[str, int], LevelOverridePlan](),
    town: TownOverride | None = None,
    module: ModuleOverride | None = None,
)

The whole correction file, resolved against one survey index.

Attributes:

Name Type Description
levels dict[tuple[str, int], LevelOverridePlan]

(dungeon id, level number) → that level's plan; only levels some entry touches appear.

town TownOverride | None

The town entry, validated non-empty.

module ModuleOverride | None

The module entry, validated non-empty.

levels class-attribute instance-attribute

levels: dict[tuple[str, int], LevelOverridePlan] = field(default_factory=dict[tuple[str, int], LevelOverridePlan])

town class-attribute instance-attribute

town: TownOverride | None = None

module class-attribute instance-attribute

module: ModuleOverride | None = None

apply_level_overrides

apply_level_overrides(geometry: LevelGeometry, plan: LevelOverridePlan | None) -> LevelGeometry

Apply one level's plan to its synthesized geometry, before LevelSpec construction.

Overridden cells replace an area's cluster wholesale and are kept in the author's order (the human's word verbatim); a removed area's placed cells become corridor; added areas append after the survey areas. Override edges merge over the synthesized map, override winning per canonical key (a wall entry legitimately seals a synthesized opening), and the merged map is re-sorted under the pinned key order for byte stability. width and height are recomputed as the bounding box of the final area and corridor cells. Synthesis postconditions are not re-asserted — human input is a runtime condition, not a bug; what a bad geometry override breaks, validate_adventure and the playability lint report.

Parameters:

Name Type Description Default
geometry LevelGeometry

The synthesized level geometry.

required
plan LevelOverridePlan | None

The level's plan, or None when nothing touches the level.

required

Returns:

Type Description
LevelGeometry

The effective geometry; extraction-side facts (connection ambiguities,

LevelGeometry

disconnected components) carry through unchanged — no override kind

LevelGeometry

touches the extracted connection graph.

apply_monster_overrides

apply_monster_overrides(resolutions: MonsterResolutions, overrides: Overrides) -> MonsterResolutions

Replace cached monster resolutions with the correction file's remaps.

Override keys match extracted names under the same normalization the monsters stage uses, so "Hobgoblin Chieftain" still hits the cache's "hobgoblin chieftain". A matched override replaces the cached resolution before encounter building: the stand-in (or omission) never happens, no monster_unresolved flag is emitted, and the report's monsters summary counts the name resolved. The result is in-memory only — the monsters cache is never rewritten, so a monster correction needs only assemble.

Parameters:

Name Type Description Default
resolutions MonsterResolutions

The monsters-stage cache.

required
overrides Overrides

The loaded correction file.

required

Returns:

Type Description
MonsterResolutions

The resolutions with every override applied; the input is unchanged.

Raises:

Type Description
OverrideError

If two override keys normalize to the same name (contradictory corrections), or a key matches no name in the cache (the error lists the cache's unresolved names — the likeliest targets).

apply_template_overrides

apply_template_overrides(
    blocks: dict[str, RawStatBlock | None], entries: dict[str, StatBlockOverride]
) -> dict[str, RawStatBlock | None]

Apply the planned template entries to the cached raw blocks, pre-mapping.

An entry patches its name's cached block field by field — absent leaves the extracted value, explicit null clears it back to unprinted — and a name with no cached block (or an absent marker) gets a candidate block from the entry's fields alone. The result feeds the same refusal ladder and mapping an extracted block does; the inputs are unchanged.

Parameters:

Name Type Description Default
blocks dict[str, RawStatBlock | None]

The stat-block cache's blocks (normalized name → block or absent marker).

required
entries dict[str, StatBlockOverride]

The planned entries (plan_template_overrides).

required

Returns:

Type Description
dict[str, RawStatBlock | None]

The effective candidate blocks.

canonicalize_edge_key

canonicalize_edge_key(key: str) -> str

Re-key an override edge key through osrlib's own canonical form.

osrlib stores only north/west keys — a cell's east edge is its eastern neighbour's west edge, its south edge the southern neighbour's north — and consults LevelSpec.edges through edge_key, so a south/east key stored verbatim would never take effect — the silent-no-op hazard this function defuses: "5,2:east" becomes "6,2:west".

Parameters:

Name Type Description Default
key str

An override edge key, x,y:direction, any of the four directions (the EdgeKeyString grammar).

required

Returns:

Type Description
str

The canonical edge key.

effective_roster

effective_roster(survey_level: SurveyLevel, plan: LevelOverridePlan | None) -> tuple[tuple[str, str], ...]

The draft's ordered area roster for one level: (key, name) pairs.

Survey areas in survey order minus removals, then adds in correction-file order — the one ordering rule build_draft and render_previews must agree on, so previews always show the corrected roster.

Parameters:

Name Type Description Default
survey_level SurveyLevel

The survey level.

required
plan LevelOverridePlan | None

The level's plan, or None.

required

Returns:

Type Description
tuple[str, str]

The ordered roster. Names are the survey's for survey areas and the

...

override's for adds (name overrides are content-side and don't affect

tuple[tuple[str, str], ...]

what the preview labels — it renders keys, not names).

plan_overrides

plan_overrides(index: SurveyIndex, overrides: Overrides) -> OverridePlan

Resolve and validate every non-monster override entry against the survey.

All addressing and contradiction errors surface here, before any stage tracking or artifact write — a correction file that cannot take effect fails the command without touching the workdir. Monster overrides validate separately (they address the monsters cache, not the survey) in apply_monster_overrides.

Parameters:

Name Type Description Default
index SurveyIndex

The survey cache the entries address.

required
overrides Overrides

The loaded correction file.

required

Returns:

Type Description
OverridePlan

The resolved plan.

Raises:

Type Description
OverrideError

If an entry addresses no surveyed level or area and is not a well-formed add (an add needs name, description, and a geometry override supplying its cells), contradicts itself (remove plus replacement fields or a cells entry), replaces nothing, carries a negative coordinate, or two edge keys canonicalize to the same edge.

plan_template_overrides

plan_template_overrides(overrides: Overrides, resolutions: MonsterResolutions) -> dict[str, StatBlockOverride]

Resolve and validate the monster_templates: entries against the monsters cache.

Addressing and contradiction errors surface here, before any stage tracking or artifact write. The cache-state errors (no statblocks.json, an off knob echo) are assembly's — only assembly knows what caches exist — so this function validates what the correction file alone can contradict.

Parameters:

Name Type Description Default
overrides Overrides

The loaded correction file.

required
resolutions MonsterResolutions

The monsters-stage cache (as written — the extracted-name authority the entries address).

required

Returns:

Type Description
dict[str, StatBlockOverride]

Normalized name → its entry, for every monster_templates: entry.

Raises:

Type Description
OverrideError

If two entry keys normalize to the same name, a name also appears under monsters: ("use this catalog id" and "use this custom block" are contradictory corrections), a key matches no extracted name (the error lists the cache's unresolved names — the likeliest targets), or an entry sets no field beyond reason.