Core: Remove Universally Unique ID Requirements (Per-Game Data Packages) (#1933)
This commit is contained in:
		
							parent
							
								
									f3003ff147
								
							
						
					
					
						commit
						5aa6ad63ca
					
				| 
						 | 
				
			
			@ -112,7 +112,7 @@ class AdventureContext(CommonContext):
 | 
			
		|||
            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"]:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import collections
 | 
			
		||||
import copy
 | 
			
		||||
import logging
 | 
			
		||||
import asyncio
 | 
			
		||||
| 
						 | 
				
			
			@ -8,6 +9,7 @@ import sys
 | 
			
		|||
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 @@ class CommonContext:
 | 
			
		|||
        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 @@ class CommonContext:
 | 
			
		|||
                        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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 @@ class Context:
 | 
			
		|||
            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 @@ class ClientMessageProcessor(CommonCommandProcessor):
 | 
			
		|||
        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 @@ class ClientMessageProcessor(CommonCommandProcessor):
 | 
			
		|||
            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 @@ class ClientMessageProcessor(CommonCommandProcessor):
 | 
			
		|||
        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 @@ class ClientMessageProcessor(CommonCommandProcessor):
 | 
			
		|||
        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 @@ class ClientMessageProcessor(CommonCommandProcessor):
 | 
			
		|||
        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.")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -247,7 +247,7 @@ class JSONtoTextParser(metaclass=HandlerMeta):
 | 
			
		|||
 | 
			
		||||
    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 @@ class JSONtoTextParser(metaclass=HandlerMeta):
 | 
			
		|||
        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):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										5
									
								
								Utils.py
								
								
								
								
							
							
						
						
									
										5
									
								
								Utils.py
								
								
								
								
							| 
						 | 
				
			
			@ -46,7 +46,7 @@ class Version(typing.NamedTuple):
 | 
			
		|||
        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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -176,7 +176,7 @@ class WargrooveContext(CommonContext):
 | 
			
		|||
                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 @@ class WargrooveContext(CommonContext):
 | 
			
		|||
                    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 @@ class WargrooveContext(CommonContext):
 | 
			
		|||
            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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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():
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,180 +0,0 @@
 | 
			
		|||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
    <title>{{ player_name }}'s Tracker</title>
 | 
			
		||||
    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/ootTracker.css') }}"/>
 | 
			
		||||
    <script type="application/ecmascript" src="{{ url_for('static', filename='assets/ootTracker.js') }}"></script>
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
    <div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
 | 
			
		||||
        <table id="inventory-table">
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td><img src="{{ ocarina_url }}" class="{{ 'acquired' if 'Ocarina' in acquired_items }}" title="Ocarina" /></td>
 | 
			
		||||
                <td><img src="{{ icons['Bombs'] }}" class="{{ 'acquired' if 'Bomb Bag' in acquired_items }}" title="Bombs" /></td>
 | 
			
		||||
                <td><img src="{{ icons['Bow'] }}" class="{{ 'acquired' if 'Bow' in acquired_items }}" title="Fairy Bow" /></td>
 | 
			
		||||
                <td><img src="{{ icons['Fire Arrows'] }}" class="{{ 'acquired' if 'Fire Arrows' in acquired_items }}" title="Fire Arrows" /></td>
 | 
			
		||||
                <td><img src="{{ icons['Kokiri Sword'] }}" class="{{ 'acquired' if 'Kokiri Sword' in acquired_items }}" title="Kokiri Sword" /></td>
 | 
			
		||||
                <td><img src="{{ icons['Biggoron Sword'] }}" class="{{ 'acquired' if 'Biggoron Sword' in acquired_items }}" title="Biggoron's Sword" /></td>
 | 
			
		||||
                <td><img src="{{ icons['Mirror Shield'] }}" class="{{ 'acquired' if 'Mirror Shield' in acquired_items }}" title="Mirror Shield" /></td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td><img src="{{ icons['Slingshot'] }}" class="{{ 'acquired' if 'Slingshot' in acquired_items }}" title="Slingshot" /></td>
 | 
			
		||||
                <td><img src="{{ icons['Bombchus'] }}" class="{{ 'acquired' if has_bombchus }}" title="Bombchus" /></td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div class="counted-item">
 | 
			
		||||
                        <img src="{{ hookshot_url }}" class="{{ 'acquired' if 'Progressive Hookshot' in acquired_items }}" title="Progressive Hookshot" />
 | 
			
		||||
                        <div class="item-count">{{ hookshot_length }}</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td><img src="{{ icons['Ice Arrows'] }}" class="{{ 'acquired' if 'Ice Arrows' in acquired_items }}" title="Ice Arrows" /></td>
 | 
			
		||||
                <td><img src="{{ strength_upgrade_url }}" class="{{ 'acquired' if 'Progressive Strength Upgrade' in acquired_items }}" title="Progressive Strength Upgrade" /></td>
 | 
			
		||||
                <td><img src="{{ icons['Goron Tunic'] }}" class="{{ 'acquired' if 'Goron Tunic' in acquired_items }}" title="Goron Tunic" /></td>
 | 
			
		||||
                <td><img src="{{ icons['Zora Tunic'] }}" class="{{ 'acquired' if 'Zora Tunic' in acquired_items }}" title="Zora Tunic" /></td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td><img src="{{ icons['Boomerang'] }}" class="{{ 'acquired' if 'Boomerang' in acquired_items }}" title="Boomerang" /></td>
 | 
			
		||||
                <td><img src="{{ icons['Lens of Truth'] }}" class="{{ 'acquired' if 'Lens of Truth' in acquired_items }}" title="Lens of Truth" /></td>
 | 
			
		||||
                <td><img src="{{ icons['Megaton Hammer'] }}" class="{{ 'acquired' if 'Megaton Hammer' in acquired_items }}" title="Megaton Hammer" /></td>
 | 
			
		||||
                <td><img src="{{ icons['Light Arrows'] }}" class="{{ 'acquired' if 'Light Arrows' in acquired_items }}" title="Light Arrows" /></td>
 | 
			
		||||
                <td><img src="{{ scale_url }}" class="{{ 'acquired' if 'Progressive Scale' in acquired_items }}" title="Progressive Scale" /></td>
 | 
			
		||||
                <td><img src="{{ icons['Iron Boots'] }}" class="{{ 'acquired' if 'Iron Boots' in acquired_items }}" title="Iron Boots" /></td>
 | 
			
		||||
                <td><img src="{{ icons['Hover Boots'] }}" class="{{ 'acquired' if 'Hover Boots' in acquired_items }}" title="Hover Boots" /></td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div class="counted-item">
 | 
			
		||||
                        <img src="{{ bottle_url }}" class="{{ 'acquired' if bottle_count > 0 }}" title="Bottles" />
 | 
			
		||||
                        <div class="item-count">{{ bottle_count if bottle_count > 0 else '' }}</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td><img src="{{ icons['Dins Fire'] }}" class="{{ 'acquired' if 'Dins Fire' in acquired_items }}" title="Din's Fire" /></td>
 | 
			
		||||
                <td><img src="{{ icons['Farores Wind'] }}" class="{{ 'acquired' if 'Farores Wind' in acquired_items }}" title="Farore's Wind" /></td>
 | 
			
		||||
                <td><img src="{{ icons['Nayrus Love'] }}" class="{{ 'acquired' if 'Nayrus Love' in acquired_items }}" title="Nayru's Love" /></td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div class="counted-item">
 | 
			
		||||
                        <img src="{{ wallet_url }}" class="{{ 'acquired' if 'Progressive Wallet' in acquired_items }}" title="Progressive Wallet" />
 | 
			
		||||
                        <div class="item-count">{{ wallet_size }}</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td><img src="{{ magic_meter_url }}" class="{{ 'acquired' if 'Magic Meter' in acquired_items }}" title="Magic Meter" /></td>
 | 
			
		||||
                <td><img src="{{ icons['Gerudo Membership Card'] }}" class="{{ 'acquired' if 'Gerudo Membership Card' in acquired_items }}" title="Gerudo Membership Card" /></td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div class="counted-item">
 | 
			
		||||
                        <img src="{{ icons['Zeldas Lullaby'] }}" class="{{ 'acquired' if 'Zeldas Lullaby' in acquired_items }}" title="Zelda's Lullaby" id="lullaby"/>
 | 
			
		||||
                        <div class="item-count">Zelda</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div class="counted-item">
 | 
			
		||||
                        <img src="{{ icons['Eponas Song'] }}" class="{{ 'acquired' if 'Eponas Song' in acquired_items }}" title="Epona's Song" id="epona" />
 | 
			
		||||
                        <div class="item-count">Epona</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div class="counted-item">
 | 
			
		||||
                        <img src="{{ icons['Sarias Song'] }}" class="{{ 'acquired' if 'Sarias Song' in acquired_items }}" title="Saria's Song" id="saria"/>
 | 
			
		||||
                        <div class="item-count">Saria</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div class="counted-item">
 | 
			
		||||
                        <img src="{{ icons['Suns Song'] }}" class="{{ 'acquired' if 'Suns Song' in acquired_items }}" title="Sun's Song" id="sun"/>
 | 
			
		||||
                        <div class="item-count">Sun</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div class="counted-item">
 | 
			
		||||
                        <img src="{{ icons['Song of Time'] }}" class="{{ 'acquired' if 'Song of Time' in acquired_items }}" title="Song of Time" id="time"/>
 | 
			
		||||
                        <div class="item-count">Time</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div class="counted-item">
 | 
			
		||||
                        <img src="{{ icons['Song of Storms'] }}" class="{{ 'acquired' if 'Song of Storms' in acquired_items }}" title="Song of Storms" />
 | 
			
		||||
                        <div class="item-count">Storms</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div class="counted-item">
 | 
			
		||||
                        <img src="{{ icons['Gold Skulltula Token'] }}" class="{{ 'acquired' if token_count > 0 }}" title="Gold Skulltula Tokens" />
 | 
			
		||||
                        <div class="item-count">{{ token_count }}</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div class="counted-item">
 | 
			
		||||
                        <img src="{{ icons['Minuet of Forest'] }}" class="{{ 'acquired' if 'Minuet of Forest' in acquired_items }}" title="Minuet of Forest" />
 | 
			
		||||
                        <div class="item-count">Min</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div class="counted-item">
 | 
			
		||||
                        <img src="{{ icons['Bolero of Fire'] }}" class="{{ 'acquired' if 'Bolero of Fire' in acquired_items }}" title="Bolero of Fire" />
 | 
			
		||||
                        <div class="item-count">Bol</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div class="counted-item">
 | 
			
		||||
                        <img src="{{ icons['Serenade of Water'] }}" class="{{ 'acquired' if 'Serenade of Water' in acquired_items }}" title="Serenade of Water" />
 | 
			
		||||
                        <div class="item-count">Ser</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div class="counted-item">
 | 
			
		||||
                        <img src="{{ icons['Requiem of Spirit'] }}" class="{{ 'acquired' if 'Requiem of Spirit' in acquired_items }}" title="Requiem of Spirit" />
 | 
			
		||||
                        <div class="item-count">Req</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div class="counted-item">
 | 
			
		||||
                        <img src="{{ icons['Nocturne of Shadow'] }}" class="{{ 'acquired' if 'Nocturne of Shadow' in acquired_items }}" title="Nocturne of Shadow" />
 | 
			
		||||
                        <div class="item-count">Noc</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div class="counted-item">
 | 
			
		||||
                        <img src="{{ icons['Prelude of Light'] }}" class="{{ 'acquired' if 'Prelude of Light' in acquired_items }}" title="Prelude of Light" />
 | 
			
		||||
                        <div class="item-count">Pre</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div class="counted-item">
 | 
			
		||||
                        <img src="{{ icons['Triforce'] if game_finished else icons['Triforce Piece'] }}" class="{{ 'acquired' if game_finished or piece_count > 0 }}" title="{{ 'Triforce' if game_finished else 'Triforce Pieces' }}" id=triforce />
 | 
			
		||||
                        <div class="item-count">{{ piece_count if piece_count > 0 else '' }}</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
        </table>
 | 
			
		||||
        <table id="location-table">
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td></td>
 | 
			
		||||
                <td><img src="{{ icons['Small Key'] }}" title="Small Keys" /></td>
 | 
			
		||||
                <td><img src="{{ icons['Boss Key'] }}" title="Boss Key" /></td>
 | 
			
		||||
                <td class="right-align">Items</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            {% for area in checks_done %}
 | 
			
		||||
            <tr class="location-category" id="{{area}}-header">
 | 
			
		||||
                <td>{{ area }} {{'▼' if area != 'Total'}}</td>
 | 
			
		||||
                <td class="smallkeys">{{ small_key_counts.get(area, '-') }}</td>
 | 
			
		||||
                <td class="bosskeys">{{ boss_key_counts.get(area, '-') }}</td>
 | 
			
		||||
                <td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tbody class="locations hide" id="{{area}}">
 | 
			
		||||
                {% for location in location_info[area] %}
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td class="location-name">{{ location }}</td>
 | 
			
		||||
                    <td></td>
 | 
			
		||||
                    <td></td>
 | 
			
		||||
                    <td class="counter">{{ '✔' if location_info[area][location] else '' }}</td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
        </table>
 | 
			
		||||
    </div>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			@ -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]]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 ± 2<sup>53</sup>-1.
 | 
			
		||||
`item` is the item id of the item. Item ids are only supported in the range of [-2<sup>53</sup>, 2<sup>53</sup> - 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 ± 2<sup>53</sup>-1.
 | 
			
		||||
`location` is the location id of the item inside the world. Location ids are only supported in the range of [-2<sup>53</sup>, 2<sup>53</sup> - 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.
 | 
			
		||||
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:
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
   - 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.
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										14
									
								
								kvui.py
								
								
								
								
							
							
						
						
									
										14
									
								
								kvui.py
								
								
								
								
							| 
						 | 
				
			
			@ -683,10 +683,18 @@ class HintLog(RecycleView):
 | 
			
		|||
        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"})},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,22 +6,6 @@ from . import setup_solo_multiworld
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
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():
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
| 
						 | 
				
			
			@ -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 @@ class World(metaclass=AutoWorldRegister):
 | 
			
		|||
            "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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 @@ class ALTTPSNIClient(SNIClient):
 | 
			
		|||
            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]))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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] = {}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -146,7 +146,7 @@ class Castlevania64Client(BizHawkClient):
 | 
			
		|||
                        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"),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -86,7 +86,7 @@ class DKC3SNIClient(SNIClient):
 | 
			
		|||
 | 
			
		||||
        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 @@ class DKC3SNIClient(SNIClient):
 | 
			
		|||
            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:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 @@ class FFMQWorld(World):
 | 
			
		|||
                                        hint_data[self.player][location.address] += f"/{hint}"
 | 
			
		||||
                                    else:
 | 
			
		||||
                                        hint_data[self.player][location.address] = hint
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,8 +37,6 @@ class Hylics2World(World):
 | 
			
		|||
    options_dataclass = Hylics2Options
 | 
			
		||||
    options: Hylics2Options
 | 
			
		||||
 | 
			
		||||
    data_version = 3
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def set_rules(self):
 | 
			
		||||
        Rules.set_rules(self)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -330,9 +330,9 @@ class KDL3SNIClient(SNIClient):
 | 
			
		|||
                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 @@ class KDL3SNIClient(SNIClient):
 | 
			
		|||
 | 
			
		||||
            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)})')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,7 +37,6 @@ class LingoWorld(World):
 | 
			
		|||
 | 
			
		||||
    base_id = 444400
 | 
			
		||||
    topology_present = True
 | 
			
		||||
    data_version = 1
 | 
			
		||||
 | 
			
		||||
    options_dataclass = LingoOptions
 | 
			
		||||
    options: LingoOptions
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -147,9 +147,9 @@ class L2ACSNIClient(SNIClient):
 | 
			
		|||
                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"))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}
 | 
			
		||||
    
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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]]]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -244,10 +244,10 @@ class StarcraftClientProcessor(ClientCommandProcessor):
 | 
			
		|||
                    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] = []
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -269,7 +269,7 @@ class SC2Manager(GameManager):
 | 
			
		|||
        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:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -123,7 +123,7 @@ class SMSNIClient(SNIClient):
 | 
			
		|||
            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 @@ class SMSNIClient(SNIClient):
 | 
			
		|||
            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)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -448,7 +448,7 @@ class SMWSNIClient(SNIClient):
 | 
			
		|||
 | 
			
		||||
        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 @@ class SMWSNIClient(SNIClient):
 | 
			
		|||
        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 @@ class SMWSNIClient(SNIClient):
 | 
			
		|||
 | 
			
		||||
            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 @@ class SMWSNIClient(SNIClient):
 | 
			
		|||
        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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -109,7 +109,7 @@ class SMZ3SNIClient(SNIClient):
 | 
			
		|||
            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 @@ class SMZ3SNIClient(SNIClient):
 | 
			
		|||
            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)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 @@ class TimespinnerWorld(World):
 | 
			
		|||
        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:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -116,7 +116,7 @@ class YoshisIslandSNIClient(SNIClient):
 | 
			
		|||
 | 
			
		||||
        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 @@ class YoshisIslandSNIClient(SNIClient):
 | 
			
		|||
            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:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue