From 52726139b4632a0ce9a4a0aa717b80d6ce460eed Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Sun, 23 Oct 2022 09:18:05 -0700 Subject: [PATCH] Zillion: support unicode player names (#1131) * work on unicode and seed verification * update zilliandomizer * fix log message --- CommonClient.py | 2 + ZillionClient.py | 83 +++++++++++++++++++++++---------- worlds/zillion/__init__.py | 3 +- worlds/zillion/requirements.txt | 2 +- 4 files changed, 64 insertions(+), 26 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index b17709ee..7960be0e 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -279,6 +279,7 @@ class CommonContext: self.auth = await self.console_input() async def send_connect(self, **kwargs: typing.Any) -> None: + """ send `Connect` packet to log in to server """ payload = { 'cmd': 'Connect', 'password': self.password, 'name': self.auth, 'version': Utils.version_tuple, @@ -294,6 +295,7 @@ class CommonContext: return await self.input_queue.get() async def connect(self, address: typing.Optional[str] = None) -> None: + """ disconnect any previous connection, and open new connection to the server """ await self.disconnect() self.server_task = asyncio.create_task(server_loop(self, address), name="server loop") diff --git a/ZillionClient.py b/ZillionClient.py index dee5c2b7..8ad10650 100644 --- a/ZillionClient.py +++ b/ZillionClient.py @@ -1,7 +1,7 @@ import asyncio import base64 import platform -from typing import Any, Coroutine, Dict, Optional, Type, cast +from typing import Any, Coroutine, Dict, Optional, Tuple, Type, cast # CommonClient import first to trigger ModuleUpdater from CommonClient import CommonContext, server_loop, gui_enabled, \ @@ -46,6 +46,8 @@ class ZillionContext(CommonContext): start_char: Chars = "JJ" rescues: Dict[int, RescueInfo] = {} loc_mem_to_id: Dict[int, int] = {} + got_room_info: asyncio.Event + """ flag for connected to server """ got_slot_data: asyncio.Event """ serves as a flag for whether I am logged in to the server """ @@ -65,6 +67,7 @@ class ZillionContext(CommonContext): super().__init__(server_address, password) self.from_game = asyncio.Queue() self.to_game = asyncio.Queue() + self.got_room_info = asyncio.Event() self.got_slot_data = asyncio.Event() self.look_for_retroarch = asyncio.Event() @@ -185,6 +188,9 @@ class ZillionContext(CommonContext): logger.info("received door data from server") doors = base64.b64decode(doors_b64) self.to_game.put_nowait(events.DoorEventToGame(doors)) + elif cmd == "RoomInfo": + self.seed_name = args["seed_name"] + self.got_room_info.set() def process_from_game_queue(self) -> None: if self.from_game.qsize(): @@ -238,6 +244,24 @@ class ZillionContext(CommonContext): self.next_item = len(self.items_received) +def name_seed_from_ram(data: bytes) -> Tuple[str, str]: + """ returns player name, and end of seed string """ + if len(data) == 0: + # no connection to game + return "", "xxx" + null_index = data.find(b'\x00') + if null_index == -1: + logger.warning(f"invalid game id in rom {data}") + null_index = len(data) + name = data[:null_index].decode() + null_index_2 = data.find(b'\x00', null_index + 1) + if null_index_2 == -1: + null_index_2 = len(data) + seed_name = data[null_index + 1:null_index_2].decode() + + return name, seed_name + + async def zillion_sync_task(ctx: ZillionContext) -> None: logger.info("started zillion sync task") @@ -263,47 +287,58 @@ async def zillion_sync_task(ctx: ZillionContext) -> None: with Memory(ctx.from_game, ctx.to_game) as memory: while not ctx.exit_event.is_set(): ram = await memory.read() - name = memory.get_player_name(ram).decode() + game_id = memory.get_rom_to_ram_data(ram) + name, seed_end = name_seed_from_ram(game_id) if len(name): if name == ctx.auth: # this is the name we know if ctx.server and ctx.server.socket: # type: ignore - if memory.have_generation_info(): - log_no_spam("everything connected") - await memory.process_ram(ram) - ctx.process_from_game_queue() - ctx.process_items_received() - else: # no generation info - if ctx.got_slot_data.is_set(): - memory.set_generation_info(ctx.rescues, ctx.loc_mem_to_id) - ctx.ap_id_to_name, ctx.ap_id_to_zz_id, _ap_id_to_zz_item = \ - make_id_to_others(ctx.start_char) - ctx.next_item = 0 - ctx.ap_local_count = len(ctx.checked_locations) - else: # no slot data yet - asyncio.create_task(ctx.send_connect()) - log_no_spam("logging in to server...") - await asyncio.wait(( - ctx.got_slot_data.wait(), - ctx.exit_event.wait(), - asyncio.sleep(6) - ), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets + if ctx.got_room_info.is_set(): + if ctx.seed_name and ctx.seed_name.endswith(seed_end): + # correct seed + if memory.have_generation_info(): + log_no_spam("everything connected") + await memory.process_ram(ram) + ctx.process_from_game_queue() + ctx.process_items_received() + else: # no generation info + if ctx.got_slot_data.is_set(): + memory.set_generation_info(ctx.rescues, ctx.loc_mem_to_id) + ctx.ap_id_to_name, ctx.ap_id_to_zz_id, _ap_id_to_zz_item = \ + make_id_to_others(ctx.start_char) + ctx.next_item = 0 + ctx.ap_local_count = len(ctx.checked_locations) + else: # no slot data yet + asyncio.create_task(ctx.send_connect()) + log_no_spam("logging in to server...") + await asyncio.wait(( + ctx.got_slot_data.wait(), + ctx.exit_event.wait(), + asyncio.sleep(6) + ), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets + else: # not correct seed name + log_no_spam("incorrect seed - did you mix up roms?") + else: # no room info + # If we get here, it looks like `RoomInfo` packet got lost + log_no_spam("waiting for room info from server...") else: # server not connected log_no_spam("waiting for server connection...") else: # new game log_no_spam("connected to new game") await ctx.disconnect() ctx.reset_server_state() + ctx.seed_name = None + ctx.got_room_info.clear() ctx.reset_game_state() memory.reset_game_state() ctx.auth = name asyncio.create_task(ctx.connect()) await asyncio.wait(( - ctx.got_slot_data.wait(), + ctx.got_room_info.wait(), ctx.exit_event.wait(), asyncio.sleep(6) - ), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets + ), return_when=asyncio.FIRST_COMPLETED) else: # no name found in game if not help_message_shown: logger.info('In RetroArch, make sure "Settings > Network > Network Commands" is on.') diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 32b84015..d9827828 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -304,7 +304,8 @@ class ZillionWorld(World): zz_patcher.all_fixes_and_options(zz_options) zz_patcher.set_external_item_interface(zz_options.start_char, zz_options.max_level) zz_patcher.set_multiworld_items(multi_items) - zz_patcher.set_rom_to_ram_data(self.world.player_name[self.player].replace(' ', '_').encode()) + game_id = self.world.player_name[self.player].encode() + b'\x00' + self.world.seed_name[-6:].encode() + zz_patcher.set_rom_to_ram_data(game_id) def generate_output(self, output_directory: str) -> None: """This method gets called from a threadpool, do not use world.random here. diff --git a/worlds/zillion/requirements.txt b/worlds/zillion/requirements.txt index 0ed98771..62f66899 100644 --- a/worlds/zillion/requirements.txt +++ b/worlds/zillion/requirements.txt @@ -1 +1 @@ -git+https://github.com/beauxq/zilliandomizer@45a45eaca4119a4d06d2c31546ad19f3abd77f63#egg=zilliandomizer==0.4.4 +git+https://github.com/beauxq/zilliandomizer@c97298ecb1bca58c3dd3376a1e1609fad53788cf#egg=zilliandomizer==0.4.5