Core: Remove Universally Unique ID Requirements (Per-Game Data Packages) ()

This commit is contained in:
Zach Parks 2024-06-01 06:07:13 -05:00 committed by GitHub
parent f3003ff147
commit 5aa6ad63ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
77 changed files with 317 additions and 390 deletions

View File

@ -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"]:

View File

@ -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)

View File

@ -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.")

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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():

View File

@ -1,180 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ player_name }}&apos;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>

View File

@ -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]]

View File

@ -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
View File

@ -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"})},

View File

@ -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():

View File

@ -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"

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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]))

View File

@ -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()

View File

@ -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] = {}

View File

@ -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)}

View File

@ -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

View File

@ -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),

View File

@ -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

View File

@ -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()

View File

@ -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"),

View File

@ -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)

View File

@ -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:

View File

@ -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()}

View File

@ -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

View File

@ -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()}

View File

@ -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()}

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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()}

View File

@ -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)

View File

@ -37,8 +37,6 @@ class Hylics2World(World):
options_dataclass = Hylics2Options
options: Hylics2Options
data_version = 3
def set_rules(self):
Rules.set_rules(self)

View File

@ -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)})')

View File

@ -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

View File

@ -37,7 +37,6 @@ class LingoWorld(World):
base_id = 444400
topology_present = True
data_version = 1
options_dataclass = LingoOptions
options: LingoOptions

View File

@ -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"))

View File

@ -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

View File

@ -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)

View File

@ -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 {

View File

@ -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}

View File

@ -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",

View File

@ -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 = {

View File

@ -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

View File

@ -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]]]

View File

@ -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

View File

@ -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):

View File

@ -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()

View File

@ -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

View File

@ -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()}

View File

@ -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] = []

View File

@ -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:

View File

@ -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}

View File

@ -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)

View File

@ -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]

View File

@ -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]

View File

@ -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

View File

@ -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)

View File

@ -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]

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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:

View File

@ -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()

View File

@ -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),

View File

@ -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]

View File

@ -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:

View File

@ -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: