Zillion: support unicode player names (#1131)

* work on unicode and seed verification

* update zilliandomizer

* fix log message
This commit is contained in:
Doug Hoskisson 2022-10-23 09:18:05 -07:00 committed by GitHub
parent 24105ac249
commit 52726139b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 64 additions and 26 deletions

View File

@ -279,6 +279,7 @@ class CommonContext:
self.auth = await self.console_input() self.auth = await self.console_input()
async def send_connect(self, **kwargs: typing.Any) -> None: async def send_connect(self, **kwargs: typing.Any) -> None:
""" send `Connect` packet to log in to server """
payload = { payload = {
'cmd': 'Connect', 'cmd': 'Connect',
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple, 'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
@ -294,6 +295,7 @@ class CommonContext:
return await self.input_queue.get() return await self.input_queue.get()
async def connect(self, address: typing.Optional[str] = None) -> None: async def connect(self, address: typing.Optional[str] = None) -> None:
""" disconnect any previous connection, and open new connection to the server """
await self.disconnect() await self.disconnect()
self.server_task = asyncio.create_task(server_loop(self, address), name="server loop") self.server_task = asyncio.create_task(server_loop(self, address), name="server loop")

View File

@ -1,7 +1,7 @@
import asyncio import asyncio
import base64 import base64
import platform 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 # CommonClient import first to trigger ModuleUpdater
from CommonClient import CommonContext, server_loop, gui_enabled, \ from CommonClient import CommonContext, server_loop, gui_enabled, \
@ -46,6 +46,8 @@ class ZillionContext(CommonContext):
start_char: Chars = "JJ" start_char: Chars = "JJ"
rescues: Dict[int, RescueInfo] = {} rescues: Dict[int, RescueInfo] = {}
loc_mem_to_id: Dict[int, int] = {} loc_mem_to_id: Dict[int, int] = {}
got_room_info: asyncio.Event
""" flag for connected to server """
got_slot_data: asyncio.Event got_slot_data: asyncio.Event
""" serves as a flag for whether I am logged in to the server """ """ 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) super().__init__(server_address, password)
self.from_game = asyncio.Queue() self.from_game = asyncio.Queue()
self.to_game = asyncio.Queue() self.to_game = asyncio.Queue()
self.got_room_info = asyncio.Event()
self.got_slot_data = asyncio.Event() self.got_slot_data = asyncio.Event()
self.look_for_retroarch = asyncio.Event() self.look_for_retroarch = asyncio.Event()
@ -185,6 +188,9 @@ class ZillionContext(CommonContext):
logger.info("received door data from server") logger.info("received door data from server")
doors = base64.b64decode(doors_b64) doors = base64.b64decode(doors_b64)
self.to_game.put_nowait(events.DoorEventToGame(doors)) 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: def process_from_game_queue(self) -> None:
if self.from_game.qsize(): if self.from_game.qsize():
@ -238,6 +244,24 @@ class ZillionContext(CommonContext):
self.next_item = len(self.items_received) 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: async def zillion_sync_task(ctx: ZillionContext) -> None:
logger.info("started zillion sync task") logger.info("started zillion sync task")
@ -263,11 +287,15 @@ async def zillion_sync_task(ctx: ZillionContext) -> None:
with Memory(ctx.from_game, ctx.to_game) as memory: with Memory(ctx.from_game, ctx.to_game) as memory:
while not ctx.exit_event.is_set(): while not ctx.exit_event.is_set():
ram = await memory.read() 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 len(name):
if name == ctx.auth: if name == ctx.auth:
# this is the name we know # this is the name we know
if ctx.server and ctx.server.socket: # type: ignore if ctx.server and ctx.server.socket: # type: ignore
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(): if memory.have_generation_info():
log_no_spam("everything connected") log_no_spam("everything connected")
await memory.process_ram(ram) await memory.process_ram(ram)
@ -288,22 +316,29 @@ async def zillion_sync_task(ctx: ZillionContext) -> None:
ctx.exit_event.wait(), ctx.exit_event.wait(),
asyncio.sleep(6) asyncio.sleep(6)
), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets ), 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 else: # server not connected
log_no_spam("waiting for server connection...") log_no_spam("waiting for server connection...")
else: # new game else: # new game
log_no_spam("connected to new game") log_no_spam("connected to new game")
await ctx.disconnect() await ctx.disconnect()
ctx.reset_server_state() ctx.reset_server_state()
ctx.seed_name = None
ctx.got_room_info.clear()
ctx.reset_game_state() ctx.reset_game_state()
memory.reset_game_state() memory.reset_game_state()
ctx.auth = name ctx.auth = name
asyncio.create_task(ctx.connect()) asyncio.create_task(ctx.connect())
await asyncio.wait(( await asyncio.wait((
ctx.got_slot_data.wait(), ctx.got_room_info.wait(),
ctx.exit_event.wait(), ctx.exit_event.wait(),
asyncio.sleep(6) asyncio.sleep(6)
), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets ), return_when=asyncio.FIRST_COMPLETED)
else: # no name found in game else: # no name found in game
if not help_message_shown: if not help_message_shown:
logger.info('In RetroArch, make sure "Settings > Network > Network Commands" is on.') logger.info('In RetroArch, make sure "Settings > Network > Network Commands" is on.')

View File

@ -304,7 +304,8 @@ class ZillionWorld(World):
zz_patcher.all_fixes_and_options(zz_options) 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_external_item_interface(zz_options.start_char, zz_options.max_level)
zz_patcher.set_multiworld_items(multi_items) 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: def generate_output(self, output_directory: str) -> None:
"""This method gets called from a threadpool, do not use world.random here. """This method gets called from a threadpool, do not use world.random here.

View File

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