diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 536d836e..6a4a9794 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -3957,3 +3957,11 @@ exit_ids = {'Links House Exit': (0x01, 0x00), 'Skull Woods First Section (Right)': 0x78, 'Skull Woods First Section (Top)': 0x76, 'Pyramid': 0x7B} + +exit_id_to_name = {} +for name, addresses in exit_ids.items(): + if type(addresses) == int: + exit_id_to_name[addresses] = name + else: + for address in addresses: + exit_id_to_name[address] = name diff --git a/Main.py b/Main.py index b2be128e..90c8345d 100644 --- a/Main.py +++ b/Main.py @@ -236,6 +236,24 @@ def main(args, seed=None): for future in futures: rom_name = future.result() rom_names.append(rom_name) + + def get_entrance_to_region(region: Region): + for entrance in region.entrances: + if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld): + return entrance + for entrance in region.entrances: # BFS might be better here, trying DFS for now. + return get_entrance_to_region(entrance.parent_region) + + # collect ER hint info + er_hint_data = {player: {} for player in range(1, world.players + 1) if world.shuffle[player] != "vanilla"} + from Regions import RegionType + for region in world.regions: + if region.player in er_hint_data and region.locations: + main_entrance = get_entrance_to_region(region) + for location in region.locations: + if type(location.address) == int: # skips events and crystals + er_hint_data[region.player][location.address] = main_entrance.name + multidata = zlib.compress(json.dumps({"names": parsed_names, "roms": rom_names, "remote_items": [player for player in range(1, world.players + 1) if @@ -244,7 +262,8 @@ def main(args, seed=None): (location.item.code, location.item.player)) for location in world.get_filled_locations() if type(location.address) is int], - "server_options": get_options()["server_options"] + "server_options": get_options()["server_options"], + "er_hint_data": er_hint_data, }).encode("utf-8"), 9) if args.jsonout: jsonout["multidata"] = list(multidata) diff --git a/MultiClient.py b/MultiClient.py index d3d723e0..9298e3a5 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -764,9 +764,11 @@ async def process_server_cmd(ctx : Context, cmd, args): 'yellow' if hint.finding_player != ctx.slot else 'magenta') player_recvd = color(ctx.player_names[hint.receiving_player], 'yellow' if hint.receiving_player != ctx.slot else 'magenta') - logging.info(f"[Hint]: {player_recvd}'s {item} can be found " - f"at {get_location_name_from_address(hint.location)} in {player_find}'s World." + - (" (found)" if hint.found else "")) + text = f"[Hint]: {player_recvd}'s {item} can be found " \ + f"at {color(get_location_name_from_address(hint.location), 'blue_bg')} in {player_find}'s World" + if hint.entrance: + text += " at " + color(hint.entrance, 'cyan_bg') + logging.info(text + (". (found)" if hint.found else ".")) elif cmd == "AliasUpdate": ctx.player_names = {p: n for p, n in args} elif cmd == 'Print': diff --git a/MultiServer.py b/MultiServer.py index 9cd69635..52ac103c 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -88,6 +88,7 @@ class Context: self.running = True self.client_activity_timers = {} self.client_game_state: typing.Dict[typing.Tuple[int, int], int] = collections.defaultdict(int) + self.er_hint_data: typing.Dict[int, typing.Dict[int, str]] = {} self.commandprocessor = ServerCommandProcessor(self) def get_save(self) -> dict: @@ -198,14 +199,14 @@ def notify_hints(ctx: Context, team: int, hints: typing.List[Utils.Hint]): texts = [['Print', format_hint(ctx, team, hint)] for hint in hints] for _, text in texts: logging.info("Notice (Team #%d): %s" % (team + 1, text)) + texts = json.dumps(texts) for client in ctx.clients: if client.auth and client.team == team: - if "Berserker" in client.tags: + if "Berserker" in client.tags and client.version >= [2, 2, 1]: payload = cmd - asyncio.create_task(send_json_msgs(client, payload)) else: payload = texts - asyncio.create_task(send_msgs(client, payload)) + asyncio.create_task(send_json_msgs(client, payload)) def update_aliases(ctx: Context, team: int, client: typing.Optional[Client] = None): @@ -383,7 +384,11 @@ def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[ if receiving_player == slot and item_id == seeked_item_id: location_id, finding_player = check found = location_id in ctx.location_checks[team, finding_player] - hints.append(Utils.Hint(receiving_player, finding_player, location_id, item_id, found)) + entrance = "" + if finding_player in ctx.er_hint_data: + if location_id in ctx.er_hint_data[finding_player]: + entrance = ctx.er_hint_data[finding_player][location_id] + hints.append(Utils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance)) return hints @@ -396,17 +401,24 @@ def collect_hints_location(ctx: Context, team: int, slot: int, location: str) -> if finding_player == slot and location_id == seeked_location: item_id, receiving_player = result found = location_id in ctx.location_checks[team, finding_player] - hints.append(Utils.Hint(receiving_player, finding_player, location_id, item_id, found)) - break # each location has 1 item + entrance = "" + if finding_player in ctx.er_hint_data: + if location_id in ctx.er_hint_data[finding_player]: + entrance = ctx.er_hint_data[finding_player][location_id] + hints.append(Utils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance)) + break # each location has 1 item return hints def format_hint(ctx: Context, team: int, hint: Utils.Hint) -> str: - return f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \ + text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \ f"{Items.lookup_id_to_name[hint.item]} can be found " \ f"at {get_location_name_from_address(hint.location)} " \ - f"in {ctx.player_names[team, hint.finding_player]}'s World." \ - + (" (found)" if hint.found else "") + f"in {ctx.player_names[team, hint.finding_player]}'s World" + + if hint.entrance: + text += f" at {hint.entrance}" + return text + (". (found)" if hint.found else ".") def get_intended_text(input_text: str, possible_answers: typing.Iterable[str]= console_names) -> typing.Tuple[str, bool, str]: @@ -1051,6 +1063,9 @@ async def main(args: argparse.Namespace): ctx.rom_names = {tuple(rom): (team, slot) for slot, team, rom in jsonobj['roms']} ctx.remote_items = set(jsonobj['remote_items']) ctx.locations = {tuple(k): tuple(v) for k, v in jsonobj['locations']} + if "er_hint_data" in jsonobj: + ctx.er_hint_data = {int(player): {int(address): name for address, name in loc_data.items()} + for player, loc_data in jsonobj["er_hint_data"].items()} except Exception as e: logging.exception('Failed to read multiworld data (%s)' % e) return diff --git a/Utils.py b/Utils.py index f9d5ffd7..0694fbb5 100644 --- a/Utils.py +++ b/Utils.py @@ -1,6 +1,6 @@ from __future__ import annotations -__version__ = "2.2.0" +__version__ = "2.2.1" _version_tuple = tuple(int(piece, 10) for piece in __version__.split(".")) import os @@ -159,17 +159,21 @@ class Hint(typing.NamedTuple): location: int item: int found: bool + entrance: str = "" def re_check(self, ctx, team) -> Hint: if self.found: return self found = self.location in ctx.location_checks[team, self.finding_player] if found: - return Hint(self.receiving_player, self.finding_player, self.location, self.item, found) + return Hint(self.receiving_player, self.finding_player, self.location, self.item, found, self.entrance) return self + def as_legacy(self) -> tuple: + return self.receiving_player, self.finding_player, self.location, self.item, self.found + def __hash__(self): - return hash((self.receiving_player, self.finding_player, self.location, self.item)) + return hash((self.receiving_player, self.finding_player, self.location, self.item, self.entrance)) def get_public_ipv4() -> str: import socket