diff --git a/Main.py b/Main.py index fd7d8d9a..67c861c0 100644 --- a/Main.py +++ b/Main.py @@ -1,9 +1,11 @@ import collections +from collections.abc import Mapping import concurrent.futures import logging import os import tempfile import time +from typing import Any import zipfile import zlib @@ -239,11 +241,13 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None) def write_multidata(): import NetUtils from NetUtils import HintStatus - slot_data = {} - client_versions = {} - games = {} - minimum_versions = {"server": AutoWorld.World.required_server_version, "clients": client_versions} - slot_info = {} + slot_data: dict[int, Mapping[str, Any]] = {} + client_versions: dict[int, tuple[int, int, int]] = {} + games: dict[int, str] = {} + minimum_versions: NetUtils.MinimumVersions = { + "server": AutoWorld.World.required_server_version, "clients": client_versions + } + slot_info: dict[int, NetUtils.NetworkSlot] = {} names = [[name for player, name in sorted(multiworld.player_name.items())]] for slot in multiworld.player_ids: player_world: AutoWorld.World = multiworld.worlds[slot] @@ -258,7 +262,9 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None) group_members=sorted(group["players"])) precollected_items = {player: [item.code for item in world_precollected if type(item.code) == int] for player, world_precollected in multiworld.precollected_items.items()} - precollected_hints = {player: set() for player in range(1, multiworld.players + 1 + len(multiworld.groups))} + precollected_hints: dict[int, set[NetUtils.Hint]] = { + player: set() for player in range(1, multiworld.players + 1 + len(multiworld.groups)) + } for slot in multiworld.player_ids: slot_data[slot] = multiworld.worlds[slot].fill_slot_data() @@ -315,7 +321,7 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None) if current_sphere: spheres.append(dict(current_sphere)) - multidata = { + multidata: NetUtils.MultiData | bytes = { "slot_data": slot_data, "slot_info": slot_info, "connect_names": {name: (0, player) for player, name in multiworld.player_name.items()}, @@ -325,7 +331,7 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None) "er_hint_data": er_hint_data, "precollected_items": precollected_items, "precollected_hints": precollected_hints, - "version": tuple(version_tuple), + "version": (version_tuple.major, version_tuple.minor, version_tuple.build), "tags": ["AP"], "minimum_versions": minimum_versions, "seed_name": multiworld.seed_name, @@ -333,6 +339,7 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None) "datapackage": data_package, "race_mode": int(multiworld.is_race), } + # TODO: change to `"version": version_tuple` after getting better serialization AutoWorld.call_all(multiworld, "modify_multidata", multidata) for key in ("slot_data", "er_hint_data"): diff --git a/MultiServer.py b/MultiServer.py index 59960a2a..11a9e394 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -43,7 +43,7 @@ import NetUtils import Utils from Utils import version_tuple, restricted_loads, Version, async_start, get_intended_text from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \ - SlotType, LocationStore, Hint, HintStatus + SlotType, LocationStore, MultiData, Hint, HintStatus from BaseClasses import ItemClassification @@ -445,7 +445,7 @@ class Context: raise Utils.VersionException("Incompatible multidata.") return restricted_loads(zlib.decompress(data[1:])) - def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.Any], + def _load(self, decoded_obj: MultiData, game_data_packages: typing.Dict[str, typing.Any], use_embedded_server_options: bool): self.read_data = {} diff --git a/NetUtils.py b/NetUtils.py index cc6e917c..45279183 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Mapping, Sequence import typing import enum import warnings @@ -83,7 +84,7 @@ class NetworkSlot(typing.NamedTuple): name: str game: str type: SlotType - group_members: typing.Union[typing.List[int], typing.Tuple] = () # only populated if type == group + group_members: Sequence[int] = () # only populated if type == group class NetworkItem(typing.NamedTuple): @@ -471,6 +472,42 @@ class _LocationStore(dict, typing.MutableMapping[int, typing.Dict[int, typing.Tu location_id not in checked]) +class MinimumVersions(typing.TypedDict): + server: tuple[int, int, int] + clients: dict[int, tuple[int, int, int]] + + +class GamesPackage(typing.TypedDict, total=False): + item_name_groups: dict[str, list[str]] + item_name_to_id: dict[str, int] + location_name_groups: dict[str, list[str]] + location_name_to_id: dict[str, int] + checksum: str + + +class DataPackage(typing.TypedDict): + games: dict[str, GamesPackage] + + +class MultiData(typing.TypedDict): + slot_data: dict[int, Mapping[str, typing.Any]] + slot_info: dict[int, NetworkSlot] + connect_names: dict[str, tuple[int, int]] + locations: dict[int, dict[int, tuple[int, int, int]]] + checks_in_area: dict[int, dict[str, int | list[int]]] + server_options: dict[str, object] + er_hint_data: dict[int, dict[int, str]] + precollected_items: dict[int, list[int]] + precollected_hints: dict[int, set[Hint]] + version: tuple[int, int, int] + tags: list[str] + minimum_versions: MinimumVersions + seed_name: str + spheres: list[dict[int, set[int]]] + datapackage: dict[str, GamesPackage] + race_mode: int + + if typing.TYPE_CHECKING: # type-check with pure python implementation until we have a typing stub LocationStore = _LocationStore else: diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index 4c5d411d..48885e9c 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -13,9 +13,8 @@ from pony.orm.core import TransactionIntegrityError import schema import MultiServer -from NetUtils import SlotType +from NetUtils import GamesPackage, SlotType from Utils import VersionException, __version__ -from worlds import GamesPackage from worlds.Files import AutoPatchRegister from worlds.AutoWorld import data_package_checksum from . import app diff --git a/docs/world api.md b/docs/world api.md index 3bf48219..17cf81fe 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -539,7 +539,7 @@ In addition, the following methods can be implemented and are called in this ord creates the output files if there is output to be generated. When this is called, `self.multiworld.get_locations(self.player)` has all locations for the player, with attribute `item` pointing to the item. `location.item.player` can be used to see if it's a local item. -* `fill_slot_data(self)` and `modify_multidata(self, multidata: Dict[str, Any])` can be used to modify the data that +* `fill_slot_data(self)` and `modify_multidata(self, multidata: MultiData)` can be used to modify the data that will be used by the server to host the MultiWorld. All instance methods can, optionally, have a class method defined which will be called after all instance methods are diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 6c1683e3..568bdcf9 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -16,7 +16,7 @@ from Utils import deprecate if TYPE_CHECKING: from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance - from . import GamesPackage + from NetUtils import GamesPackage, MultiData from settings import Group perf_logger = logging.getLogger("performance") @@ -450,7 +450,7 @@ class World(metaclass=AutoWorldRegister): """ pass - def modify_multidata(self, multidata: Dict[str, Any]) -> None: # TODO: TypedDict for multidata? + def modify_multidata(self, multidata: "MultiData") -> None: """For deeper modification of server multidata.""" pass diff --git a/worlds/__init__.py b/worlds/__init__.py index 7db651bd..80240275 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -7,8 +7,9 @@ import warnings import zipimport import time import dataclasses -from typing import Dict, List, TypedDict +from typing import List +from NetUtils import DataPackage from Utils import local_path, user_path local_folder = os.path.dirname(__file__) @@ -24,8 +25,6 @@ __all__ = { "world_sources", "local_folder", "user_folder", - "GamesPackage", - "DataPackage", "failed_world_loads", } @@ -33,18 +32,6 @@ __all__ = { failed_world_loads: List[str] = [] -class GamesPackage(TypedDict, total=False): - item_name_groups: Dict[str, List[str]] - item_name_to_id: Dict[str, int] - location_name_groups: Dict[str, List[str]] - location_name_to_id: Dict[str, int] - checksum: str - - -class DataPackage(TypedDict): - games: Dict[str, GamesPackage] - - @dataclasses.dataclass(order=True) class WorldSource: path: str # typically relative path from this module