Source code for pipeworks_mud_mapper.services.world_service

"""World metadata helpers for cross-zone navigation.

The mapper needs a lightweight way to discover which zones exist and what
rooms they expose so we can build cross-zone exit pickers. The MUD server
stores this information in ``world.json`` alongside the exported zone files.

This module provides best-effort readers that:

- Prefer ``world.json`` when present (authoritative list of zones)
- Fall back to enumerating zone files if the world file is missing
- Gracefully handle missing or malformed data
"""

from __future__ import annotations

import json
from pathlib import Path

from pipeworks_mud_mapper.services import zone_service
from pipeworks_mud_mapper.services.app_config import get_path_settings


def _resolve_zones_dir(zones_dir: Path | None) -> Path:
    """Resolve the zones directory using config defaults when needed."""
    if zones_dir is not None:
        return zones_dir
    return get_path_settings()["zones_dir"]


def _resolve_world_path(world_path: Path | None = None, zones_dir: Path | None = None) -> Path:
    """Derive the ``world.json`` path from config or the zones directory."""
    # Prefer explicit overrides, then zones_dir parent, then config default.
    if world_path is not None:
        return world_path
    if zones_dir is not None:
        resolved_zones = _resolve_zones_dir(zones_dir)
        return resolved_zones.parent / "world.json"
    return get_path_settings()["world_json_path"]


[docs] def load_world_json(world_path: Path | None = None, zones_dir: Path | None = None) -> dict | None: """Load the world.json payload as a dictionary when available.""" resolved_path = _resolve_world_path(world_path, zones_dir) if not resolved_path.exists(): return None try: data = json.loads(resolved_path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return None return data if isinstance(data, dict) else None
[docs] def load_world_zone_ids(zones_dir: Path | None = None, world_path: Path | None = None) -> list[str]: """Return the list of zone IDs known to the world. Parameters ---------- zones_dir : Path | None Optional override for the zones directory. When omitted, the path from ``config/server.ini`` (or defaults) is used. world_path : Path | None Optional override for the world.json path. Defaults to the configured world_json_path, or to the parent of zones_dir when provided. Returns ------- list[str] Sorted list of zone IDs. Returns an empty list if no zones are found. """ resolved_zones = _resolve_zones_dir(zones_dir) world_path = _resolve_world_path( world_path, resolved_zones if zones_dir is not None else None, ) zone_ids: list[str] = [] if world_path.exists(): try: data = json.loads(world_path.read_text(encoding="utf-8")) raw_zones = data.get("zones", []) if isinstance(raw_zones, list): zone_ids = [z for z in raw_zones if isinstance(z, str) and z] except (OSError, json.JSONDecodeError): zone_ids = [] if not zone_ids: zone_ids = [path.stem for path in zone_service.list_zone_files(resolved_zones)] return sorted(set(zone_ids))
[docs] def load_zone_room_ids(zone_id: str, zones_dir: Path | None = None) -> list[str]: """Return room IDs for a specific zone export. Parameters ---------- zone_id : str Zone ID to read from ``<zones_dir>/<zone_id>.json``. zones_dir : Path | None Optional override for the zones directory. When omitted, the path from ``config/server.ini`` (or defaults) is used. Returns ------- list[str] Sorted list of room IDs for the zone. Empty if the zone file is missing or invalid. """ if not zone_id: return [] resolved_zones = _resolve_zones_dir(zones_dir) zone_path = resolved_zones / f"{zone_id}.json" if not zone_path.exists(): return [] try: data = json.loads(zone_path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return [] rooms = data.get("rooms", {}) if not isinstance(rooms, dict): return [] return sorted([room_id for room_id in rooms.keys() if isinstance(room_id, str)])