From 5aa6ad63ca9da9aa8ea40831522e1d89726c6ce0 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Sat, 1 Jun 2024 06:07:13 -0500 Subject: [PATCH] Core: Remove Universally Unique ID Requirements (Per-Game Data Packages) (#1933) --- AdventureClient.py | 2 +- CommonClient.py | 91 ++++++++++++-- MultiServer.py | 50 ++++---- NetUtils.py | 6 +- UndertaleClient.py | 4 +- Utils.py | 5 +- WargrooveClient.py | 6 +- WebHostLib/api/__init__.py | 9 -- WebHostLib/templates/ootTracker.html | 180 --------------------------- Zelda1Client.py | 4 +- docs/network protocol.md | 58 ++++++--- kvui.py | 14 ++- test/general/test_ids.py | 16 --- test/programs/test_common_client.py | 106 ++++++++++++++++ worlds/AutoWorld.py | 13 -- worlds/__init__.py | 1 - worlds/adventure/__init__.py | 1 - worlds/alttp/Client.py | 6 +- worlds/alttp/__init__.py | 1 - worlds/bk_sudoku/__init__.py | 1 - worlds/blasphemous/__init__.py | 1 - worlds/bumpstik/__init__.py | 2 - worlds/checksfinder/__init__.py | 2 - worlds/clique/__init__.py | 1 - worlds/cv64/__init__.py | 1 - worlds/cv64/client.py | 2 +- worlds/dark_souls_3/__init__.py | 1 - worlds/dkc3/Client.py | 6 +- worlds/dkc3/__init__.py | 1 - worlds/dlcquest/__init__.py | 2 - worlds/doom_1993/__init__.py | 1 - worlds/doom_ii/__init__.py | 1 - worlds/factorio/Client.py | 4 +- worlds/factorio/__init__.py | 1 - worlds/ff1/__init__.py | 1 - worlds/ffmq/__init__.py | 3 - worlds/generic/__init__.py | 1 - worlds/heretic/__init__.py | 1 - worlds/hk/__init__.py | 1 - worlds/hylics2/__init__.py | 2 - worlds/kdl3/Client.py | 6 +- worlds/ladx/__init__.py | 5 - worlds/lingo/__init__.py | 1 - worlds/lufia2ac/Client.py | 4 +- worlds/lufia2ac/__init__.py | 1 - worlds/meritous/__init__.py | 2 - worlds/minecraft/__init__.py | 2 - worlds/mmbn3/__init__.py | 2 - worlds/noita/__init__.py | 3 +- worlds/oot/__init__.py | 2 - worlds/overcooked2/__init__.py | 1 - worlds/pokemon_emerald/__init__.py | 1 - worlds/pokemon_rb/__init__.py | 1 - worlds/raft/__init__.py | 1 - worlds/rogue_legacy/__init__.py | 1 - worlds/ror2/__init__.py | 1 - worlds/sa2b/__init__.py | 1 - worlds/sc2/Client.py | 6 +- worlds/sc2/ClientGui.py | 2 +- worlds/shorthike/__init__.py | 1 - worlds/sm/Client.py | 7 +- worlds/sm/__init__.py | 1 - worlds/sm64ex/__init__.py | 1 - worlds/smw/Client.py | 13 +- worlds/smz3/Client.py | 7 +- worlds/smz3/__init__.py | 1 - worlds/soe/__init__.py | 1 - worlds/spire/__init__.py | 1 - worlds/stardew_valley/__init__.py | 1 - worlds/subnautica/__init__.py | 1 - worlds/terraria/__init__.py | 5 - worlds/timespinner/__init__.py | 3 +- worlds/tloz/__init__.py | 1 - worlds/undertale/__init__.py | 2 - worlds/v6/__init__.py | 2 - worlds/yoshisisland/Client.py | 6 +- worlds/zillion/__init__.py | 5 - 77 files changed, 319 insertions(+), 392 deletions(-) delete mode 100644 WebHostLib/templates/ootTracker.html create mode 100644 test/programs/test_common_client.py diff --git a/AdventureClient.py b/AdventureClient.py index 06e4d60dad43..7bfbd5ef6bd3 100644 --- a/AdventureClient.py +++ b/AdventureClient.py @@ -112,7 +112,7 @@ def on_package(self, cmd: str, args: dict): if ': !' not in msg: self._set_message(msg, SYSTEM_MESSAGE_ID) elif cmd == "ReceivedItems": - msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}" + msg = f"Received {', '.join([self.item_names.lookup_in_slot(item.item) for item in args['items']])}" self._set_message(msg, SYSTEM_MESSAGE_ID) elif cmd == "Retrieved": if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]: diff --git a/CommonClient.py b/CommonClient.py index 63cac098e22a..8af822cba571 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -1,5 +1,6 @@ from __future__ import annotations +import collections import copy import logging import asyncio @@ -8,6 +9,7 @@ import typing import time import functools +import warnings import ModuleUpdate ModuleUpdate.update() @@ -173,10 +175,74 @@ class CommonContext: items_handling: typing.Optional[int] = None want_slot_data: bool = True # should slot_data be retrieved via Connect - # data package - # Contents in flux until connection to server is made, to download correct data for this multiworld. - item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})') - location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})') + class NameLookupDict: + """A specialized dict, with helper methods, for id -> name item/location data package lookups by game.""" + def __init__(self, ctx: CommonContext, lookup_type: typing.Literal["item", "location"]): + self.ctx: CommonContext = ctx + self.lookup_type: typing.Literal["item", "location"] = lookup_type + self._unknown_item: typing.Callable[[int], str] = lambda key: f"Unknown {lookup_type} (ID: {key})" + self._archipelago_lookup: typing.Dict[int, str] = {} + self._flat_store: typing.Dict[int, str] = Utils.KeyedDefaultDict(self._unknown_item) + self._game_store: typing.Dict[str, typing.ChainMap[int, str]] = collections.defaultdict( + lambda: collections.ChainMap(self._archipelago_lookup, Utils.KeyedDefaultDict(self._unknown_item))) + self.warned: bool = False + + # noinspection PyTypeChecker + def __getitem__(self, key: str) -> typing.Mapping[int, str]: + # TODO: In a future version (0.6.0?) this should be simplified by removing implicit id lookups support. + if isinstance(key, int): + if not self.warned: + # Use warnings instead of logger to avoid deprecation message from appearing on user side. + self.warned = True + warnings.warn(f"Implicit name lookup by id only is deprecated and only supported to maintain " + f"backwards compatibility for now. If multiple games share the same id for a " + f"{self.lookup_type}, name could be incorrect. Please use " + f"`{self.lookup_type}_names.lookup_in_game()` or " + f"`{self.lookup_type}_names.lookup_in_slot()` instead.") + return self._flat_store[key] # type: ignore + + return self._game_store[key] + + def __len__(self) -> int: + return len(self._game_store) + + def __iter__(self) -> typing.Iterator[str]: + return iter(self._game_store) + + def __repr__(self) -> str: + return self._game_store.__repr__() + + def lookup_in_game(self, code: int, game_name: typing.Optional[str] = None) -> str: + """Returns the name for an item/location id in the context of a specific game or own game if `game` is + omitted. + """ + if game_name is None: + game_name = self.ctx.game + assert game_name is not None, f"Attempted to lookup {self.lookup_type} with no game name available." + + return self._game_store[game_name][code] + + def lookup_in_slot(self, code: int, slot: typing.Optional[int] = None) -> str: + """Returns the name for an item/location id in the context of a specific slot or own slot if `slot` is + omitted. + """ + if slot is None: + slot = self.ctx.slot + assert slot is not None, f"Attempted to lookup {self.lookup_type} with no slot info available." + + return self.lookup_in_game(code, self.ctx.slot_info[slot].game) + + def update_game(self, game: str, name_to_id_lookup_table: typing.Dict[str, int]) -> None: + """Overrides existing lookup tables for a particular game.""" + id_to_name_lookup_table = Utils.KeyedDefaultDict(self._unknown_item) + id_to_name_lookup_table.update({code: name for name, code in name_to_id_lookup_table.items()}) + self._game_store[game] = collections.ChainMap(self._archipelago_lookup, id_to_name_lookup_table) + self._flat_store.update(id_to_name_lookup_table) # Only needed for legacy lookup method. + if game == "Archipelago": + # Keep track of the Archipelago data package separately so if it gets updated in a custom datapackage, + # it updates in all chain maps automatically. + self._archipelago_lookup.clear() + self._archipelago_lookup.update(id_to_name_lookup_table) # defaults starting_reconnect_delay: int = 5 @@ -231,7 +297,7 @@ class CommonContext: # message box reporting a loss of connection _messagebox_connection_loss: typing.Optional["kvui.MessageBox"] = None - def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None: + def __init__(self, server_address: typing.Optional[str] = None, password: typing.Optional[str] = None) -> None: # server state self.server_address = server_address self.username = None @@ -271,6 +337,9 @@ def __init__(self, server_address: typing.Optional[str], password: typing.Option self.exit_event = asyncio.Event() self.watcher_event = asyncio.Event() + self.item_names = self.NameLookupDict(self, "item") + self.location_names = self.NameLookupDict(self, "location") + self.jsontotextparser = JSONtoTextParser(self) self.rawjsontotextparser = RawJSONtoTextParser(self) self.update_data_package(network_data_package) @@ -486,19 +555,17 @@ async def prepare_data_package(self, relevant_games: typing.Set[str], or remote_checksum != cache_checksum: needed_updates.add(game) else: - self.update_game(cached_game) + self.update_game(cached_game, game) if needed_updates: await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates]) - def update_game(self, game_package: dict): - for item_name, item_id in game_package["item_name_to_id"].items(): - self.item_names[item_id] = item_name - for location_name, location_id in game_package["location_name_to_id"].items(): - self.location_names[location_id] = location_name + def update_game(self, game_package: dict, game: str): + self.item_names.update_game(game, game_package["item_name_to_id"]) + self.location_names.update_game(game, game_package["location_name_to_id"]) def update_data_package(self, data_package: dict): for game, game_data in data_package["games"].items(): - self.update_game(game_data) + self.update_game(game_data, game) def consume_network_data_package(self, data_package: dict): self.update_data_package(data_package) diff --git a/MultiServer.py b/MultiServer.py index 4fb03732d811..2c08b0b4eb08 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -168,9 +168,11 @@ class Context: slot_info: typing.Dict[int, NetworkSlot] generator_version = Version(0, 0, 0) checksums: typing.Dict[str, str] - item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})') + item_names: typing.Dict[str, typing.Dict[int, str]] = ( + collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})'))) item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] - location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})') + location_names: typing.Dict[str, typing.Dict[int, str]] = ( + collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})'))) location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] all_item_and_group_names: typing.Dict[str, typing.Set[str]] all_location_and_group_names: typing.Dict[str, typing.Set[str]] @@ -271,14 +273,21 @@ def _init_game_data(self): if "checksum" in game_package: self.checksums[game_name] = game_package["checksum"] for item_name, item_id in game_package["item_name_to_id"].items(): - self.item_names[item_id] = item_name + self.item_names[game_name][item_id] = item_name for location_name, location_id in game_package["location_name_to_id"].items(): - self.location_names[location_id] = location_name + self.location_names[game_name][location_id] = location_name self.all_item_and_group_names[game_name] = \ set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name]) self.all_location_and_group_names[game_name] = \ set(game_package["location_name_to_id"]) | set(self.location_name_groups.get(game_name, [])) + archipelago_item_names = self.item_names["Archipelago"] + archipelago_location_names = self.location_names["Archipelago"] + for game in [game_name for game_name in self.gamespackage if game_name != "Archipelago"]: + # Add Archipelago items and locations to each data package. + self.item_names[game].update(archipelago_item_names) + self.location_names[game].update(archipelago_location_names) + def item_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]: return self.gamespackage[game]["item_name_to_id"] if game in self.gamespackage else None @@ -783,10 +792,7 @@ async def on_client_connected(ctx: Context, client: Client): for slot, connected_clients in clients.items(): if connected_clients: name = ctx.player_names[team, slot] - players.append( - NetworkPlayer(team, slot, - ctx.name_aliases.get((team, slot), name), name) - ) + players.append(NetworkPlayer(team, slot, ctx.name_aliases.get((team, slot), name), name)) games = {ctx.games[x] for x in range(1, len(ctx.games) + 1)} games.add("Archipelago") await ctx.send_msgs(client, [{ @@ -801,8 +807,6 @@ async def on_client_connected(ctx: Context, client: Client): 'permissions': get_permissions(ctx), 'hint_cost': ctx.hint_cost, 'location_check_points': ctx.location_check_points, - 'datapackage_versions': {game: game_data["version"] for game, game_data - in ctx.gamespackage.items() if game in games}, 'datapackage_checksums': {game: game_data["checksum"] for game, game_data in ctx.gamespackage.items() if game in games and "checksum" in game_data}, 'seed_name': ctx.seed_name, @@ -1006,8 +1010,8 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi send_items_to(ctx, team, target_player, new_item) ctx.logger.info('(Team #%d) %s sent %s to %s (%s)' % ( - team + 1, ctx.player_names[(team, slot)], ctx.item_names[item_id], - ctx.player_names[(team, target_player)], ctx.location_names[location])) + team + 1, ctx.player_names[(team, slot)], ctx.item_names[ctx.slot_info[target_player].game][item_id], + ctx.player_names[(team, target_player)], ctx.location_names[ctx.slot_info[slot].game][location])) info_text = json_format_send_event(new_item, target_player) ctx.broadcast_team(team, [info_text]) @@ -1061,8 +1065,8 @@ def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str: text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \ - f"{ctx.item_names[hint.item]} is " \ - f"at {ctx.location_names[hint.location]} " \ + f"{ctx.item_names[ctx.slot_info[hint.receiving_player].game][hint.item]} is " \ + f"at {ctx.location_names[ctx.slot_info[hint.finding_player].game][hint.location]} " \ f"in {ctx.player_names[team, hint.finding_player]}'s World" if hint.entrance: @@ -1364,7 +1368,7 @@ def _cmd_remaining(self) -> bool: if self.ctx.remaining_mode == "enabled": remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id] + self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id] for item_id in remaining_item_ids)) else: self.output("No remaining items found.") @@ -1377,7 +1381,7 @@ def _cmd_remaining(self) -> bool: if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id] + self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id] for item_id in remaining_item_ids)) else: self.output("No remaining items found.") @@ -1395,7 +1399,8 @@ def _cmd_missing(self, filter_text="") -> bool: locations = get_missing_checks(self.ctx, self.client.team, self.client.slot) if locations: - names = [self.ctx.location_names[location] for location in locations] + game = self.ctx.slot_info[self.client.slot].game + names = [self.ctx.location_names[game][location] for location in locations] if filter_text: location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]] if filter_text in location_groups: # location group name @@ -1420,7 +1425,8 @@ def _cmd_checked(self, filter_text="") -> bool: locations = get_checked_checks(self.ctx, self.client.team, self.client.slot) if locations: - names = [self.ctx.location_names[location] for location in locations] + game = self.ctx.slot_info[self.client.slot].game + names = [self.ctx.location_names[game][location] for location in locations] if filter_text: location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]] if filter_text in location_groups: # location group name @@ -1501,10 +1507,10 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool: elif input_text.isnumeric(): game = self.ctx.games[self.client.slot] hint_id = int(input_text) - hint_name = self.ctx.item_names[hint_id] \ - if not for_location and hint_id in self.ctx.item_names \ - else self.ctx.location_names[hint_id] \ - if for_location and hint_id in self.ctx.location_names \ + hint_name = self.ctx.item_names[game][hint_id] \ + if not for_location and hint_id in self.ctx.item_names[game] \ + else self.ctx.location_names[game][hint_id] \ + if for_location and hint_id in self.ctx.location_names[game] \ else None if hint_name in self.ctx.non_hintable_names[game]: self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.") diff --git a/NetUtils.py b/NetUtils.py index 8fc3929e60b4..076fdc3ba44f 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -247,7 +247,7 @@ def _handle_item_name(self, node: JSONMessagePart): def _handle_item_id(self, node: JSONMessagePart): item_id = int(node["text"]) - node["text"] = self.ctx.item_names[item_id] + node["text"] = self.ctx.item_names.lookup_in_slot(item_id, node["player"]) return self._handle_item_name(node) def _handle_location_name(self, node: JSONMessagePart): @@ -255,8 +255,8 @@ def _handle_location_name(self, node: JSONMessagePart): return self._handle_color(node) def _handle_location_id(self, node: JSONMessagePart): - item_id = int(node["text"]) - node["text"] = self.ctx.location_names[item_id] + location_id = int(node["text"]) + node["text"] = self.ctx.location_names.lookup_in_slot(location_id, node["player"]) return self._handle_location_name(node) def _handle_entrance_name(self, node: JSONMessagePart): diff --git a/UndertaleClient.py b/UndertaleClient.py index e1538ce81d2e..cdc21c561ab8 100644 --- a/UndertaleClient.py +++ b/UndertaleClient.py @@ -247,8 +247,8 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict): with open(os.path.join(ctx.save_game_folder, filename), "w") as f: toDraw = "" for i in range(20): - if i < len(str(ctx.item_names[l.item])): - toDraw += str(ctx.item_names[l.item])[i] + if i < len(str(ctx.item_names.lookup_in_slot(l.item))): + toDraw += str(ctx.item_names.lookup_in_slot(l.item))[i] else: break f.write(toDraw) diff --git a/Utils.py b/Utils.py index 780271996583..9f6837215c5e 100644 --- a/Utils.py +++ b/Utils.py @@ -46,7 +46,7 @@ def as_simple_string(self) -> str: return ".".join(str(item) for item in self) -__version__ = "0.4.6" +__version__ = "0.5.0" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") @@ -458,6 +458,9 @@ class KeyedDefaultDict(collections.defaultdict): """defaultdict variant that uses the missing key as argument to default_factory""" default_factory: typing.Callable[[typing.Any], typing.Any] + def __init__(self, default_factory: typing.Callable[[Any], Any] = None, **kwargs): + super().__init__(default_factory, **kwargs) + def __missing__(self, key): self[key] = value = self.default_factory(key) return value diff --git a/WargrooveClient.py b/WargrooveClient.py index 77180502cefc..c5fdeb3532f5 100644 --- a/WargrooveClient.py +++ b/WargrooveClient.py @@ -176,7 +176,7 @@ def on_package(self, cmd: str, args: dict): if not os.path.isfile(path): open(path, 'w').close() # Announcing commander unlocks - item_name = self.item_names[network_item.item] + item_name = self.item_names.lookup_in_slot(network_item.item) if item_name in faction_table.keys(): for commander in faction_table[item_name]: logger.info(f"{commander.name} has been unlocked!") @@ -197,7 +197,7 @@ def on_package(self, cmd: str, args: dict): open(print_path, 'w').close() with open(print_path, 'w') as f: f.write("Received " + - self.item_names[network_item.item] + + self.item_names.lookup_in_slot(network_item.item) + " from " + self.player_names[network_item.player]) f.close() @@ -342,7 +342,7 @@ def update_commander_data(self): faction_items = 0 faction_item_names = [faction + ' Commanders' for faction in faction_table.keys()] for network_item in self.items_received: - if self.item_names[network_item.item] in faction_item_names: + if self.item_names.lookup_in_slot(network_item.item) in faction_item_names: faction_items += 1 starting_groove = (faction_items - 1) * self.starting_groove_multiplier # Must be an integer larger than 0 diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index cfdbe25ff2fe..22d1f19f6bdf 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -56,15 +56,6 @@ def get_datapackage(): return network_data_package -@api_endpoints.route('/datapackage_version') -@cache.cached() -def get_datapackage_versions(): - from worlds import AutoWorldRegister - - version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()} - return version_package - - @api_endpoints.route('/datapackage_checksum') @cache.cached() def get_datapackage_checksums(): diff --git a/WebHostLib/templates/ootTracker.html b/WebHostLib/templates/ootTracker.html deleted file mode 100644 index ea7a6d5a4c30..000000000000 --- a/WebHostLib/templates/ootTracker.html +++ /dev/null @@ -1,180 +0,0 @@ - - - - {{ player_name }}'s Tracker - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
{{ hookshot_length }}
-
-
-
- -
{{ bottle_count if bottle_count > 0 else '' }}
-
-
-
- -
{{ wallet_size }}
-
-
-
- -
Zelda
-
-
-
- -
Epona
-
-
-
- -
Saria
-
-
-
- -
Sun
-
-
-
- -
Time
-
-
-
- -
Storms
-
-
-
- -
{{ token_count }}
-
-
-
- -
Min
-
-
-
- -
Bol
-
-
-
- -
Ser
-
-
-
- -
Req
-
-
-
- -
Noc
-
-
-
- -
Pre
-
-
-
- -
{{ piece_count if piece_count > 0 else '' }}
-
-
- - - - - - - - {% for area in checks_done %} - - - - - - - - {% for location in location_info[area] %} - - - - - - - {% endfor %} - - {% endfor %} -
Items
{{ area }} {{'▼' if area != 'Total'}}{{ small_key_counts.get(area, '-') }}{{ boss_key_counts.get(area, '-') }}{{ checks_done[area] }} / {{ checks_in_area[area] }}
{{ location }}{{ '✔' if location_info[area][location] else '' }}
-
- - diff --git a/Zelda1Client.py b/Zelda1Client.py index cd76a0a5ca78..6d7af0a94dcf 100644 --- a/Zelda1Client.py +++ b/Zelda1Client.py @@ -152,7 +152,7 @@ def get_payload(ctx: ZeldaContext): def reconcile_shops(ctx: ZeldaContext): - checked_location_names = [ctx.location_names[location] for location in ctx.checked_locations] + checked_location_names = [ctx.location_names.lookup_in_slot(location) for location in ctx.checked_locations] shops = [location for location in checked_location_names if "Shop" in location] left_slots = [shop for shop in shops if "Left" in shop] middle_slots = [shop for shop in shops if "Middle" in shop] @@ -190,7 +190,7 @@ async def parse_locations(locations_array, ctx: ZeldaContext, force: bool, zone= locations_checked = [] location = None for location in ctx.missing_locations: - location_name = ctx.location_names[location] + location_name = ctx.location_names.lookup_in_slot(location) if location_name in Locations.overworld_locations and zone == "overworld": status = locations_array[Locations.major_location_offsets[location_name]] diff --git a/docs/network protocol.md b/docs/network protocol.md index 604ff6708fca..da5c41431501 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -53,7 +53,7 @@ Example: ``` ## (Server -> Client) -These packets are are sent from the multiworld server to the client. They are not messages which the server accepts. +These packets are sent from the multiworld server to the client. They are not messages which the server accepts. * [RoomInfo](#RoomInfo) * [ConnectionRefused](#ConnectionRefused) * [Connected](#Connected) @@ -80,7 +80,6 @@ Sent to clients when they connect to an Archipelago server. | hint_cost | int | The percentage of total locations that need to be checked to receive a hint from the server. | | location_check_points | int | The amount of hint points you receive per item/location check completed. | | games | list\[str\] | List of games present in this multiworld. | -| datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. Used to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents). **Deprecated. Use `datapackage_checksums` instead.** | | datapackage_checksums | dict[str, str] | Checksum hash of the individual games' data packages the server will send. Used by newer clients to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents) for more information. | | seed_name | str | Uniquely identifying name of this generation | | time | float | Unix time stamp of "now". Send for time synchronization if wanted for things like the DeathLink Bounce. | @@ -500,9 +499,9 @@ In JSON this may look like: {"item": 3, "location": 3, "player": 3, "flags": 0} ] ``` -`item` is the item id of the item. Item ids are in the range of ± 253-1. +`item` is the item id of the item. Item ids are only supported in the range of [-253, 253 - 1], with anything ≤ 0 reserved for Archipelago use. -`location` is the location id of the item inside the world. Location ids are in the range of ± 253-1. +`location` is the location id of the item inside the world. Location ids are only supported in the range of [-253, 253 - 1], with anything ≤ 0 reserved for Archipelago use. `player` is the player slot of the world the item is located in, except when inside an [LocationInfo](#LocationInfo) Packet then it will be the slot of the player to receive the item @@ -646,15 +645,47 @@ class Hint(typing.NamedTuple): ``` ### Data Package Contents -A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago server most easily. Currently, this package is used to send ID to name mappings so that clients need not maintain their own mappings. - -We encourage clients to cache the data package they receive on disk, or otherwise not tied to a session. You will know when your cache is outdated if the [RoomInfo](#RoomInfo) packet or the datapackage itself denote a different version. A special case is datapackage version 0, where it is expected the package is custom and should not be cached. - -Note: - * Any ID is unique to its type across AP: Item 56 only exists once and Location 56 only exists once. - * Any Name is unique to its type across its own Game only: Single Arrow can exist in two games. - * The IDs from the game "Archipelago" may be used in any other game. - Especially Location ID -1: Cheat Console and -2: Server (typically Remote Start Inventory) +A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago +server most easily and not maintain their own mappings. Some contents include: + + - Name to ID mappings for items and locations. + - A checksum of each game's data package for clients to tell if a cached package is invalid. + +We encourage clients to cache the data package they receive on disk, or otherwise not tied to a session. You will know +when your cache is outdated if the [RoomInfo](#RoomInfo) packet or the datapackage itself denote a different checksum +than any locally cached ones. + +**Important Notes about IDs and Names**: + +* IDs ≤ 0 are reserved for "Archipelago" and should not be used by other world implementations. +* The IDs from the game "Archipelago" (in `worlds/generic`) may be used in any world. + * Especially Location ID `-1`: `Cheat Console` and `-2`: `Server` (typically Remote Start Inventory) +* Any names and IDs are only unique in its own world data package, but different games may reuse these names or IDs. + * At runtime, you will need to look up the game of the player to know which item or location ID/Name to lookup in the + data package. This can be easily achieved by reviewing the `slot_info` for a particular player ID prior to lookup. + * For example, a data package like this is valid (Some properties such as `checksum` were omitted): + ```json + { + "games": { + "Game A": { + "location_name_to_id": { + "Boss Chest": 40 + }, + "item_name_to_id": { + "Item X": 12 + } + }, + "Game B": { + "location_name_to_id": { + "Minigame Prize": 40 + }, + "item_name_to_id": { + "Item X": 40 + } + } + } + } + ``` #### Contents | Name | Type | Notes | @@ -668,7 +699,6 @@ GameData is a **dict** but contains these keys and values. It's broken out into |---------------------|----------------|-------------------------------------------------------------------------------------------------------------------------------| | item_name_to_id | dict[str, int] | Mapping of all item names to their respective ID. | | location_name_to_id | dict[str, int] | Mapping of all location names to their respective ID. | -| version | int | Version number of this game's data. Deprecated. Used by older clients to request an updated datapackage if cache is outdated. | | checksum | str | A checksum hash of this game's data. | ### Tags diff --git a/kvui.py b/kvui.py index a1663126cc71..98aa9516b266 100644 --- a/kvui.py +++ b/kvui.py @@ -683,10 +683,18 @@ def refresh_hints(self, hints): for hint in hints: data.append({ "receiving": {"text": self.parser.handle_node({"type": "player_id", "text": hint["receiving_player"]})}, - "item": {"text": self.parser.handle_node( - {"type": "item_id", "text": hint["item"], "flags": hint["item_flags"]})}, + "item": {"text": self.parser.handle_node({ + "type": "item_id", + "text": hint["item"], + "flags": hint["item_flags"], + "player": hint["receiving_player"], + })}, "finding": {"text": self.parser.handle_node({"type": "player_id", "text": hint["finding_player"]})}, - "location": {"text": self.parser.handle_node({"type": "location_id", "text": hint["location"]})}, + "location": {"text": self.parser.handle_node({ + "type": "location_id", + "text": hint["location"], + "player": hint["finding_player"], + })}, "entrance": {"text": self.parser.handle_node({"type": "color" if hint["entrance"] else "text", "color": "blue", "text": hint["entrance"] if hint["entrance"] else "Vanilla"})}, diff --git a/test/general/test_ids.py b/test/general/test_ids.py index 98c41b67b176..e4010af394f5 100644 --- a/test/general/test_ids.py +++ b/test/general/test_ids.py @@ -6,22 +6,6 @@ class TestIDs(unittest.TestCase): - def test_unique_items(self): - """Tests that every game has a unique ID per item in the datapackage""" - known_item_ids = set() - for gamename, world_type in AutoWorldRegister.world_types.items(): - current = len(known_item_ids) - known_item_ids |= set(world_type.item_id_to_name) - self.assertEqual(len(known_item_ids) - len(world_type.item_id_to_name), current) - - def test_unique_locations(self): - """Tests that every game has a unique ID per location in the datapackage""" - known_location_ids = set() - for gamename, world_type in AutoWorldRegister.world_types.items(): - current = len(known_location_ids) - known_location_ids |= set(world_type.location_id_to_name) - self.assertEqual(len(known_location_ids) - len(world_type.location_id_to_name), current) - def test_range_items(self): """There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision.""" for gamename, world_type in AutoWorldRegister.world_types.items(): diff --git a/test/programs/test_common_client.py b/test/programs/test_common_client.py new file mode 100644 index 000000000000..9936240d17b9 --- /dev/null +++ b/test/programs/test_common_client.py @@ -0,0 +1,106 @@ +import unittest + +import NetUtils +from CommonClient import CommonContext + + +class TestCommonContext(unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.ctx = CommonContext() + self.ctx.slot = 1 # Pretend we're player 1 for this. + self.ctx.slot_info.update({ + 1: NetUtils.NetworkSlot("Player 1", "__TestGame1", NetUtils.SlotType.player), + 2: NetUtils.NetworkSlot("Player 2", "__TestGame1", NetUtils.SlotType.player), + 3: NetUtils.NetworkSlot("Player 3", "__TestGame2", NetUtils.SlotType.player), + }) + self.ctx.consume_players_package([ + NetUtils.NetworkPlayer(1, 1, "Player 1", "Player 1"), + NetUtils.NetworkPlayer(1, 2, "Player 2", "Player 2"), + NetUtils.NetworkPlayer(1, 3, "Player 3", "Player 3"), + ]) + # Using IDs outside the "safe range" for testing purposes only. If this fails unit tests, it's because + # another world is not following the spec for allowed ID ranges. + self.ctx.update_data_package({ + "games": { + "__TestGame1": { + "location_name_to_id": { + "Test Location 1 - Safe": 2**54 + 1, + "Test Location 2 - Duplicate": 2**54 + 2, + }, + "item_name_to_id": { + "Test Item 1 - Safe": 2**54 + 1, + "Test Item 2 - Duplicate": 2**54 + 2, + }, + }, + "__TestGame2": { + "location_name_to_id": { + "Test Location 3 - Duplicate": 2**54 + 2, + }, + "item_name_to_id": { + "Test Item 3 - Duplicate": 2**54 + 2, + }, + }, + }, + }) + + async def test_archipelago_datapackage_lookups_exist(self): + assert "Archipelago" in self.ctx.item_names, "Archipelago item names entry does not exist" + assert "Archipelago" in self.ctx.location_names, "Archipelago location names entry does not exist" + + async def test_implicit_name_lookups(self): + # Items + assert self.ctx.item_names[2**54 + 1] == "Test Item 1 - Safe" + assert self.ctx.item_names[2**54 + 3] == f"Unknown item (ID: {2**54+3})" + assert self.ctx.item_names[-1] == "Nothing" + + # Locations + assert self.ctx.location_names[2**54 + 1] == "Test Location 1 - Safe" + assert self.ctx.location_names[2**54 + 3] == f"Unknown location (ID: {2**54+3})" + assert self.ctx.location_names[-1] == "Cheat Console" + + async def test_explicit_name_lookups(self): + # Items + assert self.ctx.item_names["__TestGame1"][2**54+1] == "Test Item 1 - Safe" + assert self.ctx.item_names["__TestGame1"][2**54+2] == "Test Item 2 - Duplicate" + assert self.ctx.item_names["__TestGame1"][2**54+3] == f"Unknown item (ID: {2**54+3})" + assert self.ctx.item_names["__TestGame1"][-1] == "Nothing" + assert self.ctx.item_names["__TestGame2"][2**54+1] == f"Unknown item (ID: {2**54+1})" + assert self.ctx.item_names["__TestGame2"][2**54+2] == "Test Item 3 - Duplicate" + assert self.ctx.item_names["__TestGame2"][2**54+3] == f"Unknown item (ID: {2**54+3})" + assert self.ctx.item_names["__TestGame2"][-1] == "Nothing" + + # Locations + assert self.ctx.location_names["__TestGame1"][2**54+1] == "Test Location 1 - Safe" + assert self.ctx.location_names["__TestGame1"][2**54+2] == "Test Location 2 - Duplicate" + assert self.ctx.location_names["__TestGame1"][2**54+3] == f"Unknown location (ID: {2**54+3})" + assert self.ctx.location_names["__TestGame1"][-1] == "Cheat Console" + assert self.ctx.location_names["__TestGame2"][2**54+1] == f"Unknown location (ID: {2**54+1})" + assert self.ctx.location_names["__TestGame2"][2**54+2] == "Test Location 3 - Duplicate" + assert self.ctx.location_names["__TestGame2"][2**54+3] == f"Unknown location (ID: {2**54+3})" + assert self.ctx.location_names["__TestGame2"][-1] == "Cheat Console" + + async def test_lookup_helper_functions(self): + # Checking own slot. + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 1) == "Test Item 1 - Safe" + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 2) == "Test Item 2 - Duplicate" + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 3) == f"Unknown item (ID: {2 ** 54 + 3})" + assert self.ctx.item_names.lookup_in_slot(-1) == f"Nothing" + + # Checking others' slots. + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 1, 2) == "Test Item 1 - Safe" + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 2, 2) == "Test Item 2 - Duplicate" + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 1, 3) == f"Unknown item (ID: {2 ** 54 + 1})" + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 2, 3) == "Test Item 3 - Duplicate" + + # Checking by game. + assert self.ctx.item_names.lookup_in_game(2 ** 54 + 1, "__TestGame1") == "Test Item 1 - Safe" + assert self.ctx.item_names.lookup_in_game(2 ** 54 + 2, "__TestGame1") == "Test Item 2 - Duplicate" + assert self.ctx.item_names.lookup_in_game(2 ** 54 + 3, "__TestGame1") == f"Unknown item (ID: {2 ** 54 + 3})" + assert self.ctx.item_names.lookup_in_game(2 ** 54 + 1, "__TestGame2") == f"Unknown item (ID: {2 ** 54 + 1})" + assert self.ctx.item_names.lookup_in_game(2 ** 54 + 2, "__TestGame2") == "Test Item 3 - Duplicate" + + # Checking with Archipelago ids are valid in any game package. + assert self.ctx.item_names.lookup_in_slot(-1, 2) == "Nothing" + assert self.ctx.item_names.lookup_in_slot(-1, 3) == "Nothing" + assert self.ctx.item_names.lookup_in_game(-1, "__TestGame1") == "Nothing" + assert self.ctx.item_names.lookup_in_game(-1, "__TestGame2") == "Nothing" diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 5d674c0c22fd..6e17f023f6fb 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -258,18 +258,6 @@ class World(metaclass=AutoWorldRegister): location_name_groups: ClassVar[Dict[str, Set[str]]] = {} """maps location group names to sets of locations. Example: {"Sewer": {"Sewer Key Drop 1", "Sewer Key Drop 2"}}""" - data_version: ClassVar[int] = 0 - """ - 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) """ override this if changes to a world break forward-compatibility of the client @@ -543,7 +531,6 @@ def get_data_package_data(cls) -> "GamesPackage": "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 diff --git a/worlds/__init__.py b/worlds/__init__.py index 53b0c5ceb948..09f72882195e 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -33,7 +33,6 @@ class GamesPackage(TypedDict, total=False): location_name_groups: Dict[str, List[str]] location_name_to_id: Dict[str, int] checksum: str - version: int # TODO: Remove support after per game data packages API change. class DataPackage(TypedDict): diff --git a/worlds/adventure/__init__.py b/worlds/adventure/__init__.py index 84caca828f2c..1c2583b3ed6e 100644 --- a/worlds/adventure/__init__.py +++ b/worlds/adventure/__init__.py @@ -113,7 +113,6 @@ class AdventureWorld(World): settings: ClassVar[AdventureSettings] item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()} location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()} - data_version: ClassVar[int] = 1 required_client_version: Tuple[int, int, int] = (0, 3, 9) def __init__(self, world: MultiWorld, player: int): diff --git a/worlds/alttp/Client.py b/worlds/alttp/Client.py index 5b27f559efd7..db7555f24615 100644 --- a/worlds/alttp/Client.py +++ b/worlds/alttp/Client.py @@ -339,7 +339,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool: def new_check(location_id): new_locations.append(location_id) ctx.locations_checked.add(location_id) - location = ctx.location_names[location_id] + location = ctx.location_names.lookup_in_slot(location_id) snes_logger.info( f'New Check: {location} ' + f'({len(ctx.checked_locations) + 1 if ctx.checked_locations else len(ctx.locations_checked)}/' + @@ -552,9 +552,9 @@ async def game_watcher(self, ctx): item = ctx.items_received[recv_index] recv_index += 1 logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], recv_index, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index ae3dfe9e3b1a..3176f7a7fcce 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -213,7 +213,6 @@ class ALTTPWorld(World): item_name_to_id = {name: data.item_code for name, data in item_table.items() if type(data.item_code) == int} location_name_to_id = lookup_name_to_id - data_version = 9 required_client_version = (0, 4, 1) web = ALTTPWeb() diff --git a/worlds/bk_sudoku/__init__.py b/worlds/bk_sudoku/__init__.py index 195339c38070..2c57bc7301ff 100644 --- a/worlds/bk_sudoku/__init__.py +++ b/worlds/bk_sudoku/__init__.py @@ -34,7 +34,6 @@ 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] = {} diff --git a/worlds/blasphemous/__init__.py b/worlds/blasphemous/__init__.py index 9abcd81b20e1..a46fb55b9541 100644 --- a/worlds/blasphemous/__init__.py +++ b/worlds/blasphemous/__init__.py @@ -32,7 +32,6 @@ class BlasphemousWorld(World): game: str = "Blasphemous" web = BlasphemousWeb() - data_version = 2 item_name_to_id = {item["name"]: (base_id + index) for index, item in enumerate(item_table)} location_name_to_id = {loc["name"]: (base_id + index) for index, loc in enumerate(location_table)} diff --git a/worlds/bumpstik/__init__.py b/worlds/bumpstik/__init__.py index d922c0277ac3..fe261dc94d30 100644 --- a/worlds/bumpstik/__init__.py +++ b/worlds/bumpstik/__init__.py @@ -39,8 +39,6 @@ class BumpStikWorld(World): location_name_to_id = location_table item_name_groups = item_groups - data_version = 1 - required_client_version = (0, 3, 8) options: BumpstikOptions diff --git a/worlds/checksfinder/__init__.py b/worlds/checksfinder/__init__.py index b70c65bb08f5..c8b9587f8500 100644 --- a/worlds/checksfinder/__init__.py +++ b/worlds/checksfinder/__init__.py @@ -33,8 +33,6 @@ class ChecksFinderWorld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {name: data.id for name, data in advancement_table.items()} - data_version = 4 - def _get_checksfinder_data(self): return { 'world_seed': self.multiworld.per_slot_randoms[self.player].getrandbits(32), diff --git a/worlds/clique/__init__.py b/worlds/clique/__init__.py index 30c0e47f818e..b5cc74d94ac0 100644 --- a/worlds/clique/__init__.py +++ b/worlds/clique/__init__.py @@ -37,7 +37,6 @@ class CliqueWorld(World): """The greatest game of all time.""" game = "Clique" - data_version = 3 web = CliqueWebWorld() option_definitions = clique_options location_name_to_id = location_table diff --git a/worlds/cv64/__init__.py b/worlds/cv64/__init__.py index 2f483cd4d919..0d384acc8f3d 100644 --- a/worlds/cv64/__init__.py +++ b/worlds/cv64/__init__.py @@ -64,7 +64,6 @@ class CV64World(World): options: CV64Options settings: typing.ClassVar[CV64Settings] topology_present = True - data_version = 1 item_name_to_id = get_item_names_to_ids() location_name_to_id = get_location_names_to_ids() diff --git a/worlds/cv64/client.py b/worlds/cv64/client.py index ff9c79f578be..bea8ce38825d 100644 --- a/worlds/cv64/client.py +++ b/worlds/cv64/client.py @@ -146,7 +146,7 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: text_color = bytearray([0xA2, 0x0B]) else: text_color = bytearray([0xA2, 0x02]) - received_text, num_lines = cv64_text_wrap(f"{ctx.item_names[next_item.item]}\n" + received_text, num_lines = cv64_text_wrap(f"{ctx.item_names.lookup_in_slot(next_item.item)}\n" f"from {ctx.player_names[next_item.player]}", 96) await bizhawk.guarded_write(ctx.bizhawk_ctx, [(0x389BE1, [next_item.item & 0xFF], "RDRAM"), diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index c4b2232b32dc..020010981160 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -49,7 +49,6 @@ class DarkSouls3World(World): option_definitions = dark_souls_options topology_present: bool = True web = DarkSouls3Web() - data_version = 8 base_id = 100000 enabled_location_categories: Set[DS3LocationCategory] required_client_version = (0, 4, 2) diff --git a/worlds/dkc3/Client.py b/worlds/dkc3/Client.py index efa199e1d0c9..8e4a1bf2a423 100644 --- a/worlds/dkc3/Client.py +++ b/worlds/dkc3/Client.py @@ -86,7 +86,7 @@ async def game_watcher(self, ctx): for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names[new_check_id] + location = ctx.location_names.lookup_in_slot(new_check_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) @@ -99,9 +99,9 @@ async def game_watcher(self, ctx): item = ctx.items_received[recv_index] recv_index += 1 logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], recv_index, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) snes_buffered_write(ctx, DKC3_RECV_PROGRESS_ADDR, bytes([recv_index])) if item.item in item_rom_data: diff --git a/worlds/dkc3/__init__.py b/worlds/dkc3/__init__.py index f298114905fd..de6fb4a44a03 100644 --- a/worlds/dkc3/__init__.py +++ b/worlds/dkc3/__init__.py @@ -61,7 +61,6 @@ class DKC3World(World): options: DKC3Options topology_present = False - data_version = 2 #hint_blacklist = {LocationName.rocket_rush_flag} item_name_to_id = {name: data.code for name, data in item_table.items()} diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index ca2862113fd4..a9dfcc5044b1 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -43,8 +43,6 @@ class DLCqworld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = location_table - data_version = 1 - options_dataclass = DLCQuestOptions options: DLCQuestOptions diff --git a/worlds/doom_1993/__init__.py b/worlds/doom_1993/__init__.py index ace33f994c33..b6138ae07103 100644 --- a/worlds/doom_1993/__init__.py +++ b/worlds/doom_1993/__init__.py @@ -42,7 +42,6 @@ class DOOM1993World(World): options: DOOM1993Options game = "DOOM 1993" web = DOOM1993Web() - data_version = 3 required_client_version = (0, 3, 9) item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} diff --git a/worlds/doom_ii/__init__.py b/worlds/doom_ii/__init__.py index daad94553517..38840f552a13 100644 --- a/worlds/doom_ii/__init__.py +++ b/worlds/doom_ii/__init__.py @@ -43,7 +43,6 @@ class DOOM2World(World): options: DOOM2Options game = "DOOM II" web = DOOM2Web() - data_version = 3 required_client_version = (0, 3, 9) item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} diff --git a/worlds/factorio/Client.py b/worlds/factorio/Client.py index d245e1bb7af6..258a5445328c 100644 --- a/worlds/factorio/Client.py +++ b/worlds/factorio/Client.py @@ -247,7 +247,7 @@ async def game_watcher(ctx: FactorioContext): if ctx.locations_checked != research_data: bridge_logger.debug( f"New researches done: " - f"{[ctx.location_names[rid] for rid in research_data - ctx.locations_checked]}") + f"{[ctx.location_names.lookup_in_slot(rid) for rid in research_data - ctx.locations_checked]}") ctx.locations_checked = research_data await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}]) death_link_tick = data.get("death_link_tick", 0) @@ -360,7 +360,7 @@ async def factorio_server_watcher(ctx: FactorioContext): transfer_item: NetworkItem = ctx.items_received[ctx.send_index] item_id = transfer_item.item player_name = ctx.player_names[transfer_item.player] - item_name = ctx.item_names[item_id] + item_name = ctx.item_names.lookup_in_slot(item_id) factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.") commands[ctx.send_index] = f"/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}" ctx.send_index += 1 diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 3b7475738489..1ea2f6e4c98c 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -95,7 +95,6 @@ class Factorio(World): item_name_groups = { "Progressive": set(progressive_tech_table.keys()), } - data_version = 8 required_client_version = (0, 4, 2) ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs() diff --git a/worlds/ff1/__init__.py b/worlds/ff1/__init__.py index ce5519b13a1a..3a5047506850 100644 --- a/worlds/ff1/__init__.py +++ b/worlds/ff1/__init__.py @@ -40,7 +40,6 @@ class FF1World(World): settings_key = "ffr_options" game = "Final Fantasy" topology_present = False - data_version = 2 ff1_items = FF1Items() ff1_locations = FF1Locations() diff --git a/worlds/ffmq/__init__.py b/worlds/ffmq/__init__.py index b995cc427c9b..ac3e91370933 100644 --- a/worlds/ffmq/__init__.py +++ b/worlds/ffmq/__init__.py @@ -56,8 +56,6 @@ class FFMQWorld(World): create_regions = create_regions set_rules = set_rules stage_set_rules = stage_set_rules - - data_version = 1 web = FFMQWebWorld() # settings: FFMQSettings @@ -216,4 +214,3 @@ def extend_hint_information(self, hint_data): hint_data[self.player][location.address] += f"/{hint}" else: hint_data[self.player][location.address] = hint - diff --git a/worlds/generic/__init__.py b/worlds/generic/__init__.py index 6b2ffdfee180..b88295b43237 100644 --- a/worlds/generic/__init__.py +++ b/worlds/generic/__init__.py @@ -40,7 +40,6 @@ 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 diff --git a/worlds/heretic/__init__.py b/worlds/heretic/__init__.py index c83cdb9477b2..fc5ffdd2de2b 100644 --- a/worlds/heretic/__init__.py +++ b/worlds/heretic/__init__.py @@ -41,7 +41,6 @@ class HereticWorld(World): options: HereticOptions game = "Heretic" web = HereticWeb() - data_version = 3 required_client_version = (0, 3, 9) item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 3530030fa695..fdaece8d34cd 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -154,7 +154,6 @@ class HKWorld(World): ranges: typing.Dict[str, typing.Tuple[int, int]] charm_costs: typing.List[int] cached_filler_items = {} - data_version = 2 def __init__(self, world, player): super(HKWorld, self).__init__(world, player) diff --git a/worlds/hylics2/__init__.py b/worlds/hylics2/__init__.py index be7ebf199127..18bcb0edc143 100644 --- a/worlds/hylics2/__init__.py +++ b/worlds/hylics2/__init__.py @@ -37,8 +37,6 @@ class Hylics2World(World): options_dataclass = Hylics2Options options: Hylics2Options - data_version = 3 - def set_rules(self): Rules.set_rules(self) diff --git a/worlds/kdl3/Client.py b/worlds/kdl3/Client.py index e33a680bc025..6faa8206c2df 100644 --- a/worlds/kdl3/Client.py +++ b/worlds/kdl3/Client.py @@ -330,9 +330,9 @@ async def game_watcher(self, ctx) -> None: item = ctx.items_received[recv_amount] recv_amount += 1 logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], recv_amount, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), recv_amount, len(ctx.items_received))) snes_buffered_write(ctx, KDL3_RECV_COUNT, pack("H", recv_amount)) item_idx = item.item & 0x00000F @@ -415,7 +415,7 @@ async def game_watcher(self, ctx) -> None: for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names[new_check_id] + location = ctx.location_names.lookup_in_slot(new_check_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/' f'{len(ctx.missing_locations) + len(ctx.checked_locations)})') diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 6c7517f359dc..f7de0f41f9c0 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -78,11 +78,6 @@ class LinksAwakeningWorld(World): settings: typing.ClassVar[LinksAwakeningSettings] topology_present = True # show path to required location checks in spoiler - # data_version is used to signal that items, locations or their names - # changed. Set this to 0 during development so other games' clients do not - # cache any texts, then increase by 1 for each release that makes changes. - data_version = 1 - # ID of first item and location, could be hard-coded but code may be easier # to read with this as a propery. base_id = BASE_ID diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index fa24fdc3bc63..302e7e1d85b2 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -37,7 +37,6 @@ class LingoWorld(World): base_id = 444400 topology_present = True - data_version = 1 options_dataclass = LingoOptions options: LingoOptions diff --git a/worlds/lufia2ac/Client.py b/worlds/lufia2ac/Client.py index 3c05e6395d90..1e8437d20e84 100644 --- a/worlds/lufia2ac/Client.py +++ b/worlds/lufia2ac/Client.py @@ -147,9 +147,9 @@ async def game_watcher(self, ctx: SNIContext) -> None: snes_items_received += 1 snes_logger.info("Received %s from %s (%s) (%d/%d in list)" % ( - ctx.item_names[item.item], + ctx.item_names.lookup_in_slot(item.item), ctx.player_names[item.player], - ctx.location_names[item.location], + ctx.location_names.lookup_in_slot(item.location, item.player), snes_items_received, len(ctx.items_received))) snes_buffered_write(ctx, L2AC_RX_ADDR + 2 * (snes_items_received + 1), item_code.to_bytes(2, "little")) snes_buffered_write(ctx, L2AC_RX_ADDR, snes_items_received.to_bytes(2, "little")) diff --git a/worlds/lufia2ac/__init__.py b/worlds/lufia2ac/__init__.py index 561429c825f3..6433452cefea 100644 --- a/worlds/lufia2ac/__init__.py +++ b/worlds/lufia2ac/__init__.py @@ -65,7 +65,6 @@ class L2ACWorld(World): "Iris treasures": {name for name, data in l2ac_item_table.items() if data.type is ItemType.IRIS_TREASURE}, "Party members": {name for name, data in l2ac_item_table.items() if data.type is ItemType.PARTY_MEMBER}, } - data_version: ClassVar[int] = 2 required_client_version: Tuple[int, int, int] = (0, 4, 4) # L2ACWorld specific properties diff --git a/worlds/meritous/__init__.py b/worlds/meritous/__init__.py index 728d7af8616d..7a21b19ef247 100644 --- a/worlds/meritous/__init__.py +++ b/worlds/meritous/__init__.py @@ -44,8 +44,6 @@ class MeritousWorld(World): location_name_to_id = location_table item_name_groups = item_groups - data_version = 2 - # NOTE: Remember to change this before this game goes live required_client_version = (0, 2, 4) diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index 343b9bad19a9..75e043d0cbaf 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -92,8 +92,6 @@ class MinecraftWorld(World): item_name_to_id = Constants.item_name_to_id location_name_to_id = Constants.location_name_to_id - data_version = 7 - def _get_mc_data(self) -> Dict[str, Any]: exits = [connection[0] for connection in Constants.region_info["default_connections"]] return { diff --git a/worlds/mmbn3/__init__.py b/worlds/mmbn3/__init__.py index eac8a37bf06d..97725e728bae 100644 --- a/worlds/mmbn3/__init__.py +++ b/worlds/mmbn3/__init__.py @@ -57,8 +57,6 @@ class MMBN3World(World): settings: typing.ClassVar[MMBN3Settings] topology_present = False - data_version = 1 - item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {loc_data.name: loc_data.id for loc_data in all_locations} diff --git a/worlds/noita/__init__.py b/worlds/noita/__init__.py index 43078c5e4320..af2921768d6a 100644 --- a/worlds/noita/__init__.py +++ b/worlds/noita/__init__.py @@ -34,14 +34,13 @@ class NoitaWorld(World): item_name_groups = items.item_name_groups location_name_groups = locations.location_name_groups - data_version = 2 web = NoitaWeb() def generate_early(self) -> None: if not self.multiworld.get_player_name(self.player).isascii(): raise Exception("Noita yaml's slot name has invalid character(s).") - + # Returned items will be sent over to the client def fill_slot_data(self) -> Dict[str, Any]: return self.options.as_dict("death_link", "victory_condition", "path_option", "hidden_chests", diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index d9ee63850eaf..9346ac55103a 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -150,8 +150,6 @@ class OOTWorld(World): location_name_to_id = location_name_to_id web = OOTWeb() - data_version = 3 - required_client_version = (0, 4, 0) item_name_groups = { diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py index be66fa3a8a1e..44227d4becaa 100644 --- a/worlds/overcooked2/__init__.py +++ b/worlds/overcooked2/__init__.py @@ -48,7 +48,6 @@ class Overcooked2World(World): web = Overcooked2Web() required_client_version = (0, 3, 8) topology_present: bool = False - data_version = 3 item_name_to_id = item_name_to_id item_id_to_name = item_id_to_name diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index 6225350a5e51..3e50f748d922 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -87,7 +87,6 @@ class PokemonEmeraldWorld(World): item_name_groups = ITEM_GROUPS location_name_groups = LOCATION_GROUPS - data_version = 2 required_client_version = (0, 4, 6) badge_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]] diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 79028a68b187..003e0a32e97d 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -74,7 +74,6 @@ class PokemonRedBlueWorld(World): option_definitions = pokemon_rb_options settings: typing.ClassVar[PokemonSettings] - data_version = 9 required_client_version = (0, 4, 2) topology_present = True diff --git a/worlds/raft/__init__.py b/worlds/raft/__init__.py index 8e4eda09e10f..e96cd4471268 100644 --- a/worlds/raft/__init__.py +++ b/worlds/raft/__init__.py @@ -39,7 +39,6 @@ class RaftWorld(World): location_name_to_id = locations_lookup_name_to_id option_definitions = raft_options - data_version = 2 required_client_version = (0, 3, 4) def create_items(self): diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index c5a8d71b5d63..eb657699540f 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -35,7 +35,6 @@ class RLWorld(World): game = "Rogue Legacy" option_definitions = rl_options topology_present = True - data_version = 4 required_client_version = (0, 3, 5) web = RLWeb() diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 5afdb797e7de..b6a1901a8db1 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -44,7 +44,6 @@ class RiskOfRainWorld(World): } location_name_to_id = item_pickups - data_version = 9 required_client_version = (0, 4, 5) web = RiskOfWeb() total_revivals: int diff --git a/worlds/sa2b/__init__.py b/worlds/sa2b/__init__.py index 6279aa943263..f7d1ca72b09f 100644 --- a/worlds/sa2b/__init__.py +++ b/worlds/sa2b/__init__.py @@ -58,7 +58,6 @@ class SA2BWorld(World): options_dataclass = SA2BOptions options: SA2BOptions topology_present = False - data_version = 7 item_name_groups = item_groups item_name_to_id = {name: data.code for name, data in item_table.items()} diff --git a/worlds/sc2/Client.py b/worlds/sc2/Client.py index 4e55509dda48..ac9ccfffcdbc 100644 --- a/worlds/sc2/Client.py +++ b/worlds/sc2/Client.py @@ -244,10 +244,10 @@ def print_faction_title(): self.formatted_print(f" [u]{faction.name}[/u] ") for item_id in categorized_items[faction]: - item_name = self.ctx.item_names[item_id] + item_name = self.ctx.item_names.lookup_in_slot(item_id) received_child_items = items_received_set.intersection(parent_to_child.get(item_id, [])) matching_children = [child for child in received_child_items - if item_matches_filter(self.ctx.item_names[child])] + if item_matches_filter(self.ctx.item_names.lookup_in_slot(child))] received_items_of_this_type = items_received.get(item_id, []) item_is_match = item_matches_filter(item_name) if item_is_match or len(matching_children) > 0: @@ -1165,7 +1165,7 @@ def request_unfinished_missions(ctx: SC2Context) -> None: objectives = set(ctx.locations_for_mission(mission)) if objectives: remaining_objectives = objectives.difference(ctx.checked_locations) - unfinished_locations[mission] = [ctx.location_names[location_id] for location_id in remaining_objectives] + unfinished_locations[mission] = [ctx.location_names.lookup_in_slot(location_id) for location_id in remaining_objectives] else: unfinished_locations[mission] = [] diff --git a/worlds/sc2/ClientGui.py b/worlds/sc2/ClientGui.py index 167583fd1ecb..f9dcfc18eb4a 100644 --- a/worlds/sc2/ClientGui.py +++ b/worlds/sc2/ClientGui.py @@ -269,7 +269,7 @@ def sort_unfinished_locations(self, mission_name: str) -> Tuple[Dict[LocationTyp for loc in self.ctx.locations_for_mission(mission_name): if loc in self.ctx.missing_locations: count += 1 - locations[lookup_location_id_to_type[loc]].append(self.ctx.location_names[loc]) + locations[lookup_location_id_to_type[loc]].append(self.ctx.location_names.lookup_in_slot(loc)) plando_locations = [] for plando_loc in self.ctx.plando_locations: diff --git a/worlds/shorthike/__init__.py b/worlds/shorthike/__init__.py index 3e0430f024ca..470b061c4bc0 100644 --- a/worlds/shorthike/__init__.py +++ b/worlds/shorthike/__init__.py @@ -28,7 +28,6 @@ class ShortHikeWorld(World): game = "A Short Hike" web = ShortHikeWeb() - data_version = 2 item_name_to_id = {item["name"]: item["id"] for item in item_table} location_name_to_id = {loc["name"]: loc["id"] for loc in location_table} diff --git a/worlds/sm/Client.py b/worlds/sm/Client.py index 7c97f743c552..6d6dd08ba5df 100644 --- a/worlds/sm/Client.py +++ b/worlds/sm/Client.py @@ -123,7 +123,7 @@ async def game_watcher(self, ctx): location_id = locations_start_id + item_index ctx.locations_checked.add(location_id) - location = ctx.location_names[location_id] + location = ctx.location_names.lookup_in_slot(location_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) @@ -151,9 +151,8 @@ async def game_watcher(self, ctx): snes_buffered_write(ctx, SM_RECV_QUEUE_WCOUNT, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF])) logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], item_out_ptr, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), item_out_ptr, len(ctx.items_received))) await snes_flush_writes(ctx) - diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 7f12bf484c0f..826b1447793d 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -99,7 +99,6 @@ class SMWorld(World): game: str = "Super Metroid" topology_present = True - data_version = 3 option_definitions = sm_options settings: typing.ClassVar[SMSettings] diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 0e944aa4ab4b..833ae56ca302 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -35,7 +35,6 @@ class SM64World(World): item_name_to_id = item_table location_name_to_id = location_table - data_version = 9 required_client_version = (0, 3, 5) area_connections: typing.Dict[int, int] diff --git a/worlds/smw/Client.py b/worlds/smw/Client.py index 33a74b3dc80f..85bb3fe1ee16 100644 --- a/worlds/smw/Client.py +++ b/worlds/smw/Client.py @@ -448,7 +448,7 @@ async def game_watcher(self, ctx): for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names[new_check_id] + location = ctx.location_names.lookup_in_slot(new_check_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) @@ -499,15 +499,16 @@ async def game_watcher(self, ctx): if recv_index < len(ctx.items_received): item = ctx.items_received[recv_index] recv_index += 1 + sending_game = ctx.slot_info[item.player].game logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], recv_index, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) if self.should_show_message(ctx, item): if item.item != 0xBC0012 and item.item not in trap_rom_data: # Don't send messages for Boss Tokens - item_name = ctx.item_names[item.item] + item_name = ctx.item_names.lookup_in_slot(item.item) player_name = ctx.player_names[item.player] receive_message = generate_received_text(item_name, player_name) @@ -515,7 +516,7 @@ async def game_watcher(self, ctx): snes_buffered_write(ctx, SMW_RECV_PROGRESS_ADDR, bytes([recv_index&0xFF, (recv_index>>8)&0xFF])) if item.item in trap_rom_data: - item_name = ctx.item_names[item.item] + item_name = ctx.item_names.lookup_in_slot(item.item) player_name = ctx.player_names[item.player] receive_message = generate_received_text(item_name, player_name) @@ -596,7 +597,7 @@ async def game_watcher(self, ctx): for loc_id in ctx.checked_locations: if loc_id not in ctx.locations_checked: ctx.locations_checked.add(loc_id) - loc_name = ctx.location_names[loc_id] + loc_name = ctx.location_names.lookup_in_slot(loc_id) if loc_name not in location_id_to_level_id: continue diff --git a/worlds/smz3/Client.py b/worlds/smz3/Client.py index 0a248aa5d3f2..3c90ead0064c 100644 --- a/worlds/smz3/Client.py +++ b/worlds/smz3/Client.py @@ -109,7 +109,7 @@ async def game_watcher(self, ctx): location_id = locations_start_id + convertLocSMZ3IDToAPID(item_index) ctx.locations_checked.add(location_id) - location = ctx.location_names[location_id] + location = ctx.location_names.lookup_in_slot(location_id) snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) @@ -132,8 +132,7 @@ async def game_watcher(self, ctx): item_out_ptr += 1 snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + recv_progress_addr_table_offset, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF])) logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], item_out_ptr, len(ctx.items_received))) + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), + ctx.location_names.lookup_in_slot(item.location, item.player), item_out_ptr, len(ctx.items_received))) await snes_flush_writes(ctx) - diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index b030e3fa50d2..6056a171d370 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -68,7 +68,6 @@ class SMZ3World(World): """ game: str = "SMZ3" topology_present = False - data_version = 3 option_definitions = smz3_options item_names: Set[str] = frozenset(TotalSMZ3Item.lookup_name_to_id) location_names: Set[str] diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index 061322650e68..3baed165d821 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -176,7 +176,6 @@ class SoEWorld(World): options: SoEOptions settings: typing.ClassVar[SoESettings] topology_present = False - data_version = 5 web = SoEWebWorld() required_client_version = (0, 4, 4) diff --git a/worlds/spire/__init__.py b/worlds/spire/__init__.py index d8a9322ab415..5b0e1e17f23d 100644 --- a/worlds/spire/__init__.py +++ b/worlds/spire/__init__.py @@ -30,7 +30,6 @@ class SpireWorld(World): option_definitions = spire_options game = "Slay the Spire" topology_present = False - data_version = 2 web = SpireWeb() required_client_version = (0, 3, 7) diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index dafb1c64730f..61c866631690 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -73,7 +73,6 @@ class StardewValleyWorld(World): [location.name for location in locations] for group, locations in locations_by_tag.items() } - data_version = 3 required_client_version = (0, 4, 0) options_dataclass = StardewValleyOptions diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 08df70d78bbd..856117469e55 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -44,7 +44,6 @@ class SubnauticaWorld(World): location_name_to_id = all_locations options_dataclass = options.SubnauticaOptions options: options.SubnauticaOptions - data_version = 10 required_client_version = (0, 4, 1) creatures_to_scan: List[str] diff --git a/worlds/terraria/__init__.py b/worlds/terraria/__init__.py index ac6b25e51632..abc10a7bb37c 100644 --- a/worlds/terraria/__init__.py +++ b/worlds/terraria/__init__.py @@ -52,11 +52,6 @@ class TerrariaWorld(World): options_dataclass = TerrariaOptions options: TerrariaOptions - # data_version is used to signal that items, locations or their names - # changed. Set this to 0 during development so other games' clients do not - # cache any texts, then increase by 1 for each release that makes changes. - data_version = 2 - item_name_to_id = item_name_to_id location_name_to_id = location_name_to_id diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index ff7b3515e605..cab6fb648b95 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -39,7 +39,6 @@ class TimespinnerWorld(World): option_definitions = timespinner_options game = "Timespinner" topology_present = True - data_version = 12 web = TimespinnerWebWorld() required_client_version = (0, 4, 2) @@ -228,7 +227,7 @@ def assign_starter_items(self, excluded_items: Set[str]) -> None: non_local_items: Set[str] = self.multiworld.non_local_items[self.player].value local_items: Set[str] = self.multiworld.local_items[self.player].value - local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if + local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if item in local_items or not item in non_local_items) if not local_starter_melee_weapons: if 'Plasma Orb' in non_local_items: diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index 7565dc0147ce..a1f9081418e4 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -68,7 +68,6 @@ class TLoZWorld(World): settings: typing.ClassVar[TLoZSettings] game = "The Legend of Zelda" topology_present = False - data_version = 1 base_id = 7000 web = TLoZWeb() diff --git a/worlds/undertale/__init__.py b/worlds/undertale/__init__.py index 0694456a6b12..b87d3ac01e8f 100644 --- a/worlds/undertale/__init__.py +++ b/worlds/undertale/__init__.py @@ -52,8 +52,6 @@ class UndertaleWorld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {name: data.id for name, data in advancement_table.items()} - data_version = 7 - def _get_undertale_data(self): return { "world_seed": self.multiworld.per_slot_randoms[self.player].getrandbits(32), diff --git a/worlds/v6/__init__.py b/worlds/v6/__init__.py index 30a76f82cce6..3d3ee8cf58fd 100644 --- a/worlds/v6/__init__.py +++ b/worlds/v6/__init__.py @@ -34,8 +34,6 @@ class V6World(World): item_name_to_id = item_table location_name_to_id = location_table - data_version = 1 - area_connections: typing.Dict[int, int] area_cost_map: typing.Dict[int,int] diff --git a/worlds/yoshisisland/Client.py b/worlds/yoshisisland/Client.py index 1aff36c553c7..2a710b046a70 100644 --- a/worlds/yoshisisland/Client.py +++ b/worlds/yoshisisland/Client.py @@ -116,7 +116,7 @@ async def game_watcher(self, ctx: "SNIContext") -> None: for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names[new_check_id] + location = ctx.location_names.lookup_in_slot(new_check_id) total_locations = len(ctx.missing_locations) + len(ctx.checked_locations) snes_logger.info(f"New Check: {location} ({len(ctx.locations_checked)}/{total_locations})") await ctx.send_msgs([{"cmd": "LocationChecks", "locations": [new_check_id]}]) @@ -127,9 +127,9 @@ async def game_watcher(self, ctx: "SNIContext") -> None: item = ctx.items_received[recv_index] recv_index += 1 logging.info("Received %s from %s (%s) (%d/%d in list)" % ( - color(ctx.item_names[item.item], "red", "bold"), + color(ctx.item_names.lookup_in_slot(item.item), "red", "bold"), color(ctx.player_names[item.player], "yellow"), - ctx.location_names[item.location], recv_index, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) snes_buffered_write(ctx, ITEMQUEUE_HIGH, pack("H", recv_index)) if item.item in item_values: diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index cce120d7e3f4..205cc9ad6ba1 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -86,11 +86,6 @@ class ZillionWorld(World): item_name_to_id = _item_name_to_id location_name_to_id = _loc_name_to_id - # increment this every time something in your world's names/id mappings changes. - # While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be - # retrieved by clients on every connection. - data_version = 1 - logger: logging.Logger class LogStreamInterface: