Core: MultiData typing (#5071)

This commit is contained in:
Doug Hoskisson
2025-07-26 16:27:29 -07:00
committed by GitHub
parent a36e6259f1
commit c9ebf69e0d
7 changed files with 61 additions and 31 deletions

23
Main.py
View File

@@ -1,9 +1,11 @@
import collections import collections
from collections.abc import Mapping
import concurrent.futures import concurrent.futures
import logging import logging
import os import os
import tempfile import tempfile
import time import time
from typing import Any
import zipfile import zipfile
import zlib import zlib
@@ -239,11 +241,13 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
def write_multidata(): def write_multidata():
import NetUtils import NetUtils
from NetUtils import HintStatus from NetUtils import HintStatus
slot_data = {} slot_data: dict[int, Mapping[str, Any]] = {}
client_versions = {} client_versions: dict[int, tuple[int, int, int]] = {}
games = {} games: dict[int, str] = {}
minimum_versions = {"server": AutoWorld.World.required_server_version, "clients": client_versions} minimum_versions: NetUtils.MinimumVersions = {
slot_info = {} "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())]] names = [[name for player, name in sorted(multiworld.player_name.items())]]
for slot in multiworld.player_ids: for slot in multiworld.player_ids:
player_world: AutoWorld.World = multiworld.worlds[slot] 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"])) group_members=sorted(group["players"]))
precollected_items = {player: [item.code for item in world_precollected if type(item.code) == int] precollected_items = {player: [item.code for item in world_precollected if type(item.code) == int]
for player, world_precollected in multiworld.precollected_items.items()} 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: for slot in multiworld.player_ids:
slot_data[slot] = multiworld.worlds[slot].fill_slot_data() 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: if current_sphere:
spheres.append(dict(current_sphere)) spheres.append(dict(current_sphere))
multidata = { multidata: NetUtils.MultiData | bytes = {
"slot_data": slot_data, "slot_data": slot_data,
"slot_info": slot_info, "slot_info": slot_info,
"connect_names": {name: (0, player) for player, name in multiworld.player_name.items()}, "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, "er_hint_data": er_hint_data,
"precollected_items": precollected_items, "precollected_items": precollected_items,
"precollected_hints": precollected_hints, "precollected_hints": precollected_hints,
"version": tuple(version_tuple), "version": (version_tuple.major, version_tuple.minor, version_tuple.build),
"tags": ["AP"], "tags": ["AP"],
"minimum_versions": minimum_versions, "minimum_versions": minimum_versions,
"seed_name": multiworld.seed_name, "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, "datapackage": data_package,
"race_mode": int(multiworld.is_race), "race_mode": int(multiworld.is_race),
} }
# TODO: change to `"version": version_tuple` after getting better serialization
AutoWorld.call_all(multiworld, "modify_multidata", multidata) AutoWorld.call_all(multiworld, "modify_multidata", multidata)
for key in ("slot_data", "er_hint_data"): for key in ("slot_data", "er_hint_data"):

View File

@@ -43,7 +43,7 @@ import NetUtils
import Utils import Utils
from Utils import version_tuple, restricted_loads, Version, async_start, get_intended_text from Utils import version_tuple, restricted_loads, Version, async_start, get_intended_text
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \ from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
SlotType, LocationStore, Hint, HintStatus SlotType, LocationStore, MultiData, Hint, HintStatus
from BaseClasses import ItemClassification from BaseClasses import ItemClassification
@@ -445,7 +445,7 @@ class Context:
raise Utils.VersionException("Incompatible multidata.") raise Utils.VersionException("Incompatible multidata.")
return restricted_loads(zlib.decompress(data[1:])) 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): use_embedded_server_options: bool):
self.read_data = {} self.read_data = {}

View File

@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping, Sequence
import typing import typing
import enum import enum
import warnings import warnings
@@ -83,7 +84,7 @@ class NetworkSlot(typing.NamedTuple):
name: str name: str
game: str game: str
type: SlotType 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): class NetworkItem(typing.NamedTuple):
@@ -471,6 +472,42 @@ class _LocationStore(dict, typing.MutableMapping[int, typing.Dict[int, typing.Tu
location_id not in checked]) 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 if typing.TYPE_CHECKING: # type-check with pure python implementation until we have a typing stub
LocationStore = _LocationStore LocationStore = _LocationStore
else: else:

View File

@@ -13,9 +13,8 @@ from pony.orm.core import TransactionIntegrityError
import schema import schema
import MultiServer import MultiServer
from NetUtils import SlotType from NetUtils import GamesPackage, SlotType
from Utils import VersionException, __version__ from Utils import VersionException, __version__
from worlds import GamesPackage
from worlds.Files import AutoPatchRegister from worlds.Files import AutoPatchRegister
from worlds.AutoWorld import data_package_checksum from worlds.AutoWorld import data_package_checksum
from . import app from . import app

View File

@@ -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, 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 `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. 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. 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 All instance methods can, optionally, have a class method defined which will be called after all instance methods are

View File

@@ -16,7 +16,7 @@ from Utils import deprecate
if TYPE_CHECKING: if TYPE_CHECKING:
from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance
from . import GamesPackage from NetUtils import GamesPackage, MultiData
from settings import Group from settings import Group
perf_logger = logging.getLogger("performance") perf_logger = logging.getLogger("performance")
@@ -450,7 +450,7 @@ class World(metaclass=AutoWorldRegister):
""" """
pass 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.""" """For deeper modification of server multidata."""
pass pass

View File

@@ -7,8 +7,9 @@ import warnings
import zipimport import zipimport
import time import time
import dataclasses import dataclasses
from typing import Dict, List, TypedDict from typing import List
from NetUtils import DataPackage
from Utils import local_path, user_path from Utils import local_path, user_path
local_folder = os.path.dirname(__file__) local_folder = os.path.dirname(__file__)
@@ -24,8 +25,6 @@ __all__ = {
"world_sources", "world_sources",
"local_folder", "local_folder",
"user_folder", "user_folder",
"GamesPackage",
"DataPackage",
"failed_world_loads", "failed_world_loads",
} }
@@ -33,18 +32,6 @@ __all__ = {
failed_world_loads: List[str] = [] 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) @dataclasses.dataclass(order=True)
class WorldSource: class WorldSource:
path: str # typically relative path from this module path: str # typically relative path from this module