from__future__importannotationsimportloggingfrompathlibimportPathfromtypingimportAny,ItemsViewfromtangl.loadersimportUniqueLabel,WorldBundle,WorldCompilerfromtangl.story.fabulaimportWorldfromtangl.utils.sanitize_strimportsanitize_strlogger=logging.getLogger(__name__)_MANUAL_WORLDS:dict[str,World]={}def_get_world_dirs()->list[Path]:returnlist(get_world_dirs())defget_world_dirs()->list[Path]:"""Compatibility helper retained for existing tests and monkeypatch hooks."""fromtangl.configimportget_world_dirsas_config_get_world_dirsreturnlist(_config_get_world_dirs())deflegacy_world_label(script_data:dict[str,Any])->str|None:"""Derive a stable legacy label from raw script payload."""metadata=script_data.get("metadata")ifisinstance(metadata,dict):title=metadata.get("title")ifisinstance(title,str)andtitle.strip():returnsanitize_str(title).lower()raw_label=script_data.get("label")ifisinstance(raw_label,str)andraw_label.strip():returnsanitize_str(raw_label).lower()returnNonedefregister_manual_world(world:World)->None:"""Register one process-local manual world."""_MANUAL_WORLDS[world.label]=worlddefpop_manual_world(world_id:str)->World|None:"""Remove one process-local manual world by label."""return_MANUAL_WORLDS.pop(world_id,None)defiter_manual_worlds()->ItemsView[str,World]:"""Return the process-local manual worlds."""return_MANUAL_WORLDS.items()defclear_manual_worlds()->None:"""Clear process-local manual worlds."""_MANUAL_WORLDS.clear()defresolve_world(world_id:str)->World:"""Resolve a world from manual overrides or configured registries."""ifworld_idin_MANUAL_WORLDS:return_MANUAL_WORLDS[world_id]registry=WorldRegistry()world=registry.get_world(world_id)ifnotisinstance(world,World):raiseTypeError(f"Expected Story world for '{world_id}', got {type(world)!r}")returnworld
[docs]classWorldRegistry:"""Discover and lazily compile worlds from configured directories."""def__init__(self,world_dirs:list[Path]|None=None,compiler:WorldCompiler|None=None)->None:self.compiler=compilerorWorldCompiler()self.bundles:dict[UniqueLabel,WorldBundle]={}self.worlds:dict[UniqueLabel,World]={}ifworld_dirsisNone:world_dirs=_get_world_dirs()self._discover(world_dirs)def_discover(self,world_dirs:list[Path])->None:forworld_dirinworld_dirs:ifnotworld_dir.exists():logger.warning("World directory %s does not exist",world_dir)continueforiteminworld_dir.iterdir():ifnotitem.is_dir():continuemanifest_path=item/"world.yaml"ifnotmanifest_path.exists():continuetry:bundle=WorldBundle.load(item)exceptExceptionasexc:# pragma: no cover - defensive logginglogger.error("Failed to load %s: %s",item,exc)continueself.bundles[bundle.manifest.label]=bundlelogger.info("Discovered world: %s",bundle.manifest.label)deflist_worlds(self)->list[dict]:return[{"label":bundle.manifest.label,"metadata":bundle.manifest.metadata,"is_anthology":bundle.manifest.is_anthology,}forbundleinself.bundles.values()]defget_world(self,label:UniqueLabel)->World:iflabelnotinself.worlds:bundle=self.bundles.get(label)ifnotbundle:msg=f"Unknown world: {label}"raiseValueError(msg)self.worlds[label]=self.compiler.compile(bundle)returnself.worlds[label]defget_anthology(self,label:UniqueLabel,)->dict[str,World]:bundle=self.bundles.get(label)ifnotbundle:msg=f"Unknown world: {label}"raiseValueError(msg)ifnotbundle.manifest.is_anthology:msg=f"World {label} is not an anthology"raiseValueError(msg)returnself.compiler.compile_anthology(bundle)