Merge branch 'pull/5'
# Conflicts: # MultiClient.py # MultiServer.py # Mystery.py
This commit is contained in:
		
						commit
						72c33a2348
					
				| 
						 | 
				
			
			@ -60,6 +60,7 @@ class World(object):
 | 
			
		|||
                self.__dict__.setdefault(attr, {})[player] = val
 | 
			
		||||
            set_player_attr('_region_cache', {})
 | 
			
		||||
            set_player_attr('player_names', [])
 | 
			
		||||
            set_player_attr('remote_items', False)
 | 
			
		||||
            set_player_attr('required_medallions', ['Ether', 'Quake'])
 | 
			
		||||
            set_player_attr('swamp_patch_required', False)
 | 
			
		||||
            set_player_attr('powder_patch_required', False)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -266,6 +266,7 @@ def parse_arguments(argv, no_defaults=False):
 | 
			
		|||
    parser.add_argument('--enemy_damage', default=defval('default'), choices=['default', 'shuffled', 'chaos'])
 | 
			
		||||
    parser.add_argument('--shufflepots', default=defval(False), action='store_true')
 | 
			
		||||
    parser.add_argument('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4))
 | 
			
		||||
    parser.add_argument('--remote_items', default=defval(False), action='store_true')
 | 
			
		||||
    parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
 | 
			
		||||
    parser.add_argument('--names', default=defval(''))
 | 
			
		||||
    parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1))
 | 
			
		||||
| 
						 | 
				
			
			@ -291,7 +292,8 @@ def parse_arguments(argv, no_defaults=False):
 | 
			
		|||
                         'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
 | 
			
		||||
                         'retro', 'accessibility', 'hints', 'beemizer',
 | 
			
		||||
                         'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
 | 
			
		||||
                         'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep']:
 | 
			
		||||
                         'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep',
 | 
			
		||||
                         'remote_items']:
 | 
			
		||||
                value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
 | 
			
		||||
                if player == 1:
 | 
			
		||||
                    setattr(ret, name, {1: value})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										12
									
								
								Main.py
								
								
								
								
							
							
						
						
									
										12
									
								
								Main.py
								
								
								
								
							| 
						 | 
				
			
			@ -39,6 +39,7 @@ def main(args, seed=None):
 | 
			
		|||
        world.seed = int(seed)
 | 
			
		||||
    random.seed(world.seed)
 | 
			
		||||
 | 
			
		||||
    world.remote_items = args.remote_items.copy()
 | 
			
		||||
    world.mapshuffle = args.mapshuffle.copy()
 | 
			
		||||
    world.compassshuffle = args.compassshuffle.copy()
 | 
			
		||||
    world.keyshuffle = args.keyshuffle.copy()
 | 
			
		||||
| 
						 | 
				
			
			@ -187,6 +188,7 @@ def main(args, seed=None):
 | 
			
		|||
                    outfilepname = f'_T{team+1}' if world.teams > 1 else ''
 | 
			
		||||
                    if world.players > 1:
 | 
			
		||||
                        outfilepname += f'_P{player}'
 | 
			
		||||
                    if world.players > 1 or world.teams > 1:
 | 
			
		||||
                        outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" if world.player_names[player][team] != 'Player %d' % player else ''
 | 
			
		||||
                    outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % (world.logic[player], world.difficulty[player], world.difficulty_adjustments[player],
 | 
			
		||||
                                                                              world.mode[player], world.goal[player],
 | 
			
		||||
| 
						 | 
				
			
			@ -197,9 +199,12 @@ def main(args, seed=None):
 | 
			
		|||
                                                                              "-nohints" if not world.hints[player] else "")) if not args.outputname else ''
 | 
			
		||||
                    rom.write_to_file(output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc'))
 | 
			
		||||
 | 
			
		||||
        multidata = zlib.compress(json.dumps((parsed_names, rom_names,
 | 
			
		||||
                                              [((location.address, location.player), (location.item.code, location.item.player)) for location in world.get_filled_locations() if type(location.address) is int])
 | 
			
		||||
                                             ).encode("utf-8"))
 | 
			
		||||
        multidata = zlib.compress(json.dumps({"names": parsed_names,
 | 
			
		||||
                                              "roms": rom_names,
 | 
			
		||||
                                              "remote_items": [player for player in range(1, world.players + 1) if world.remote_items[player]],
 | 
			
		||||
                                              "locations": [((location.address, location.player), (location.item.code, location.item.player))
 | 
			
		||||
                                                            for location in world.get_filled_locations() if type(location.address) is int]
 | 
			
		||||
                                              }).encode("utf-8"))
 | 
			
		||||
        if args.jsonout:
 | 
			
		||||
            jsonout["multidata"] = list(multidata)
 | 
			
		||||
        else:
 | 
			
		||||
| 
						 | 
				
			
			@ -228,6 +233,7 @@ def copy_world(world):
 | 
			
		|||
    ret = World(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
 | 
			
		||||
    ret.teams = world.teams
 | 
			
		||||
    ret.player_names = copy.deepcopy(world.player_names)
 | 
			
		||||
    ret.remote_items = world.remote_items.copy()
 | 
			
		||||
    ret.required_medallions = world.required_medallions.copy()
 | 
			
		||||
    ret.swamp_patch_required = world.swamp_patch_required.copy()
 | 
			
		||||
    ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										198
									
								
								MultiClient.py
								
								
								
								
							
							
						
						
									
										198
									
								
								MultiClient.py
								
								
								
								
							| 
						 | 
				
			
			@ -16,26 +16,19 @@ while True:
 | 
			
		|||
        break
 | 
			
		||||
    except ImportError:
 | 
			
		||||
        aioconsole = None
 | 
			
		||||
        input('Required python module "aioconsole" not found, press enter to install it')
 | 
			
		||||
        print('Required python module "aioconsole" not found, press enter to install it')
 | 
			
		||||
        input()
 | 
			
		||||
        subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'aioconsole'])
 | 
			
		||||
 | 
			
		||||
websockets = None
 | 
			
		||||
 | 
			
		||||
while not websockets:
 | 
			
		||||
while True:
 | 
			
		||||
    try:
 | 
			
		||||
        import websockets
 | 
			
		||||
        break
 | 
			
		||||
    except ImportError:
 | 
			
		||||
        websockets = None
 | 
			
		||||
        input('Required python module "websockets" not found, press enter to install it')
 | 
			
		||||
        print('Required python module "websockets" not found, press enter to install it')
 | 
			
		||||
        input()
 | 
			
		||||
        subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'websockets'])
 | 
			
		||||
    else:
 | 
			
		||||
        version = websockets.__version__
 | 
			
		||||
        if type(version) == str:
 | 
			
		||||
            version = tuple(int(part) for part in version.split("."))
 | 
			
		||||
        if version < (8,1):
 | 
			
		||||
            websockets = None
 | 
			
		||||
            input('Required python module "websockets" too old, press enter to upgrade it')
 | 
			
		||||
            subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'websockets'])
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
    import colorama
 | 
			
		||||
| 
						 | 
				
			
			@ -54,6 +47,7 @@ class Context:
 | 
			
		|||
        self.server_address = server_address
 | 
			
		||||
 | 
			
		||||
        self.exit_event = asyncio.Event()
 | 
			
		||||
        self.watcher_event = asyncio.Event()
 | 
			
		||||
 | 
			
		||||
        self.input_queue = asyncio.Queue()
 | 
			
		||||
        self.input_requests = 0
 | 
			
		||||
| 
						 | 
				
			
			@ -75,7 +69,9 @@ class Context:
 | 
			
		|||
        self.slot = None
 | 
			
		||||
        self.player_names = {}
 | 
			
		||||
        self.locations_checked = set()
 | 
			
		||||
        self.locations_scouted = set()
 | 
			
		||||
        self.items_received = []
 | 
			
		||||
        self.locations_info = {}
 | 
			
		||||
        self.awaiting_rom = False
 | 
			
		||||
        self.rom = None
 | 
			
		||||
        self.auth = None
 | 
			
		||||
| 
						 | 
				
			
			@ -104,11 +100,15 @@ INGAME_MODES = {0x07, 0x09, 0x0b}
 | 
			
		|||
SAVEDATA_START = WRAM_START + 0xF000
 | 
			
		||||
SAVEDATA_SIZE = 0x500
 | 
			
		||||
 | 
			
		||||
RECV_PROGRESS_ADDR = SAVEDATA_START + 0x4D0     # 2 bytes
 | 
			
		||||
RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2         # 1 byte
 | 
			
		||||
RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3  # 1 byte
 | 
			
		||||
ROOMID_ADDR = SAVEDATA_START + 0x4D4            # 2 bytes
 | 
			
		||||
ROOMDATA_ADDR = SAVEDATA_START + 0x4D6          # 1 byte
 | 
			
		||||
RECV_PROGRESS_ADDR = SAVEDATA_START + 0x4D0         # 2 bytes
 | 
			
		||||
RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2             # 1 byte
 | 
			
		||||
RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3      # 1 byte
 | 
			
		||||
ROOMID_ADDR = SAVEDATA_START + 0x4D4                # 2 bytes
 | 
			
		||||
ROOMDATA_ADDR = SAVEDATA_START + 0x4D6              # 1 byte
 | 
			
		||||
SCOUT_LOCATION_ADDR = SAVEDATA_START + 0x4D7        # 1 byte
 | 
			
		||||
SCOUTREPLY_LOCATION_ADDR = SAVEDATA_START + 0x4D8   # 1 byte
 | 
			
		||||
SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9       # 1 byte
 | 
			
		||||
SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA     # 1 byte
 | 
			
		||||
 | 
			
		||||
location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
 | 
			
		||||
                     "Blind's Hideout - Left": (0x11d, 0x20),
 | 
			
		||||
| 
						 | 
				
			
			@ -334,7 +334,7 @@ SNES_ATTACHED = 3
 | 
			
		|||
 | 
			
		||||
async def snes_connect(ctx : Context, address):
 | 
			
		||||
    if ctx.snes_socket is not None:
 | 
			
		||||
        print('Already connected to snes')
 | 
			
		||||
        logging.error('Already connected to snes')
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    ctx.snes_state = SNES_CONNECTING
 | 
			
		||||
| 
						 | 
				
			
			@ -342,7 +342,7 @@ async def snes_connect(ctx : Context, address):
 | 
			
		|||
 | 
			
		||||
    address = f"ws://{address}" if "://" not in address else address
 | 
			
		||||
 | 
			
		||||
    print("Connecting to QUsb2snes at %s ..." % address)
 | 
			
		||||
    logging.info("Connecting to QUsb2snes at %s ..." % address)
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        ctx.snes_socket = await websockets.connect(address, ping_timeout=None, ping_interval=None)
 | 
			
		||||
| 
						 | 
				
			
			@ -360,9 +360,9 @@ async def snes_connect(ctx : Context, address):
 | 
			
		|||
        if not devices:
 | 
			
		||||
            raise Exception('No device found')
 | 
			
		||||
 | 
			
		||||
        print("Available devices:")
 | 
			
		||||
        logging.info("Available devices:")
 | 
			
		||||
        for id, device in enumerate(devices):
 | 
			
		||||
            print("[%d] %s" % (id + 1, device))
 | 
			
		||||
            logging.info("[%d] %s" % (id + 1, device))
 | 
			
		||||
 | 
			
		||||
        device = None
 | 
			
		||||
        if len(devices) == 1:
 | 
			
		||||
| 
						 | 
				
			
			@ -374,18 +374,18 @@ async def snes_connect(ctx : Context, address):
 | 
			
		|||
                device = devices[ctx.snes_attached_device[0]]
 | 
			
		||||
        else:
 | 
			
		||||
            while True:
 | 
			
		||||
                print("Select a device:")
 | 
			
		||||
                logging.info("Select a device:")
 | 
			
		||||
                choice = await console_input(ctx)
 | 
			
		||||
                if choice is None:
 | 
			
		||||
                    raise Exception('Abort input')
 | 
			
		||||
                if not choice.isdigit() or int(choice) < 1 or int(choice) > len(devices):
 | 
			
		||||
                    print("Invalid choice (%s)" % choice)
 | 
			
		||||
                    logging.warning("Invalid choice (%s)" % choice)
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                device = devices[int(choice) - 1]
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
        print("Attaching to " + device)
 | 
			
		||||
        logging.info("Attaching to " + device)
 | 
			
		||||
 | 
			
		||||
        Attach_Request = {
 | 
			
		||||
            "Opcode" : "Attach",
 | 
			
		||||
| 
						 | 
				
			
			@ -397,12 +397,12 @@ async def snes_connect(ctx : Context, address):
 | 
			
		|||
        ctx.snes_attached_device = (devices.index(device), device)
 | 
			
		||||
 | 
			
		||||
        if 'SD2SNES'.lower() in device.lower() or (len(device) == 4 and device[:3] == 'COM'):
 | 
			
		||||
            print("SD2SNES Detected")
 | 
			
		||||
            logging.info("SD2SNES Detected")
 | 
			
		||||
            ctx.is_sd2snes = True
 | 
			
		||||
            await ctx.snes_socket.send(json.dumps({"Opcode" : "Info", "Space" : "SNES"}))
 | 
			
		||||
            reply = json.loads(await ctx.snes_socket.recv())
 | 
			
		||||
            if reply and 'Results' in reply:
 | 
			
		||||
                print(reply['Results'])
 | 
			
		||||
                logging.info(reply['Results'])
 | 
			
		||||
        else:
 | 
			
		||||
            ctx.is_sd2snes = False
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -420,9 +420,9 @@ async def snes_connect(ctx : Context, address):
 | 
			
		|||
                ctx.snes_socket = None
 | 
			
		||||
            ctx.snes_state = SNES_DISCONNECTED
 | 
			
		||||
        if not ctx.snes_reconnect_address:
 | 
			
		||||
            print("Error connecting to snes (%s)" % e)
 | 
			
		||||
            logging.error("Error connecting to snes (%s)" % e)
 | 
			
		||||
        else:
 | 
			
		||||
            print(f"Error connecting to snes, attempt again in {RECONNECT_DELAY}s")
 | 
			
		||||
            logging.error(f"Error connecting to snes, attempt again in {RECONNECT_DELAY}s")
 | 
			
		||||
            asyncio.create_task(snes_autoreconnect(ctx))
 | 
			
		||||
 | 
			
		||||
async def snes_autoreconnect(ctx: Context):
 | 
			
		||||
| 
						 | 
				
			
			@ -434,11 +434,11 @@ async def snes_recv_loop(ctx : Context):
 | 
			
		|||
    try:
 | 
			
		||||
        async for msg in ctx.snes_socket:
 | 
			
		||||
            ctx.snes_recv_queue.put_nowait(msg)
 | 
			
		||||
        print("Snes disconnected")
 | 
			
		||||
        logging.warning("Snes disconnected")
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        if not isinstance(e, websockets.WebSocketException):
 | 
			
		||||
            logging.exception(e)
 | 
			
		||||
        print("Lost connection to the snes, type /snes to reconnect")
 | 
			
		||||
        logging.error("Lost connection to the snes, type /snes to reconnect")
 | 
			
		||||
    finally:
 | 
			
		||||
        socket, ctx.snes_socket = ctx.snes_socket, None
 | 
			
		||||
        if socket is not None and not socket.closed:
 | 
			
		||||
| 
						 | 
				
			
			@ -451,7 +451,7 @@ async def snes_recv_loop(ctx : Context):
 | 
			
		|||
        ctx.rom = None
 | 
			
		||||
 | 
			
		||||
        if ctx.snes_reconnect_address:
 | 
			
		||||
            print(f"...reconnecting in {RECONNECT_DELAY}s")
 | 
			
		||||
            logging.info(f"...reconnecting in {RECONNECT_DELAY}s")
 | 
			
		||||
            asyncio.create_task(snes_autoreconnect(ctx))
 | 
			
		||||
 | 
			
		||||
async def snes_read(ctx : Context, address, size):
 | 
			
		||||
| 
						 | 
				
			
			@ -479,9 +479,9 @@ async def snes_read(ctx : Context, address, size):
 | 
			
		|||
                break
 | 
			
		||||
 | 
			
		||||
        if len(data) != size:
 | 
			
		||||
            print('Error reading %s, requested %d bytes, received %d' % (hex(address), size, len(data)))
 | 
			
		||||
            logging.error('Error reading %s, requested %d bytes, received %d' % (hex(address), size, len(data)))
 | 
			
		||||
            if len(data):
 | 
			
		||||
                print(str(data))
 | 
			
		||||
                logging.error(str(data))
 | 
			
		||||
            if ctx.snes_socket is not None and not ctx.snes_socket.closed:
 | 
			
		||||
                await ctx.snes_socket.close()
 | 
			
		||||
            return None
 | 
			
		||||
| 
						 | 
				
			
			@ -507,7 +507,7 @@ async def snes_write(ctx : Context, write_list):
 | 
			
		|||
 | 
			
		||||
            for address, data in write_list:
 | 
			
		||||
                if (address < WRAM_START) or ((address + len(data)) > (WRAM_START + WRAM_SIZE)):
 | 
			
		||||
                    print("SD2SNES: Write out of range %s (%d)" % (hex(address), len(data)))
 | 
			
		||||
                    logging.error("SD2SNES: Write out of range %s (%d)" % (hex(address), len(data)))
 | 
			
		||||
                    return False
 | 
			
		||||
                for ptr, byte in enumerate(data, address + 0x7E0000 - WRAM_START):
 | 
			
		||||
                    cmd += b'\xA9' # LDA
 | 
			
		||||
| 
						 | 
				
			
			@ -566,48 +566,49 @@ async def send_msgs(websocket, msgs):
 | 
			
		|||
 | 
			
		||||
async def server_loop(ctx : Context, address = None):
 | 
			
		||||
    if ctx.socket is not None:
 | 
			
		||||
        print('Already connected')
 | 
			
		||||
        logging.error('Already connected')
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if address is None:
 | 
			
		||||
        address = ctx.server_address
 | 
			
		||||
 | 
			
		||||
    while not address:
 | 
			
		||||
        print('Enter multiworld server address')
 | 
			
		||||
        logging.info('Enter multiworld server address')
 | 
			
		||||
        address = await console_input(ctx)
 | 
			
		||||
 | 
			
		||||
    address = f"ws://{address}" if "://" not in address else address
 | 
			
		||||
    port = urllib.parse.urlparse(address).port or 38281
 | 
			
		||||
 | 
			
		||||
    print('Connecting to multiworld server at %s' % address)
 | 
			
		||||
    logging.info('Connecting to multiworld server at %s' % address)
 | 
			
		||||
    try:
 | 
			
		||||
        ctx.socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None)
 | 
			
		||||
        print('Connected')
 | 
			
		||||
        logging.info('Connected')
 | 
			
		||||
        ctx.server_address = address
 | 
			
		||||
 | 
			
		||||
        async for data in ctx.socket:
 | 
			
		||||
            for msg in json.loads(data):
 | 
			
		||||
                cmd, args = (msg[0], msg[1]) if len(msg) > 1 else (msg, None)
 | 
			
		||||
                await process_server_cmd(ctx, cmd, args)
 | 
			
		||||
        print('Disconnected from multiworld server, type /connect to reconnect')
 | 
			
		||||
        logging.warning('Disconnected from multiworld server, type /connect to reconnect')
 | 
			
		||||
    except ConnectionRefusedError:
 | 
			
		||||
        print('Connection refused by the multiworld server')
 | 
			
		||||
        logging.error('Connection refused by the multiworld server')
 | 
			
		||||
    except (OSError, websockets.InvalidURI):
 | 
			
		||||
        print('Failed to connect to the multiworld server')
 | 
			
		||||
        logging.error('Failed to connect to the multiworld server')
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        print('Lost connection to the multiworld server, type /connect to reconnect')
 | 
			
		||||
        logging.error('Lost connection to the multiworld server, type /connect to reconnect')
 | 
			
		||||
        if not isinstance(e, websockets.WebSocketException):
 | 
			
		||||
            logging.exception(e)
 | 
			
		||||
    finally:
 | 
			
		||||
        ctx.awaiting_rom = False
 | 
			
		||||
        ctx.auth = None
 | 
			
		||||
        ctx.items_received = []
 | 
			
		||||
        ctx.locations_info = {}
 | 
			
		||||
        socket, ctx.socket = ctx.socket, None
 | 
			
		||||
        if socket is not None and not socket.closed:
 | 
			
		||||
            await socket.close()
 | 
			
		||||
        ctx.server_task = None
 | 
			
		||||
        if ctx.server_address:
 | 
			
		||||
            print(f"... reconnecting in {RECONNECT_DELAY}s")
 | 
			
		||||
            logging.info(f"... reconnecting in {RECONNECT_DELAY}s")
 | 
			
		||||
            asyncio.create_task(server_autoreconnect(ctx))
 | 
			
		||||
 | 
			
		||||
async def server_autoreconnect(ctx: Context):
 | 
			
		||||
| 
						 | 
				
			
			@ -617,28 +618,28 @@ async def server_autoreconnect(ctx: Context):
 | 
			
		|||
 | 
			
		||||
async def process_server_cmd(ctx : Context, cmd, args):
 | 
			
		||||
    if cmd == 'RoomInfo':
 | 
			
		||||
        print('--------------------------------')
 | 
			
		||||
        print('Room Information:')
 | 
			
		||||
        print('--------------------------------')
 | 
			
		||||
        logging.info('--------------------------------')
 | 
			
		||||
        logging.info('Room Information:')
 | 
			
		||||
        logging.info('--------------------------------')
 | 
			
		||||
        if args['password']:
 | 
			
		||||
            print('Password required')
 | 
			
		||||
            logging.info('Password required')
 | 
			
		||||
        if len(args['players']) < 1:
 | 
			
		||||
            print('No player connected')
 | 
			
		||||
            logging.info('No player connected')
 | 
			
		||||
        else:
 | 
			
		||||
            args['players'].sort()
 | 
			
		||||
            current_team = 0
 | 
			
		||||
            print('Connected players:')
 | 
			
		||||
            print('  Team #1')
 | 
			
		||||
            logging.info('Connected players:')
 | 
			
		||||
            logging.info('  Team #1')
 | 
			
		||||
            for team, slot, name in args['players']:
 | 
			
		||||
                if team != current_team:
 | 
			
		||||
                    print(f'  Team #{team + 1}')
 | 
			
		||||
                    logging.info(f'  Team #{team + 1}')
 | 
			
		||||
                    current_team = team
 | 
			
		||||
                print('    %s (Player %d)' % (name, slot))
 | 
			
		||||
                logging.info('    %s (Player %d)' % (name, slot))
 | 
			
		||||
        await server_auth(ctx, args['password'])
 | 
			
		||||
 | 
			
		||||
    if cmd == 'ConnectionRefused':
 | 
			
		||||
        if 'InvalidPassword' in args:
 | 
			
		||||
            print('Invalid password')
 | 
			
		||||
            logging.error('Invalid password')
 | 
			
		||||
            ctx.password = None
 | 
			
		||||
            await server_auth(ctx, True)
 | 
			
		||||
        if 'InvalidRom' in args:
 | 
			
		||||
| 
						 | 
				
			
			@ -650,8 +651,13 @@ async def process_server_cmd(ctx : Context, cmd, args):
 | 
			
		|||
    if cmd == 'Connected':
 | 
			
		||||
        ctx.team, ctx.slot = args[0]
 | 
			
		||||
        ctx.player_names = {p: n for p, n in args[1]}
 | 
			
		||||
        msgs = []
 | 
			
		||||
        if ctx.locations_checked:
 | 
			
		||||
            await send_msgs(ctx.socket, [['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]]])
 | 
			
		||||
            msgs.append(['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]])
 | 
			
		||||
        if ctx.locations_scouted:
 | 
			
		||||
            msgs.append(['LocationScouts', list(ctx.locations_scouted)])
 | 
			
		||||
        if msgs:
 | 
			
		||||
            await send_msgs(ctx.socket, msgs)
 | 
			
		||||
 | 
			
		||||
    if cmd == 'ReceivedItems':
 | 
			
		||||
        start_index, items = args
 | 
			
		||||
| 
						 | 
				
			
			@ -665,24 +671,34 @@ async def process_server_cmd(ctx : Context, cmd, args):
 | 
			
		|||
        if start_index == len(ctx.items_received):
 | 
			
		||||
            for item in items:
 | 
			
		||||
                ctx.items_received.append(ReceivedItem(*item))
 | 
			
		||||
        ctx.watcher_event.set()
 | 
			
		||||
 | 
			
		||||
    if cmd == 'LocationInfo':
 | 
			
		||||
        for location, item, player in args:
 | 
			
		||||
            if location not in ctx.locations_info:
 | 
			
		||||
                replacements = {0xA2: 'Small Key', 0x9D: 'Big Key', 0x8D: 'Compass', 0x7D: 'Map'}
 | 
			
		||||
                item_name = replacements.get(item, get_item_name_from_id(item))
 | 
			
		||||
                logging.info(f"Saw {color(item_name, 'red', 'bold')} at {list(Regions.location_table.keys())[location - 1]}")
 | 
			
		||||
                ctx.locations_info[location] = (item, player)
 | 
			
		||||
        ctx.watcher_event.set()
 | 
			
		||||
 | 
			
		||||
    if cmd == 'ItemSent':
 | 
			
		||||
        player_sent, location, player_recvd, item = args
 | 
			
		||||
        item = color(get_item_name_from_id(item), 'cyan' if player_sent != ctx.slot else 'green')
 | 
			
		||||
        player_sent = color(ctx.player_names[player_sent], 'yellow' if player_sent != ctx.slot else 'magenta')
 | 
			
		||||
        player_recvd = color(ctx.player_names[player_recvd], 'yellow' if player_recvd != ctx.slot else 'magenta')
 | 
			
		||||
        print('%s sent %s to %s (%s)' % (player_sent, item, player_recvd, get_location_name_from_address(location)))
 | 
			
		||||
        logging.info('%s sent %s to %s (%s)' % (player_sent, item, player_recvd, get_location_name_from_address(location)))
 | 
			
		||||
 | 
			
		||||
    if cmd == 'Print':
 | 
			
		||||
        print(args)
 | 
			
		||||
        logging.info(args)
 | 
			
		||||
 | 
			
		||||
async def server_auth(ctx : Context, password_requested):
 | 
			
		||||
    if password_requested and not ctx.password:
 | 
			
		||||
        print('Enter the password required to join this game:')
 | 
			
		||||
        logging.info('Enter the password required to join this game:')
 | 
			
		||||
        ctx.password = await console_input(ctx)
 | 
			
		||||
    if ctx.rom is None:
 | 
			
		||||
        ctx.awaiting_rom = True
 | 
			
		||||
        print('No ROM detected, awaiting snes connection to authenticate to the multiworld server')
 | 
			
		||||
        logging.info('No ROM detected, awaiting snes connection to authenticate to the multiworld server')
 | 
			
		||||
        return
 | 
			
		||||
    ctx.awaiting_rom = False
 | 
			
		||||
    ctx.auth = ctx.rom.copy()
 | 
			
		||||
| 
						 | 
				
			
			@ -718,12 +734,6 @@ async def console_loop(ctx : Context):
 | 
			
		|||
        if command[0] == '/exit':
 | 
			
		||||
            ctx.exit_event.set()
 | 
			
		||||
 | 
			
		||||
        if command[0] == '/installcolors' and 'colorama' not in sys.modules:
 | 
			
		||||
            subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'colorama'])
 | 
			
		||||
            global colorama
 | 
			
		||||
            import colorama
 | 
			
		||||
            colorama.init()
 | 
			
		||||
 | 
			
		||||
        if command[0] == '/snes':
 | 
			
		||||
            ctx.snes_reconnect_address = None
 | 
			
		||||
            asyncio.create_task(snes_connect(ctx, command[1] if len(command) > 1 else ctx.snes_address))
 | 
			
		||||
| 
						 | 
				
			
			@ -742,25 +752,25 @@ async def console_loop(ctx : Context):
 | 
			
		|||
            asyncio.create_task(send_msgs(ctx.socket, [['Say', input]]))
 | 
			
		||||
 | 
			
		||||
        if command[0] == '/received':
 | 
			
		||||
            print('Received items:')
 | 
			
		||||
            logging.info('Received items:')
 | 
			
		||||
            for index, item in enumerate(ctx.items_received, 1):
 | 
			
		||||
                print('%s from %s (%s) (%d/%d in list)' % (
 | 
			
		||||
                logging.info('%s from %s (%s) (%d/%d in list)' % (
 | 
			
		||||
                    color(get_item_name_from_id(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
 | 
			
		||||
                    get_location_name_from_address(item.location), index, len(ctx.items_received)))
 | 
			
		||||
 | 
			
		||||
        if command[0] == '/missing':
 | 
			
		||||
            for location in [k for k, v in Regions.location_table.items() if type(v[0]) is int]:
 | 
			
		||||
                if location not in ctx.locations_checked:
 | 
			
		||||
                    print('Missing: ' + location)
 | 
			
		||||
                    logging.info('Missing: ' + location)
 | 
			
		||||
        if command[0] == '/getitem' and len(command) > 1:
 | 
			
		||||
            item = input[9:]
 | 
			
		||||
            item_id = Items.item_table[item][3] if item in Items.item_table else None
 | 
			
		||||
            if type(item_id) is int and item_id in range(0x100):
 | 
			
		||||
                print('Sending item: ' + item)
 | 
			
		||||
                logging.info('Sending item: ' + item)
 | 
			
		||||
                snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item_id]))
 | 
			
		||||
                snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([0]))
 | 
			
		||||
            else:
 | 
			
		||||
                print('Invalid item: ' + item)
 | 
			
		||||
                logging.info('Invalid item: ' + item)
 | 
			
		||||
 | 
			
		||||
        await snes_flush_writes(ctx)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -779,7 +789,7 @@ async def track_locations(ctx : Context, roomid, roomdata):
 | 
			
		|||
    new_locations = []
 | 
			
		||||
    def new_check(location):
 | 
			
		||||
        ctx.locations_checked.add(location)
 | 
			
		||||
        print("New check: %s (%d/216)" % (location, len(ctx.locations_checked)))
 | 
			
		||||
        logging.info("New check: %s (%d/216)" % (location, len(ctx.locations_checked)))
 | 
			
		||||
        new_locations.append(Regions.location_table[location][0])
 | 
			
		||||
 | 
			
		||||
    for location, (loc_roomid, loc_mask) in location_table_uw.items():
 | 
			
		||||
| 
						 | 
				
			
			@ -838,7 +848,11 @@ async def track_locations(ctx : Context, roomid, roomdata):
 | 
			
		|||
 | 
			
		||||
async def game_watcher(ctx : Context):
 | 
			
		||||
    while not ctx.exit_event.is_set():
 | 
			
		||||
        await asyncio.sleep(2)
 | 
			
		||||
        try:
 | 
			
		||||
            await asyncio.wait_for(ctx.watcher_event.wait(), 2)
 | 
			
		||||
        except asyncio.TimeoutError:
 | 
			
		||||
            pass
 | 
			
		||||
        ctx.watcher_event.clear()
 | 
			
		||||
 | 
			
		||||
        if not ctx.rom:
 | 
			
		||||
            rom = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
 | 
			
		||||
| 
						 | 
				
			
			@ -847,50 +861,64 @@ async def game_watcher(ctx : Context):
 | 
			
		|||
 | 
			
		||||
            ctx.rom = list(rom)
 | 
			
		||||
            ctx.locations_checked = set()
 | 
			
		||||
            ctx.locations_scouted = set()
 | 
			
		||||
            if ctx.awaiting_rom:
 | 
			
		||||
                await server_auth(ctx, False)
 | 
			
		||||
 | 
			
		||||
        if ctx.auth and ctx.auth != ctx.rom:
 | 
			
		||||
            print("ROM change detected, please reconnect to the multiworld server")
 | 
			
		||||
            logging.warning("ROM change detected, please reconnect to the multiworld server")
 | 
			
		||||
            await disconnect(ctx)
 | 
			
		||||
 | 
			
		||||
        gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
 | 
			
		||||
        if gamemode is None or gamemode[0] not in INGAME_MODES:
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        data = await snes_read(ctx, RECV_PROGRESS_ADDR, 7)
 | 
			
		||||
        data = await snes_read(ctx, RECV_PROGRESS_ADDR, 8)
 | 
			
		||||
        if data is None:
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        recv_index = data[0] | (data[1] << 8)
 | 
			
		||||
        assert(RECV_ITEM_ADDR == RECV_PROGRESS_ADDR + 2)
 | 
			
		||||
        assert RECV_ITEM_ADDR == RECV_PROGRESS_ADDR + 2
 | 
			
		||||
        recv_item = data[2]
 | 
			
		||||
        assert(ROOMID_ADDR == RECV_PROGRESS_ADDR + 4)
 | 
			
		||||
        assert ROOMID_ADDR == RECV_PROGRESS_ADDR + 4
 | 
			
		||||
        roomid = data[4] | (data[5] << 8)
 | 
			
		||||
        assert(ROOMDATA_ADDR == RECV_PROGRESS_ADDR + 6)
 | 
			
		||||
        assert ROOMDATA_ADDR == RECV_PROGRESS_ADDR + 6
 | 
			
		||||
        roomdata = data[6]
 | 
			
		||||
 | 
			
		||||
        await track_locations(ctx, roomid, roomdata)
 | 
			
		||||
        assert SCOUT_LOCATION_ADDR == RECV_PROGRESS_ADDR + 7
 | 
			
		||||
        scout_location = data[7]
 | 
			
		||||
 | 
			
		||||
        if recv_index < len(ctx.items_received) and recv_item == 0:
 | 
			
		||||
            item = ctx.items_received[recv_index]
 | 
			
		||||
            print('Received %s from %s (%s) (%d/%d in list)' % (
 | 
			
		||||
            logging.info('Received %s from %s (%s) (%d/%d in list)' % (
 | 
			
		||||
                color(get_item_name_from_id(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
 | 
			
		||||
                get_location_name_from_address(item.location), recv_index + 1, len(ctx.items_received)))
 | 
			
		||||
            recv_index += 1
 | 
			
		||||
            snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
 | 
			
		||||
            snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item.item]))
 | 
			
		||||
            snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([item.player]))
 | 
			
		||||
            snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([item.player if item.player != ctx.slot else 0]))
 | 
			
		||||
        if scout_location > 0 and scout_location in ctx.locations_info:
 | 
			
		||||
            snes_buffered_write(ctx, SCOUTREPLY_LOCATION_ADDR, bytes([scout_location]))
 | 
			
		||||
            snes_buffered_write(ctx, SCOUTREPLY_ITEM_ADDR, bytes([ctx.locations_info[scout_location][0]]))
 | 
			
		||||
            snes_buffered_write(ctx, SCOUTREPLY_PLAYER_ADDR, bytes([ctx.locations_info[scout_location][1]]))
 | 
			
		||||
 | 
			
		||||
        await snes_flush_writes(ctx)
 | 
			
		||||
 | 
			
		||||
        if scout_location > 0 and scout_location not in ctx.locations_scouted:
 | 
			
		||||
            ctx.locations_scouted.add(scout_location)
 | 
			
		||||
            logging.info(f'Scouting item at {list(Regions.location_table.keys())[scout_location - 1]}')
 | 
			
		||||
            await send_msgs(ctx.socket, [['LocationScouts', [scout_location]]])
 | 
			
		||||
        await track_locations(ctx, roomid, roomdata)
 | 
			
		||||
 | 
			
		||||
async def main():
 | 
			
		||||
    parser = argparse.ArgumentParser()
 | 
			
		||||
    parser.add_argument('--snes', default='localhost:8080', help='Address of the QUsb2snes server.')
 | 
			
		||||
    parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
 | 
			
		||||
    parser.add_argument('--password', default=None, help='Password of the multiworld host.')
 | 
			
		||||
    parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
 | 
			
		||||
    args = parser.parse_args()
 | 
			
		||||
 | 
			
		||||
    logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
 | 
			
		||||
 | 
			
		||||
    ctx = Context(args.snes, args.connect, args.password)
 | 
			
		||||
 | 
			
		||||
    input_task = asyncio.create_task(console_loop(ctx))
 | 
			
		||||
| 
						 | 
				
			
			@ -924,13 +952,9 @@ async def main():
 | 
			
		|||
    await input_task
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    if 'colorama' in sys.modules:
 | 
			
		||||
        colorama.init()
 | 
			
		||||
 | 
			
		||||
    colorama.init()
 | 
			
		||||
    loop = asyncio.get_event_loop()
 | 
			
		||||
    loop.run_until_complete(main())
 | 
			
		||||
    loop.run_until_complete(asyncio.gather(*asyncio.Task.all_tasks()))
 | 
			
		||||
    loop.close()
 | 
			
		||||
 | 
			
		||||
    if 'colorama' in sys.modules:
 | 
			
		||||
        colorama.deinit()
 | 
			
		||||
    colorama.deinit()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,6 +30,7 @@ class Context:
 | 
			
		|||
        self.disable_save = False
 | 
			
		||||
        self.player_names = {}
 | 
			
		||||
        self.rom_names = {}
 | 
			
		||||
        self.remote_items = set()
 | 
			
		||||
        self.locations = {}
 | 
			
		||||
        self.host = host
 | 
			
		||||
        self.port = port
 | 
			
		||||
| 
						 | 
				
			
			@ -58,17 +59,17 @@ def broadcast_team(ctx : Context, team, msgs):
 | 
			
		|||
            asyncio.create_task(send_msgs(client.socket, msgs))
 | 
			
		||||
 | 
			
		||||
def notify_all(ctx : Context, text):
 | 
			
		||||
    print("Notice (all): %s" % text)
 | 
			
		||||
    logging.info("Notice (all): %s" % text)
 | 
			
		||||
    broadcast_all(ctx, [['Print', text]])
 | 
			
		||||
 | 
			
		||||
def notify_team(ctx : Context, team : int, text : str):
 | 
			
		||||
    print("Notice (Team #%d): %s" % (team+1, text))
 | 
			
		||||
    logging.info("Notice (Team #%d): %s" % (team+1, text))
 | 
			
		||||
    broadcast_team(ctx, team, [['Print', text]])
 | 
			
		||||
 | 
			
		||||
def notify_client(client : Client, text : str):
 | 
			
		||||
    if not client.auth:
 | 
			
		||||
        return
 | 
			
		||||
    print("Notice (Player %s in team %d): %s" % (client.name, client.team+1, text))
 | 
			
		||||
    logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team+1, text))
 | 
			
		||||
    asyncio.create_task(send_msgs(client.socket,  [['Print', text]]))
 | 
			
		||||
 | 
			
		||||
async def server(websocket, path, ctx : Context):
 | 
			
		||||
| 
						 | 
				
			
			@ -162,7 +163,7 @@ def register_location_checks(ctx : Context, team, slot, locations):
 | 
			
		|||
    for location in locations:
 | 
			
		||||
        if (location, slot) in ctx.locations:
 | 
			
		||||
            target_item, target_player = ctx.locations[(location, slot)]
 | 
			
		||||
            if target_player != slot:
 | 
			
		||||
            if target_player != slot or slot in ctx.remote_items:
 | 
			
		||||
                found = False
 | 
			
		||||
                recvd_items = get_received_items(ctx, team, target_player)
 | 
			
		||||
                for recvd_item in recvd_items:
 | 
			
		||||
| 
						 | 
				
			
			@ -172,8 +173,9 @@ def register_location_checks(ctx : Context, team, slot, locations):
 | 
			
		|||
                if not found:
 | 
			
		||||
                    new_item = ReceivedItem(target_item, location, slot)
 | 
			
		||||
                    recvd_items.append(new_item)
 | 
			
		||||
                    broadcast_team(ctx, team, [['ItemSent', (slot, location, target_player, target_item)]])
 | 
			
		||||
                    print('(Team #%d) %s sent %s to %s (%s)' % (team, ctx.player_names[(team, slot)], get_item_name_from_id(target_item), ctx.player_names[(team, target_player)], get_location_name_from_address(location)))
 | 
			
		||||
                    if slot != target_player:
 | 
			
		||||
                        broadcast_team(ctx, team, [['ItemSent', (slot, location, target_player, target_item)]])
 | 
			
		||||
                    logging.info('(Team #%d) %s sent %s to %s (%s)' % (team+1, ctx.player_names[(team, slot)], get_item_name_from_id(target_item), ctx.player_names[(team, target_player)], get_location_name_from_address(location)))
 | 
			
		||||
                    found_items = True
 | 
			
		||||
    send_new_items(ctx)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -232,7 +234,7 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
 | 
			
		|||
        items = get_received_items(ctx, client.team, client.slot)
 | 
			
		||||
        if items:
 | 
			
		||||
            client.send_index = len(items)
 | 
			
		||||
            await send_msgs(client.socket, ['ReceivedItems', (0, tuplize_received_items(items))])
 | 
			
		||||
            await send_msgs(client.socket, [['ReceivedItems', (0, tuplize_received_items(items))]])
 | 
			
		||||
 | 
			
		||||
    if cmd == 'LocationChecks':
 | 
			
		||||
        if type(args) is not list:
 | 
			
		||||
| 
						 | 
				
			
			@ -240,6 +242,28 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
 | 
			
		|||
            return
 | 
			
		||||
        register_location_checks(ctx, client.team, client.slot, args)
 | 
			
		||||
 | 
			
		||||
    if cmd == 'LocationScouts':
 | 
			
		||||
        if type(args) is not list:
 | 
			
		||||
            await send_msgs(client.socket, [['InvalidArguments', 'LocationScouts']])
 | 
			
		||||
            return
 | 
			
		||||
        locs = []
 | 
			
		||||
        for location in args:
 | 
			
		||||
            if type(location) is not int or 0 >= location > len(Regions.location_table):
 | 
			
		||||
                await send_msgs(client.socket, [['InvalidArguments', 'LocationScouts']])
 | 
			
		||||
                return
 | 
			
		||||
            loc_name = list(Regions.location_table.keys())[location - 1]
 | 
			
		||||
            target_item, target_player = ctx.locations[(Regions.location_table[loc_name][0], client.slot)]
 | 
			
		||||
 | 
			
		||||
            replacements = {'SmallKey': 0xA2, 'BigKey': 0x9D, 'Compass': 0x8D, 'Map': 0x7D}
 | 
			
		||||
            item_type = [i[2] for i in Items.item_table.values() if type(i[3]) is int and i[3] == target_item]
 | 
			
		||||
            if item_type:
 | 
			
		||||
                target_item = replacements.get(item_type[0], target_item)
 | 
			
		||||
 | 
			
		||||
            locs.append([loc_name, location, target_item, target_player])
 | 
			
		||||
 | 
			
		||||
        logging.info(f"{client.name} in team {client.team+1} scouted {', '.join([l[0] for l in locs])}")
 | 
			
		||||
        await send_msgs(client.socket, [['LocationInfo', [l[1:] for l in locs]]])
 | 
			
		||||
 | 
			
		||||
    if cmd == 'Say':
 | 
			
		||||
        if type(args) is not str or not args.isprintable():
 | 
			
		||||
            await send_msgs(client.socket, [['InvalidArguments', 'Say']])
 | 
			
		||||
| 
						 | 
				
			
			@ -260,7 +284,7 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
 | 
			
		|||
 | 
			
		||||
def set_password(ctx : Context, password):
 | 
			
		||||
    ctx.password = password
 | 
			
		||||
    print('Password set to ' + password if password is not None else 'Password disabled')
 | 
			
		||||
    logging.warning('Password set to ' + password if password is not None else 'Password disabled')
 | 
			
		||||
 | 
			
		||||
async def console(ctx : Context):
 | 
			
		||||
    while True:
 | 
			
		||||
| 
						 | 
				
			
			@ -276,7 +300,7 @@ async def console(ctx : Context):
 | 
			
		|||
                break
 | 
			
		||||
 | 
			
		||||
            if command[0] == '/players':
 | 
			
		||||
                print(get_connected_players_string(ctx))
 | 
			
		||||
                logging.info(get_connected_players_string(ctx))
 | 
			
		||||
            if command[0] == '/password':
 | 
			
		||||
                set_password(ctx, command[1] if len(command) > 1 else None)
 | 
			
		||||
            if command[0] == '/kick' and len(command) > 1:
 | 
			
		||||
| 
						 | 
				
			
			@ -310,7 +334,7 @@ async def console(ctx : Context):
 | 
			
		|||
                            notify_all(ctx, 'Cheat console: sending "' + item + '" to ' + client.name)
 | 
			
		||||
                    send_new_items(ctx)
 | 
			
		||||
                else:
 | 
			
		||||
                    print("Unknown item: " + item)
 | 
			
		||||
                    logging.warning("Unknown item: " + item)
 | 
			
		||||
            if command[0] == '/hint':
 | 
			
		||||
                for (team,slot), name in ctx.player_names.items():
 | 
			
		||||
                    if len(command) == 1:
 | 
			
		||||
| 
						 | 
				
			
			@ -328,7 +352,7 @@ async def console(ctx : Context):
 | 
			
		|||
                                           f"{get_location_name_from_address(location_id)} in {name_finder}'s World"
 | 
			
		||||
                                    notify_team(ctx, team, hint)
 | 
			
		||||
                        else:
 | 
			
		||||
                            print("Unknown item: " + item)
 | 
			
		||||
                            logging.warning("Unknown item: " + item)
 | 
			
		||||
            if command[0][0] != '/':
 | 
			
		||||
                notify_all(ctx, '[Server]: ' + input)
 | 
			
		||||
        except:
 | 
			
		||||
| 
						 | 
				
			
			@ -343,8 +367,11 @@ async def main():
 | 
			
		|||
    parser.add_argument('--multidata', default=None)
 | 
			
		||||
    parser.add_argument('--savefile', default=None)
 | 
			
		||||
    parser.add_argument('--disable_save', default=False, action='store_true')
 | 
			
		||||
    parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
 | 
			
		||||
    args = parser.parse_args()
 | 
			
		||||
 | 
			
		||||
    logging.basicConfig(format='[%(asctime)s] %(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
 | 
			
		||||
 | 
			
		||||
    ctx = Context(args.host, args.port, args.password)
 | 
			
		||||
 | 
			
		||||
    ctx.data_filename = args.multidata
 | 
			
		||||
| 
						 | 
				
			
			@ -359,17 +386,18 @@ async def main():
 | 
			
		|||
 | 
			
		||||
        with open(ctx.data_filename, 'rb') as f:
 | 
			
		||||
            jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
 | 
			
		||||
            for team, names in enumerate(jsonobj[0]):
 | 
			
		||||
            for team, names in enumerate(jsonobj['names']):
 | 
			
		||||
                for player, name in enumerate(names, 1):
 | 
			
		||||
                    ctx.player_names[(team, player)] = name
 | 
			
		||||
            ctx.rom_names = {tuple(rom): (team, slot) for slot, team, rom in jsonobj[1]}
 | 
			
		||||
            ctx.locations = {tuple(k): tuple(v) for k, v in jsonobj[2]}
 | 
			
		||||
            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']}
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        print('Failed to read multiworld data (%s)' % e)
 | 
			
		||||
        logging.error('Failed to read multiworld data (%s)' % e)
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8') if not ctx.host else ctx.host
 | 
			
		||||
    print('Hosting game at %s:%d (%s)' % (ip, ctx.port, 'No password' if not ctx.password else 'Password: %s' % ctx.password))
 | 
			
		||||
    logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port, 'No password' if not ctx.password else 'Password: %s' % ctx.password))
 | 
			
		||||
 | 
			
		||||
    ctx.disable_save = args.disable_save
 | 
			
		||||
    if not ctx.disable_save:
 | 
			
		||||
| 
						 | 
				
			
			@ -383,11 +411,11 @@ async def main():
 | 
			
		|||
                if not all([ctx.rom_names[tuple(rom)] == (team, slot) for rom, (team, slot) in rom_names]):
 | 
			
		||||
                    raise Exception('Save file mismatch, will start a new game')
 | 
			
		||||
                ctx.received_items = received_items
 | 
			
		||||
                print('Loaded save file with %d received items for %d players' % (sum([len(p) for p in received_items.values()]), len(received_items)))
 | 
			
		||||
                logging.info('Loaded save file with %d received items for %d players' % (sum([len(p) for p in received_items.values()]), len(received_items)))
 | 
			
		||||
        except FileNotFoundError:
 | 
			
		||||
            print('No save data found, starting a new game')
 | 
			
		||||
            logging.error('No save data found, starting a new game')
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            print(e)
 | 
			
		||||
            logging.info(e)
 | 
			
		||||
 | 
			
		||||
    ctx.server = websockets.serve(functools.partial(server,ctx=ctx), ctx.host, ctx.port, ping_timeout=None, ping_interval=None)
 | 
			
		||||
    await ctx.server
 | 
			
		||||
| 
						 | 
				
			
			@ -397,4 +425,4 @@ if __name__ == '__main__':
 | 
			
		|||
    loop = asyncio.get_event_loop()
 | 
			
		||||
    loop.run_until_complete(main())
 | 
			
		||||
    loop.run_until_complete(asyncio.gather(*asyncio.Task.all_tasks()))
 | 
			
		||||
    loop.close()
 | 
			
		||||
    loop.close()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -152,13 +152,14 @@ def roll_settings(weights):
 | 
			
		|||
    entrance_shuffle = get_choice('entrance_shuffle')
 | 
			
		||||
    ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
 | 
			
		||||
 | 
			
		||||
    goal = get_choice('goals')
 | 
			
		||||
    ret.goal = {'ganon': 'ganon',
 | 
			
		||||
                'fast_ganon': 'crystals',
 | 
			
		||||
                'dungeons': 'dungeons',
 | 
			
		||||
                'pedestal': 'pedestal',
 | 
			
		||||
                'triforce-hunt': 'triforcehunt'
 | 
			
		||||
                }[get_choice('goals')]
 | 
			
		||||
    ret.openpyramid = ret.goal == 'crystals'
 | 
			
		||||
                }[goal]
 | 
			
		||||
    ret.openpyramid = goal == 'fast_ganon'
 | 
			
		||||
 | 
			
		||||
    ret.crystals_gt = get_choice('tower_open')
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										41
									
								
								Rom.py
								
								
								
								
							
							
						
						
									
										41
									
								
								Rom.py
								
								
								
								
							| 
						 | 
				
			
			@ -11,6 +11,7 @@ import subprocess
 | 
			
		|||
 | 
			
		||||
from BaseClasses import CollectionState, ShopType, Region, Location
 | 
			
		||||
from Dungeons import dungeon_music_addresses
 | 
			
		||||
from Regions import location_table
 | 
			
		||||
from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable
 | 
			
		||||
from Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts, junk_texts
 | 
			
		||||
from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
 | 
			
		||||
| 
						 | 
				
			
			@ -495,23 +496,27 @@ def patch_rom(world, rom, player, team, enemized):
 | 
			
		|||
            continue
 | 
			
		||||
 | 
			
		||||
        if not location.crystal:
 | 
			
		||||
            # Keys in their native dungeon should use the orignal item code for keys
 | 
			
		||||
            if location.parent_region.dungeon:
 | 
			
		||||
                dungeon = location.parent_region.dungeon
 | 
			
		||||
                if location.item is not None and dungeon.is_dungeon_item(location.item):
 | 
			
		||||
                    if location.item.bigkey:
 | 
			
		||||
                        itemid = 0x32
 | 
			
		||||
                    if location.item.smallkey:
 | 
			
		||||
                        itemid = 0x24
 | 
			
		||||
                    if location.item.map:
 | 
			
		||||
                        itemid = 0x33
 | 
			
		||||
                    if location.item.compass:
 | 
			
		||||
                        itemid = 0x25
 | 
			
		||||
            if location.item and location.item.player != player:
 | 
			
		||||
                if location.player_address is not None:
 | 
			
		||||
                    rom.write_byte(location.player_address, location.item.player)
 | 
			
		||||
                else:
 | 
			
		||||
                    itemid = 0x5A
 | 
			
		||||
            if location.item is not None:
 | 
			
		||||
                # Keys in their native dungeon should use the orignal item code for keys
 | 
			
		||||
                if location.parent_region.dungeon:
 | 
			
		||||
                    if location.parent_region.dungeon.is_dungeon_item(location.item):
 | 
			
		||||
                        if location.item.bigkey:
 | 
			
		||||
                            itemid = 0x32
 | 
			
		||||
                        if location.item.smallkey:
 | 
			
		||||
                            itemid = 0x24
 | 
			
		||||
                        if location.item.map:
 | 
			
		||||
                            itemid = 0x33
 | 
			
		||||
                        if location.item.compass:
 | 
			
		||||
                            itemid = 0x25
 | 
			
		||||
                if world.remote_items[player]:
 | 
			
		||||
                    itemid = list(location_table.keys()).index(location.name) + 1
 | 
			
		||||
                    assert itemid < 0x100
 | 
			
		||||
                    rom.write_byte(location.player_address, 0xFF)
 | 
			
		||||
                elif location.item.player != player:
 | 
			
		||||
                    if location.player_address is not None:
 | 
			
		||||
                        rom.write_byte(location.player_address, location.item.player)
 | 
			
		||||
                    else:
 | 
			
		||||
                        itemid = 0x5A
 | 
			
		||||
            rom.write_byte(location.address, itemid)
 | 
			
		||||
        else:
 | 
			
		||||
            # crystals
 | 
			
		||||
| 
						 | 
				
			
			@ -1229,6 +1234,8 @@ def patch_rom(world, rom, player, team, enemized):
 | 
			
		|||
 | 
			
		||||
    write_strings(rom, world, player, team)
 | 
			
		||||
 | 
			
		||||
    rom.write_byte(0x18636C, 1 if world.remote_items[player] else 0)
 | 
			
		||||
 | 
			
		||||
    # set rom name
 | 
			
		||||
    # 21 bytes
 | 
			
		||||
    from Main import __version__
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
aioconsole==0.1.15
 | 
			
		||||
colorama==0.4.3
 | 
			
		||||
websockets==8.1
 | 
			
		||||
		Loading…
	
		Reference in New Issue