Deprecate data_version and introduce checksum for DataPackages. (#684)

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
This commit is contained in:
Zach Parks
2023-03-20 11:01:08 -05:00
committed by GitHub
parent d825576f12
commit ff9f563d4a
20 changed files with 297 additions and 119 deletions

View File

@@ -1,20 +1,24 @@
from __future__ import annotations
import hashlib
import logging
import sys
import pathlib
from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Type, Union, TYPE_CHECKING, \
ClassVar
import sys
from typing import Any, Callable, ClassVar, Dict, FrozenSet, List, Optional, Set, TYPE_CHECKING, TextIO, Tuple, Type, \
Union
from Options import AssembleOptions
from BaseClasses import CollectionState
from Options import AssembleOptions
if TYPE_CHECKING:
from BaseClasses import MultiWorld, Item, Location, Tutorial
from . import GamesPackage
class AutoWorldRegister(type):
world_types: Dict[str, Type[World]] = {}
__file__: str
zip_path: Optional[str]
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoWorldRegister:
if "web" in dct:
@@ -154,9 +158,14 @@ class World(metaclass=AutoWorldRegister):
data_version: ClassVar[int] = 1
"""
increment this every time something in your world's names/id mappings changes.
While this is set to 0, this world's DataPackage is considered in testing mode and will be inserted to the multidata
and retrieved by clients on every connection.
Increment this every time something in your world's names/id mappings changes.
When this is set to 0, that world's DataPackage is considered in "testing mode", which signals to servers/clients
that it should not be cached, and clients should request that world's DataPackage every connection. Not
recommended for production-ready worlds.
Deprecated. Clients should utilize `checksum` to determine if DataPackage has changed since last connection and
request a new DataPackage, if necessary.
"""
required_client_version: Tuple[int, int, int] = (0, 1, 6)
@@ -343,8 +352,35 @@ class World(metaclass=AutoWorldRegister):
def create_filler(self) -> "Item":
return self.create_item(self.get_filler_item_name())
@classmethod
def get_data_package_data(cls) -> "GamesPackage":
sorted_item_name_groups = {
name: sorted(cls.item_name_groups[name]) for name in sorted(cls.item_name_groups)
}
sorted_location_name_groups = {
name: sorted(cls.location_name_groups[name]) for name in sorted(cls.location_name_groups)
}
res: "GamesPackage" = {
# sorted alphabetically
"item_name_groups": sorted_item_name_groups,
"item_name_to_id": cls.item_name_to_id,
"location_name_groups": sorted_location_name_groups,
"location_name_to_id": cls.location_name_to_id,
"version": cls.data_version,
}
res["checksum"] = data_package_checksum(res)
return res
# any methods attached to this can be used as part of CollectionState,
# please use a prefix as all of them get clobbered together
class LogicMixin(metaclass=AutoLogicRegister):
pass
def data_package_checksum(data: "GamesPackage") -> str:
"""Calculates the data package checksum for a game from a dict"""
assert "checksum" not in data, "Checksum already in data"
assert sorted(data) == list(data), "Data not ordered"
from NetUtils import encode
return hashlib.sha1(encode(data).encode()).hexdigest()

View File

@@ -20,14 +20,19 @@ if typing.TYPE_CHECKING:
from .AutoWorld import World
class GamesPackage(typing.TypedDict):
class GamesData(typing.TypedDict):
item_name_groups: typing.Dict[str, typing.List[str]]
item_name_to_id: typing.Dict[str, int]
location_name_groups: typing.Dict[str, typing.List[str]]
location_name_to_id: typing.Dict[str, int]
version: int
class GamesPackage(GamesData, total=False):
checksum: str
class DataPackage(typing.TypedDict):
version: int
games: typing.Dict[str, GamesPackage]
@@ -75,14 +80,9 @@ games: typing.Dict[str, GamesPackage] = {}
from .AutoWorld import AutoWorldRegister
# Build the data package for each game.
for world_name, world in AutoWorldRegister.world_types.items():
games[world_name] = {
"item_name_to_id": world.item_name_to_id,
"location_name_to_id": world.location_name_to_id,
"version": world.data_version,
# seems clients don't actually want this. Keeping it here in case someone changes their mind.
# "item_name_groups": {name: tuple(items) for name, items in world.item_name_groups.items()}
}
games[world_name] = world.get_data_package_data()
lookup_any_item_id_to_name.update(world.item_id_to_name)
lookup_any_location_id_to_name.update(world.location_id_to_name)

View File

@@ -1,7 +1,8 @@
from BaseClasses import Tutorial
from ..AutoWorld import World, WebWorld
from typing import Dict
from BaseClasses import Tutorial
from ..AutoWorld import WebWorld, World
class Bk_SudokuWebWorld(WebWorld):
settings_page = "games/Sudoku/info/en"
@@ -24,6 +25,7 @@ class Bk_SudokuWorld(World):
"""
game = "Sudoku"
web = Bk_SudokuWebWorld()
data_version = 1
item_name_to_id: Dict[str, int] = {}
location_name_to_id: Dict[str, int] = {}

View File

@@ -42,6 +42,7 @@ class GenericWorld(World):
}
hidden = True
web = GenericWeb()
data_version = 1
def generate_early(self):
self.multiworld.player_types[self.player] = SlotType.spectator # mark as spectator

View File

@@ -2,8 +2,8 @@ import random
from typing import Dict, Any
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification
from worlds.generic.Rules import set_rule
from ..AutoWorld import World, WebWorld
from . import Items, Locations, Options, Rules, Exits
from . import Exits, Items, Locations, Options, Rules
from ..AutoWorld import WebWorld, World
class Hylics2Web(WebWorld):
@@ -20,13 +20,13 @@ class Hylics2Web(WebWorld):
class Hylics2World(World):
"""
Hylics 2 is a surreal and unusual RPG, with a bizarre yet unique visual style. Play as Wayne,
Hylics 2 is a surreal and unusual RPG, with a bizarre yet unique visual style. Play as Wayne,
travel the world, and gather your allies to defeat the nefarious Gibby in his Hylemxylem!
"""
game: str = "Hylics 2"
web = Hylics2Web()
all_items = {**Items.item_table, **Items.gesture_item_table, **Items.party_item_table,
all_items = {**Items.item_table, **Items.gesture_item_table, **Items.party_item_table,
**Items.medallion_item_table}
all_locations = {**Locations.location_table, **Locations.tv_location_table, **Locations.party_location_table,
**Locations.medallion_location_table}
@@ -37,7 +37,7 @@ class Hylics2World(World):
topology_present: bool = True
data_version: 1
data_version = 1
start_location = "Waynehouse"
@@ -59,7 +59,7 @@ class Hylics2World(World):
def create_event(self, event: str):
return Hylics2Item(event, ItemClassification.progression_skip_balancing, None, self.player)
# set random starting location if option is enabled
def generate_early(self):
if self.multiworld.random_start[self.player]:
@@ -76,7 +76,7 @@ class Hylics2World(World):
def generate_basic(self):
# create item pool
pool = []
# add regular items
for i, data in Items.item_table.items():
if data["count"] > 0:
@@ -114,7 +114,7 @@ class Hylics2World(World):
gestures = list(Items.gesture_item_table.items())
tvs = list(Locations.tv_location_table.items())
# if Extra Items in Logic is enabled place CHARGE UP first and make sure it doesn't get
# if Extra Items in Logic is enabled place CHARGE UP first and make sure it doesn't get
# placed at Sage Airship: TV
if self.multiworld.extra_items_in_logic[self.player]:
tv = self.multiworld.random.choice(tvs)
@@ -122,7 +122,7 @@ class Hylics2World(World):
while tv[1]["name"] == "Sage Airship: TV":
tv = self.multiworld.random.choice(tvs)
self.multiworld.get_location(tv[1]["name"], self.player)\
.place_locked_item(self.add_item(gestures[gest][1]["name"], gestures[gest][1]["classification"],
.place_locked_item(self.add_item(gestures[gest][1]["name"], gestures[gest][1]["classification"],
gestures[gest]))
gestures.remove(gestures[gest])
tvs.remove(tv)
@@ -182,7 +182,7 @@ class Hylics2World(World):
16: Region("Sage Airship", self.player, self.multiworld),
17: Region("Hylemxylem", self.player, self.multiworld)
}
# create regions from table
for i, reg in region_table.items():
self.multiworld.regions.append(reg)
@@ -214,7 +214,7 @@ class Hylics2World(World):
for i, data in Locations.tv_location_table.items():
region_table[data["region"]].locations\
.append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]]))
# add party member locations if option is enabled
if self.multiworld.party_shuffle[self.player]:
for i, data in Locations.party_location_table.items():
@@ -241,4 +241,4 @@ class Hylics2Location(Location):
class Hylics2Item(Item):
game: str = "Hylics 2"
game: str = "Hylics 2"

View File

@@ -13,6 +13,7 @@ class OriBlindForest(World):
game: str = "Ori and the Blind Forest"
topology_present = True
data_version = 1
item_name_to_id = item_table
location_name_to_id = lookup_name_to_id