mud/src/mudlib/zones.py
Jared Miller b5c5542792
Add boundary region data model with TOML parsing and export
Boundaries are rectangular regions within zones that can trigger effects
when players enter or exit. Added BoundaryRegion dataclass with contains()
method, TOML parsing in load_zone(), and export support. Tests verify
parsing, export, and round-trip behavior.
2026-02-14 12:39:48 -05:00

179 lines
4.8 KiB
Python

"""Zone registry and loading."""
from __future__ import annotations
import logging
import tomllib
from pathlib import Path
from mudlib.portal import Portal
from mudlib.zone import BoundaryRegion, SpawnRule, Zone
log = logging.getLogger(__name__)
# Module-level zone registry
zone_registry: dict[str, Zone] = {}
def register_zone(name: str, zone: Zone) -> None:
"""Register a zone by name.
Args:
name: Unique name for the zone
zone: Zone instance to register
"""
zone_registry[name] = zone
def get_zone(name: str) -> Zone | None:
"""Look up a zone by name.
Args:
name: Zone name to look up
Returns:
Zone instance if found, None otherwise
"""
return zone_registry.get(name)
def load_zone(path: Path) -> Zone:
"""Load a zone from a TOML file.
Args:
path: Path to TOML file
Returns:
Zone instance loaded from file
"""
with open(path, "rb") as f:
data = tomllib.load(f)
# Extract basic properties
name = data["name"]
description = data.get("description", "")
width = data["width"]
height = data["height"]
toroidal = data.get("toroidal", True)
spawn_x = data.get("spawn_x", 0)
spawn_y = data.get("spawn_y", 0)
safe = data.get("safe", False)
# Parse terrain rows into 2D list
terrain_rows = data.get("terrain", {}).get("rows", [])
terrain = []
for row in terrain_rows:
terrain.append(list(row))
# Parse impassable tiles
impassable_list = data.get("terrain", {}).get("impassable", {}).get("tiles", [])
impassable = set(impassable_list) if impassable_list else {"^", "~"}
# Parse ambient messages
ambient_data = data.get("ambient", {})
ambient_messages = ambient_data.get("messages", [])
ambient_interval = ambient_data.get("interval", 120)
# Parse spawn rules
spawns_data = data.get("spawns", [])
spawn_rules = [
SpawnRule(
mob=spawn["mob"],
max_count=spawn.get("max_count", 1),
respawn_seconds=spawn.get("respawn_seconds", 300),
home_region=spawn.get("home_region"),
)
for spawn in spawns_data
]
# Parse boundaries
boundaries_data = data.get("boundaries", [])
boundaries = [
BoundaryRegion(
name=boundary["name"],
x_min=boundary["x"][0],
x_max=boundary["x"][1],
y_min=boundary["y"][0],
y_max=boundary["y"][1],
on_enter_message=boundary.get("on_enter_message"),
on_exit_message=boundary.get("on_exit_message"),
on_exit_check=boundary.get("on_exit_check"),
on_exit_fail=boundary.get("on_exit_fail"),
)
for boundary in boundaries_data
]
zone = Zone(
name=name,
description=description,
width=width,
height=height,
toroidal=toroidal,
terrain=terrain,
impassable=impassable,
spawn_x=spawn_x,
spawn_y=spawn_y,
ambient_messages=ambient_messages,
ambient_interval=ambient_interval,
spawn_rules=spawn_rules,
safe=safe,
boundaries=boundaries,
)
# Load portals
portals_data = data.get("portals", [])
for portal_dict in portals_data:
# Parse target string "zone_name:x,y"
target = portal_dict["target"]
try:
target_zone, coords = target.split(":")
target_x, target_y = map(int, coords.split(","))
except ValueError:
log.warning(
"skipping portal '%s' at (%d, %d): malformed target '%s'",
portal_dict["label"],
portal_dict["x"],
portal_dict["y"],
target,
)
continue
# Create portal (automatically added to zone._contents via Object.__post_init__)
Portal(
name=portal_dict["label"],
aliases=portal_dict.get("aliases", []),
target_zone=target_zone,
target_x=target_x,
target_y=target_y,
location=zone,
x=portal_dict["x"],
y=portal_dict["y"],
)
return zone
def load_zones(directory: Path) -> dict[str, Zone]:
"""Load all zones from a directory.
Args:
directory: Path to directory containing zone TOML files
Returns:
Dict mapping zone names to Zone instances
"""
zones = {}
if not directory.exists():
log.warning("zones directory does not exist: %s", directory)
return zones
for toml_file in directory.glob("*.toml"):
try:
zone = load_zone(toml_file)
zones[zone.name] = zone
log.debug("loaded zone '%s' from %s", zone.name, toml_file)
except Exception as e:
log.error("failed to load zone from %s: %s", toml_file, e, exc_info=True)
return zones