mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-30 14:53:28 -07:00
163 lines
5.8 KiB
Python
163 lines
5.8 KiB
Python
import dataclasses
|
|
import importlib
|
|
from abc import ABC, abstractmethod
|
|
from collections.abc import Mapping
|
|
from typing import TYPE_CHECKING, Any, ClassVar, Self, TypeVar, cast, overload
|
|
|
|
from typing_extensions import override
|
|
|
|
from Options import Option
|
|
|
|
if TYPE_CHECKING:
|
|
from worlds.AutoWorld import World
|
|
|
|
|
|
class FieldResolverRegister:
|
|
"""A container class to contain world custom resolvers"""
|
|
|
|
custom_resolvers: ClassVar[dict[str, dict[str, type["FieldResolver"]]]] = {}
|
|
"""
|
|
A mapping of game name to mapping of resolver name to resolver class
|
|
to hold custom resolvers implemented by worlds
|
|
"""
|
|
|
|
@classmethod
|
|
def get_resolver_cls(cls, game_name: str, resolver_name: str) -> type["FieldResolver"]:
|
|
"""Returns the world-registered or default resolver with the given name"""
|
|
custom_resolver_classes = cls.custom_resolvers.get(game_name, {})
|
|
if resolver_name not in DEFAULT_RESOLVERS and resolver_name not in custom_resolver_classes:
|
|
raise ValueError(f"Resolver '{resolver_name}' for game '{game_name}' not found")
|
|
return custom_resolver_classes.get(resolver_name) or DEFAULT_RESOLVERS[resolver_name]
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class FieldResolver(ABC):
|
|
@abstractmethod
|
|
def resolve(self, world: "World") -> Any: ...
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
"""Returns a JSON compatible dict representation of this resolver"""
|
|
fields = {field.name: getattr(self, field.name, None) for field in dataclasses.fields(self)}
|
|
return {
|
|
"resolver": self.__class__.__name__,
|
|
**fields,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
"""Returns a new instance of this resolver from a serialized dict representation"""
|
|
assert data.get("resolver", None) == cls.__name__
|
|
return cls(**{k: v for k, v in data.items() if k != "resolver"})
|
|
|
|
@override
|
|
def __str__(self) -> str:
|
|
return self.__class__.__name__
|
|
|
|
@classmethod
|
|
def __init_subclass__(cls, /, game: str) -> None:
|
|
if game != "Archipelago":
|
|
custom_resolvers = FieldResolverRegister.custom_resolvers.setdefault(game, {})
|
|
if cls.__qualname__ in custom_resolvers:
|
|
raise TypeError(f"Resolver {cls.__qualname__} has already been registered for game {game}")
|
|
custom_resolvers[cls.__qualname__] = cls
|
|
elif cls.__module__ != "rule_builder.field_resolvers":
|
|
raise TypeError("You cannot define custom resolvers for the base Archipelago world")
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class FromOption(FieldResolver, game="Archipelago"):
|
|
option: type[Option[Any]]
|
|
field: str = "value"
|
|
|
|
@override
|
|
def resolve(self, world: "World") -> Any:
|
|
option_name = next(
|
|
(name for name, cls in world.options.__class__.type_hints.items() if cls is self.option),
|
|
None,
|
|
)
|
|
|
|
if option_name is None:
|
|
raise ValueError(
|
|
f"Cannot find option {self.option.__name__} in options class {world.options.__class__.__name__}"
|
|
)
|
|
opt = cast(Option[Any] | None, getattr(world.options, option_name, None))
|
|
if opt is None:
|
|
raise ValueError(f"Invalid option: {option_name}")
|
|
return getattr(opt, self.field)
|
|
|
|
@override
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"resolver": "FromOption",
|
|
"option": f"{self.option.__module__}.{self.option.__name__}",
|
|
"field": self.field,
|
|
}
|
|
|
|
@override
|
|
@classmethod
|
|
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
if "option" not in data:
|
|
raise ValueError("Missing required option")
|
|
|
|
option_path = data["option"]
|
|
try:
|
|
option_mod_name, option_cls_name = option_path.rsplit(".", 1)
|
|
option_module = importlib.import_module(option_mod_name)
|
|
option = getattr(option_module, option_cls_name, None)
|
|
except (ValueError, ImportError) as e:
|
|
raise ValueError(f"Cannot parse option '{option_path}'") from e
|
|
if option is None or not issubclass(option, Option):
|
|
raise ValueError(f"Invalid option '{option_path}' returns type '{option}' instead of Option subclass")
|
|
|
|
return cls(cast(type[Option[Any]], option), data.get("field", "value"))
|
|
|
|
@override
|
|
def __str__(self) -> str:
|
|
field = f".{self.field}" if self.field != "value" else ""
|
|
return f"FromOption({self.option.__name__}{field})"
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class FromWorldAttr(FieldResolver, game="Archipelago"):
|
|
name: str
|
|
|
|
@override
|
|
def resolve(self, world: "World") -> Any:
|
|
obj: Any = world
|
|
for field in self.name.split("."):
|
|
if obj is None:
|
|
return None
|
|
if isinstance(obj, Mapping):
|
|
obj = obj.get(field, None) # pyright: ignore[reportUnknownMemberType]
|
|
else:
|
|
obj = getattr(obj, field, None)
|
|
return obj
|
|
|
|
@override
|
|
def __str__(self) -> str:
|
|
return f"FromWorldAttr({self.name})"
|
|
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
@overload
|
|
def resolve_field(field: Any, world: "World", expected_type: type[T]) -> T: ...
|
|
@overload
|
|
def resolve_field(field: Any, world: "World", expected_type: None = None) -> Any: ...
|
|
def resolve_field(field: Any, world: "World", expected_type: type[T] | None = None) -> T | Any:
|
|
if isinstance(field, FieldResolver):
|
|
field = field.resolve(world)
|
|
if expected_type:
|
|
assert isinstance(field, expected_type), f"Expected type {expected_type} but got {type(field)}"
|
|
return field
|
|
|
|
|
|
DEFAULT_RESOLVERS = {
|
|
resolver_name: resolver_class
|
|
for resolver_name, resolver_class in locals().items()
|
|
if isinstance(resolver_class, type)
|
|
and issubclass(resolver_class, FieldResolver)
|
|
and resolver_class is not FieldResolver
|
|
}
|