Merge branch 'multiworld_31' of github.com:Bonta0/ALttPEntranceRandomizer into owg

This commit is contained in:
qadan 2020-01-21 21:41:18 -04:00
commit edd0a08143
10 changed files with 199 additions and 143 deletions

View File

@ -60,6 +60,7 @@ class World(object):
self.__dict__.setdefault(attr, {})[player] = val self.__dict__.setdefault(attr, {})[player] = val
set_player_attr('_region_cache', {}) set_player_attr('_region_cache', {})
set_player_attr('player_names', []) set_player_attr('player_names', [])
set_player_attr('remote_items', False)
set_player_attr('required_medallions', ['Ether', 'Quake']) set_player_attr('required_medallions', ['Ether', 'Quake'])
set_player_attr('swamp_patch_required', False) set_player_attr('swamp_patch_required', False)
set_player_attr('powder_patch_required', False) set_player_attr('powder_patch_required', False)

View File

@ -269,6 +269,7 @@ def parse_arguments(argv, no_defaults=False):
parser.add_argument('--enemy_damage', default=defval('default'), choices=['default', 'shuffled', 'chaos']) 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('--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('--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('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--names', default=defval('')) parser.add_argument('--names', default=defval(''))
parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1)) parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1))
@ -294,7 +295,8 @@ def parse_arguments(argv, no_defaults=False):
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'retro', 'accessibility', 'hints', 'beemizer', 'retro', 'accessibility', 'hints', 'beemizer',
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', '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) value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1: if player == 1:
setattr(ret, name, {1: value}) setattr(ret, name, {1: value})

12
Main.py
View File

@ -39,6 +39,7 @@ def main(args, seed=None):
world.seed = int(seed) world.seed = int(seed)
random.seed(world.seed) random.seed(world.seed)
world.remote_items = args.remote_items.copy()
world.mapshuffle = args.mapshuffle.copy() world.mapshuffle = args.mapshuffle.copy()
world.compassshuffle = args.compassshuffle.copy() world.compassshuffle = args.compassshuffle.copy()
world.keyshuffle = args.keyshuffle.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 '' outfilepname = f'_T{team+1}' if world.teams > 1 else ''
if world.players > 1: if world.players > 1:
outfilepname += f'_P{player}' 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 '' 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], 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], 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 '' "-nohints" if not world.hints[player] else "")) if not args.outputname else ''
rom.write_to_file(output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc')) rom.write_to_file(output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc'))
multidata = zlib.compress(json.dumps((parsed_names, rom_names, multidata = zlib.compress(json.dumps({"names": parsed_names,
[((location.address, location.player), (location.item.code, location.item.player)) for location in world.get_filled_locations() if type(location.address) is int]) "roms": rom_names,
).encode("utf-8")) "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: if args.jsonout:
jsonout["multidata"] = list(multidata) jsonout["multidata"] = list(multidata)
else: 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 = 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.teams = world.teams
ret.player_names = copy.deepcopy(world.player_names) ret.player_names = copy.deepcopy(world.player_names)
ret.remote_items = world.remote_items.copy()
ret.required_medallions = world.required_medallions.copy() ret.required_medallions = world.required_medallions.copy()
ret.swamp_patch_required = world.swamp_patch_required.copy() ret.swamp_patch_required = world.swamp_patch_required.copy()
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy() ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()

View File

@ -1,39 +1,16 @@
import aioconsole
import argparse import argparse
import asyncio import asyncio
import colorama
import json import json
import logging import logging
import shlex import shlex
import subprocess
import sys
import urllib.parse import urllib.parse
import websockets
import Items import Items
import Regions import Regions
while True:
try:
import aioconsole
break
except ImportError:
aioconsole = None
print('Required python module "aioconsole" not found, press enter to install it')
input()
subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'aioconsole'])
while True:
try:
import websockets
break
except ImportError:
websockets = None
print('Required python module "websockets" not found, press enter to install it')
input()
subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'websockets'])
try:
import colorama
except ImportError:
colorama = None
class ReceivedItem: class ReceivedItem:
def __init__(self, item, location, player): def __init__(self, item, location, player):
@ -47,6 +24,7 @@ class Context:
self.server_address = server_address self.server_address = server_address
self.exit_event = asyncio.Event() self.exit_event = asyncio.Event()
self.watcher_event = asyncio.Event()
self.input_queue = asyncio.Queue() self.input_queue = asyncio.Queue()
self.input_requests = 0 self.input_requests = 0
@ -68,7 +46,9 @@ class Context:
self.slot = None self.slot = None
self.player_names = {} self.player_names = {}
self.locations_checked = set() self.locations_checked = set()
self.locations_scouted = set()
self.items_received = [] self.items_received = []
self.locations_info = {}
self.awaiting_rom = False self.awaiting_rom = False
self.rom = None self.rom = None
self.auth = None self.auth = None
@ -102,6 +82,10 @@ RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte
RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte
ROOMID_ADDR = SAVEDATA_START + 0x4D4 # 2 bytes ROOMID_ADDR = SAVEDATA_START + 0x4D4 # 2 bytes
ROOMDATA_ADDR = SAVEDATA_START + 0x4D6 # 1 byte 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), location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
"Blind's Hideout - Left": (0x11d, 0x20), "Blind's Hideout - Left": (0x11d, 0x20),
@ -327,7 +311,7 @@ SNES_ATTACHED = 3
async def snes_connect(ctx : Context, address): async def snes_connect(ctx : Context, address):
if ctx.snes_socket is not None: if ctx.snes_socket is not None:
print('Already connected to snes') logging.error('Already connected to snes')
return return
ctx.snes_state = SNES_CONNECTING ctx.snes_state = SNES_CONNECTING
@ -335,7 +319,7 @@ async def snes_connect(ctx : Context, address):
address = f"ws://{address}" if "://" not in address else 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: try:
ctx.snes_socket = await websockets.connect(address, ping_timeout=None, ping_interval=None) ctx.snes_socket = await websockets.connect(address, ping_timeout=None, ping_interval=None)
@ -353,9 +337,9 @@ async def snes_connect(ctx : Context, address):
if not devices: if not devices:
raise Exception('No device found') raise Exception('No device found')
print("Available devices:") logging.info("Available devices:")
for id, device in enumerate(devices): for id, device in enumerate(devices):
print("[%d] %s" % (id + 1, device)) logging.info("[%d] %s" % (id + 1, device))
device = None device = None
if len(devices) == 1: if len(devices) == 1:
@ -367,18 +351,18 @@ async def snes_connect(ctx : Context, address):
device = devices[ctx.snes_attached_device[0]] device = devices[ctx.snes_attached_device[0]]
else: else:
while True: while True:
print("Select a device:") logging.info("Select a device:")
choice = await console_input(ctx) choice = await console_input(ctx)
if choice is None: if choice is None:
raise Exception('Abort input') raise Exception('Abort input')
if not choice.isdigit() or int(choice) < 1 or int(choice) > len(devices): 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 continue
device = devices[int(choice) - 1] device = devices[int(choice) - 1]
break break
print("Attaching to " + device) logging.info("Attaching to " + device)
Attach_Request = { Attach_Request = {
"Opcode" : "Attach", "Opcode" : "Attach",
@ -390,12 +374,12 @@ async def snes_connect(ctx : Context, address):
ctx.snes_attached_device = (devices.index(device), device) ctx.snes_attached_device = (devices.index(device), device)
if 'SD2SNES'.lower() in device.lower() or (len(device) == 4 and device[:3] == 'COM'): 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 ctx.is_sd2snes = True
await ctx.snes_socket.send(json.dumps({"Opcode" : "Info", "Space" : "SNES"})) await ctx.snes_socket.send(json.dumps({"Opcode" : "Info", "Space" : "SNES"}))
reply = json.loads(await ctx.snes_socket.recv()) reply = json.loads(await ctx.snes_socket.recv())
if reply and 'Results' in reply: if reply and 'Results' in reply:
print(reply['Results']) logging.info(reply['Results'])
else: else:
ctx.is_sd2snes = False ctx.is_sd2snes = False
@ -413,9 +397,9 @@ async def snes_connect(ctx : Context, address):
ctx.snes_socket = None ctx.snes_socket = None
ctx.snes_state = SNES_DISCONNECTED ctx.snes_state = SNES_DISCONNECTED
if not ctx.snes_reconnect_address: if not ctx.snes_reconnect_address:
print("Error connecting to snes (%s)" % e) logging.error("Error connecting to snes (%s)" % e)
else: 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)) asyncio.create_task(snes_autoreconnect(ctx))
async def snes_autoreconnect(ctx: Context): async def snes_autoreconnect(ctx: Context):
@ -427,11 +411,11 @@ async def snes_recv_loop(ctx : Context):
try: try:
async for msg in ctx.snes_socket: async for msg in ctx.snes_socket:
ctx.snes_recv_queue.put_nowait(msg) ctx.snes_recv_queue.put_nowait(msg)
print("Snes disconnected") logging.warning("Snes disconnected")
except Exception as e: except Exception as e:
if not isinstance(e, websockets.WebSocketException): if not isinstance(e, websockets.WebSocketException):
logging.exception(e) 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: finally:
socket, ctx.snes_socket = ctx.snes_socket, None socket, ctx.snes_socket = ctx.snes_socket, None
if socket is not None and not socket.closed: if socket is not None and not socket.closed:
@ -444,7 +428,7 @@ async def snes_recv_loop(ctx : Context):
ctx.rom = None ctx.rom = None
if ctx.snes_reconnect_address: 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)) asyncio.create_task(snes_autoreconnect(ctx))
async def snes_read(ctx : Context, address, size): async def snes_read(ctx : Context, address, size):
@ -472,9 +456,9 @@ async def snes_read(ctx : Context, address, size):
break break
if len(data) != size: 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): if len(data):
print(str(data)) logging.error(str(data))
if ctx.snes_socket is not None and not ctx.snes_socket.closed: if ctx.snes_socket is not None and not ctx.snes_socket.closed:
await ctx.snes_socket.close() await ctx.snes_socket.close()
return None return None
@ -500,7 +484,7 @@ async def snes_write(ctx : Context, write_list):
for address, data in write_list: for address, data in write_list:
if (address < WRAM_START) or ((address + len(data)) > (WRAM_START + WRAM_SIZE)): 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 return False
for ptr, byte in enumerate(data, address + 0x7E0000 - WRAM_START): for ptr, byte in enumerate(data, address + 0x7E0000 - WRAM_START):
cmd += b'\xA9' # LDA cmd += b'\xA9' # LDA
@ -559,48 +543,49 @@ async def send_msgs(websocket, msgs):
async def server_loop(ctx : Context, address = None): async def server_loop(ctx : Context, address = None):
if ctx.socket is not None: if ctx.socket is not None:
print('Already connected') logging.error('Already connected')
return return
if address is None: if address is None:
address = ctx.server_address address = ctx.server_address
while not address: while not address:
print('Enter multiworld server address') logging.info('Enter multiworld server address')
address = await console_input(ctx) address = await console_input(ctx)
address = f"ws://{address}" if "://" not in address else address address = f"ws://{address}" if "://" not in address else address
port = urllib.parse.urlparse(address).port or 38281 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: try:
ctx.socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None) ctx.socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None)
print('Connected') logging.info('Connected')
ctx.server_address = address ctx.server_address = address
async for data in ctx.socket: async for data in ctx.socket:
for msg in json.loads(data): for msg in json.loads(data):
cmd, args = (msg[0], msg[1]) if len(msg) > 1 else (msg, None) cmd, args = (msg[0], msg[1]) if len(msg) > 1 else (msg, None)
await process_server_cmd(ctx, cmd, args) 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: except ConnectionRefusedError:
print('Connection refused by the multiworld server') logging.error('Connection refused by the multiworld server')
except (OSError, websockets.InvalidURI): 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: 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): if not isinstance(e, websockets.WebSocketException):
logging.exception(e) logging.exception(e)
finally: finally:
ctx.awaiting_rom = False ctx.awaiting_rom = False
ctx.auth = None ctx.auth = None
ctx.items_received = [] ctx.items_received = []
ctx.locations_info = {}
socket, ctx.socket = ctx.socket, None socket, ctx.socket = ctx.socket, None
if socket is not None and not socket.closed: if socket is not None and not socket.closed:
await socket.close() await socket.close()
ctx.server_task = None ctx.server_task = None
if ctx.server_address: 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)) asyncio.create_task(server_autoreconnect(ctx))
async def server_autoreconnect(ctx: Context): async def server_autoreconnect(ctx: Context):
@ -610,28 +595,28 @@ async def server_autoreconnect(ctx: Context):
async def process_server_cmd(ctx : Context, cmd, args): async def process_server_cmd(ctx : Context, cmd, args):
if cmd == 'RoomInfo': if cmd == 'RoomInfo':
print('--------------------------------') logging.info('--------------------------------')
print('Room Information:') logging.info('Room Information:')
print('--------------------------------') logging.info('--------------------------------')
if args['password']: if args['password']:
print('Password required') logging.info('Password required')
if len(args['players']) < 1: if len(args['players']) < 1:
print('No player connected') logging.info('No player connected')
else: else:
args['players'].sort() args['players'].sort()
current_team = 0 current_team = 0
print('Connected players:') logging.info('Connected players:')
print(' Team #1') logging.info(' Team #1')
for team, slot, name in args['players']: for team, slot, name in args['players']:
if team != current_team: if team != current_team:
print(f' Team #{team + 1}') logging.info(f' Team #{team + 1}')
current_team = team current_team = team
print(' %s (Player %d)' % (name, slot)) logging.info(' %s (Player %d)' % (name, slot))
await server_auth(ctx, args['password']) await server_auth(ctx, args['password'])
if cmd == 'ConnectionRefused': if cmd == 'ConnectionRefused':
if 'InvalidPassword' in args: if 'InvalidPassword' in args:
print('Invalid password') logging.error('Invalid password')
ctx.password = None ctx.password = None
await server_auth(ctx, True) await server_auth(ctx, True)
if 'InvalidRom' in args: if 'InvalidRom' in args:
@ -643,8 +628,13 @@ async def process_server_cmd(ctx : Context, cmd, args):
if cmd == 'Connected': if cmd == 'Connected':
ctx.team, ctx.slot = args[0] ctx.team, ctx.slot = args[0]
ctx.player_names = {p: n for p, n in args[1]} ctx.player_names = {p: n for p, n in args[1]}
msgs = []
if ctx.locations_checked: 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': if cmd == 'ReceivedItems':
start_index, items = args start_index, items = args
@ -658,24 +648,34 @@ async def process_server_cmd(ctx : Context, cmd, args):
if start_index == len(ctx.items_received): if start_index == len(ctx.items_received):
for item in items: for item in items:
ctx.items_received.append(ReceivedItem(*item)) 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': if cmd == 'ItemSent':
player_sent, location, player_recvd, item = args player_sent, location, player_recvd, item = args
item = color(get_item_name_from_id(item), 'cyan' if player_sent != ctx.slot else 'green') 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_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') 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': if cmd == 'Print':
print(args) logging.info(args)
async def server_auth(ctx : Context, password_requested): async def server_auth(ctx : Context, password_requested):
if password_requested and not ctx.password: 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) ctx.password = await console_input(ctx)
if ctx.rom is None: if ctx.rom is None:
ctx.awaiting_rom = True 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 return
ctx.awaiting_rom = False ctx.awaiting_rom = False
ctx.auth = ctx.rom.copy() ctx.auth = ctx.rom.copy()
@ -711,12 +711,6 @@ async def console_loop(ctx : Context):
if command[0] == '/exit': if command[0] == '/exit':
ctx.exit_event.set() 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': if command[0] == '/snes':
ctx.snes_reconnect_address = None ctx.snes_reconnect_address = None
asyncio.create_task(snes_connect(ctx, command[1] if len(command) > 1 else ctx.snes_address)) asyncio.create_task(snes_connect(ctx, command[1] if len(command) > 1 else ctx.snes_address))
@ -735,25 +729,25 @@ async def console_loop(ctx : Context):
asyncio.create_task(send_msgs(ctx.socket, [['Say', input]])) asyncio.create_task(send_msgs(ctx.socket, [['Say', input]]))
if command[0] == '/received': if command[0] == '/received':
print('Received items:') logging.info('Received items:')
for index, item in enumerate(ctx.items_received, 1): 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'), 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))) get_location_name_from_address(item.location), index, len(ctx.items_received)))
if command[0] == '/missing': if command[0] == '/missing':
for location in [k for k, v in Regions.location_table.items() if type(v[0]) is int]: 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: if location not in ctx.locations_checked:
print('Missing: ' + location) logging.info('Missing: ' + location)
if command[0] == '/getitem' and len(command) > 1: if command[0] == '/getitem' and len(command) > 1:
item = input[9:] item = input[9:]
item_id = Items.item_table[item][3] if item in Items.item_table else None 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): 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_ADDR, bytes([item_id]))
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([0])) snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([0]))
else: else:
print('Invalid item: ' + item) logging.info('Invalid item: ' + item)
await snes_flush_writes(ctx) await snes_flush_writes(ctx)
@ -772,7 +766,7 @@ async def track_locations(ctx : Context, roomid, roomdata):
new_locations = [] new_locations = []
def new_check(location): def new_check(location):
ctx.locations_checked.add(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]) new_locations.append(Regions.location_table[location][0])
for location, (loc_roomid, loc_mask) in location_table_uw.items(): for location, (loc_roomid, loc_mask) in location_table_uw.items():
@ -831,7 +825,11 @@ async def track_locations(ctx : Context, roomid, roomdata):
async def game_watcher(ctx : Context): async def game_watcher(ctx : Context):
while not ctx.exit_event.is_set(): 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: if not ctx.rom:
rom = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE) rom = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
@ -840,50 +838,64 @@ async def game_watcher(ctx : Context):
ctx.rom = list(rom) ctx.rom = list(rom)
ctx.locations_checked = set() ctx.locations_checked = set()
ctx.locations_scouted = set()
if ctx.awaiting_rom: if ctx.awaiting_rom:
await server_auth(ctx, False) await server_auth(ctx, False)
if ctx.auth and ctx.auth != ctx.rom: 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) await disconnect(ctx)
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1) gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
if gamemode is None or gamemode[0] not in INGAME_MODES: if gamemode is None or gamemode[0] not in INGAME_MODES:
continue continue
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 7) data = await snes_read(ctx, RECV_PROGRESS_ADDR, 8)
if data is None: if data is None:
continue continue
recv_index = data[0] | (data[1] << 8) 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] recv_item = data[2]
assert(ROOMID_ADDR == RECV_PROGRESS_ADDR + 4) assert ROOMID_ADDR == RECV_PROGRESS_ADDR + 4
roomid = data[4] | (data[5] << 8) roomid = data[4] | (data[5] << 8)
assert(ROOMDATA_ADDR == RECV_PROGRESS_ADDR + 6) assert ROOMDATA_ADDR == RECV_PROGRESS_ADDR + 6
roomdata = data[6] roomdata = data[6]
assert SCOUT_LOCATION_ADDR == RECV_PROGRESS_ADDR + 7
await track_locations(ctx, roomid, roomdata) scout_location = data[7]
if recv_index < len(ctx.items_received) and recv_item == 0: if recv_index < len(ctx.items_received) and recv_item == 0:
item = ctx.items_received[recv_index] 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'), 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))) get_location_name_from_address(item.location), recv_index + 1, len(ctx.items_received)))
recv_index += 1 recv_index += 1
snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) 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_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) 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(): async def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--snes', default='localhost:8080', help='Address of the QUsb2snes server.') 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('--connect', default=None, help='Address of the multiworld host.')
parser.add_argument('--password', default=None, help='Password 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() 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) ctx = Context(args.snes, args.connect, args.password)
input_task = asyncio.create_task(console_loop(ctx)) input_task = asyncio.create_task(console_loop(ctx))
@ -917,13 +929,9 @@ async def main():
await input_task await input_task
if __name__ == '__main__': if __name__ == '__main__':
if 'colorama' in sys.modules:
colorama.init() colorama.init()
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.run_until_complete(main()) loop.run_until_complete(main())
loop.run_until_complete(asyncio.gather(*asyncio.Task.all_tasks())) loop.run_until_complete(asyncio.gather(*asyncio.Task.all_tasks()))
loop.close() loop.close()
if 'colorama' in sys.modules:
colorama.deinit() colorama.deinit()

View File

@ -30,6 +30,7 @@ class Context:
self.disable_save = False self.disable_save = False
self.player_names = {} self.player_names = {}
self.rom_names = {} self.rom_names = {}
self.remote_items = set()
self.locations = {} self.locations = {}
self.host = host self.host = host
self.port = port self.port = port
@ -58,17 +59,17 @@ def broadcast_team(ctx : Context, team, msgs):
asyncio.create_task(send_msgs(client.socket, msgs)) asyncio.create_task(send_msgs(client.socket, msgs))
def notify_all(ctx : Context, text): def notify_all(ctx : Context, text):
print("Notice (all): %s" % text) logging.info("Notice (all): %s" % text)
broadcast_all(ctx, [['Print', text]]) broadcast_all(ctx, [['Print', text]])
def notify_team(ctx : Context, team : int, text : str): 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]]) broadcast_team(ctx, team, [['Print', text]])
def notify_client(client : Client, text : str): def notify_client(client : Client, text : str):
if not client.auth: if not client.auth:
return 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]])) asyncio.create_task(send_msgs(client.socket, [['Print', text]]))
async def server(websocket, path, ctx : Context): async def server(websocket, path, ctx : Context):
@ -162,7 +163,7 @@ def register_location_checks(ctx : Context, team, slot, locations):
for location in locations: for location in locations:
if (location, slot) in ctx.locations: if (location, slot) in ctx.locations:
target_item, target_player = ctx.locations[(location, slot)] target_item, target_player = ctx.locations[(location, slot)]
if target_player != slot: if target_player != slot or slot in ctx.remote_items:
found = False found = False
recvd_items = get_received_items(ctx, team, target_player) recvd_items = get_received_items(ctx, team, target_player)
for recvd_item in recvd_items: for recvd_item in recvd_items:
@ -172,8 +173,9 @@ def register_location_checks(ctx : Context, team, slot, locations):
if not found: if not found:
new_item = ReceivedItem(target_item, location, slot) new_item = ReceivedItem(target_item, location, slot)
recvd_items.append(new_item) recvd_items.append(new_item)
if slot != target_player:
broadcast_team(ctx, team, [['ItemSent', (slot, location, target_player, target_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))) 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 found_items = True
send_new_items(ctx) 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) items = get_received_items(ctx, client.team, client.slot)
if items: if items:
client.send_index = len(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 cmd == 'LocationChecks':
if type(args) is not list: if type(args) is not list:
@ -240,6 +242,28 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
return return
register_location_checks(ctx, client.team, client.slot, args) 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 cmd == 'Say':
if type(args) is not str or not args.isprintable(): if type(args) is not str or not args.isprintable():
await send_msgs(client.socket, [['InvalidArguments', 'Say']]) 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): def set_password(ctx : Context, password):
ctx.password = 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): async def console(ctx : Context):
while True: while True:
@ -275,7 +299,7 @@ async def console(ctx : Context):
break break
if command[0] == '/players': if command[0] == '/players':
print(get_connected_players_string(ctx)) logging.info(get_connected_players_string(ctx))
if command[0] == '/password': if command[0] == '/password':
set_password(ctx, command[1] if len(command) > 1 else None) set_password(ctx, command[1] if len(command) > 1 else None)
if command[0] == '/kick' and len(command) > 1: if command[0] == '/kick' and len(command) > 1:
@ -309,7 +333,7 @@ async def console(ctx : Context):
notify_all(ctx, 'Cheat console: sending "' + item + '" to ' + client.name) notify_all(ctx, 'Cheat console: sending "' + item + '" to ' + client.name)
send_new_items(ctx) send_new_items(ctx)
else: else:
print("Unknown item: " + item) logging.warning("Unknown item: " + item)
if command[0][0] != '/': if command[0][0] != '/':
notify_all(ctx, '[Server]: ' + input) notify_all(ctx, '[Server]: ' + input)
@ -322,8 +346,11 @@ async def main():
parser.add_argument('--multidata', default=None) parser.add_argument('--multidata', default=None)
parser.add_argument('--savefile', default=None) parser.add_argument('--savefile', default=None)
parser.add_argument('--disable_save', default=False, action='store_true') 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() 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 = Context(args.host, args.port, args.password)
ctx.data_filename = args.multidata ctx.data_filename = args.multidata
@ -338,17 +365,18 @@ async def main():
with open(ctx.data_filename, 'rb') as f: with open(ctx.data_filename, 'rb') as f:
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8")) 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): for player, name in enumerate(names, 1):
ctx.player_names[(team, player)] = name ctx.player_names[(team, player)] = name
ctx.rom_names = {tuple(rom): (team, slot) for slot, team, rom in jsonobj[1]} ctx.rom_names = {tuple(rom): (team, slot) for slot, team, rom in jsonobj['roms']}
ctx.locations = {tuple(k): tuple(v) for k, v in jsonobj[2]} ctx.remote_items = set(jsonobj['remote_items'])
ctx.locations = {tuple(k): tuple(v) for k, v in jsonobj['locations']}
except Exception as e: except Exception as e:
print('Failed to read multiworld data (%s)' % e) logging.error('Failed to read multiworld data (%s)' % e)
return return
ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8') if not ctx.host else ctx.host 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 ctx.disable_save = args.disable_save
if not ctx.disable_save: if not ctx.disable_save:
@ -362,11 +390,11 @@ async def main():
if not all([ctx.rom_names[tuple(rom)] == (team, slot) for rom, (team, slot) in rom_names]): 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') raise Exception('Save file mismatch, will start a new game')
ctx.received_items = received_items 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: 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: 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) ctx.server = websockets.serve(functools.partial(server,ctx=ctx), ctx.host, ctx.port, ping_timeout=None, ping_interval=None)
await ctx.server await ctx.server

View File

@ -145,13 +145,14 @@ def roll_settings(weights):
entrance_shuffle = get_choice('entrance_shuffle') entrance_shuffle = get_choice('entrance_shuffle')
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla' ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
goal = get_choice('goals')
ret.goal = {'ganon': 'ganon', ret.goal = {'ganon': 'ganon',
'fast_ganon': 'crystals', 'fast_ganon': 'crystals',
'dungeons': 'dungeons', 'dungeons': 'dungeons',
'pedestal': 'pedestal', 'pedestal': 'pedestal',
'triforce-hunt': 'triforcehunt' 'triforce-hunt': 'triforcehunt'
}[get_choice('goals')] }[goal]
ret.openpyramid = ret.goal == 'fast_ganon' ret.openpyramid = goal == 'fast_ganon'
ret.crystals_gt = get_choice('tower_open') ret.crystals_gt = get_choice('tower_open')

13
Rom.py
View File

@ -11,6 +11,7 @@ import subprocess
from BaseClasses import CollectionState, ShopType, Region, Location from BaseClasses import CollectionState, ShopType, Region, Location
from Dungeons import dungeon_music_addresses from Dungeons import dungeon_music_addresses
from Regions import location_table
from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable 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 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 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,10 +496,10 @@ def patch_rom(world, rom, player, team, enemized):
continue continue
if not location.crystal: if not location.crystal:
if location.item is not None:
# Keys in their native dungeon should use the orignal item code for keys # Keys in their native dungeon should use the orignal item code for keys
if location.parent_region.dungeon: if location.parent_region.dungeon:
dungeon = location.parent_region.dungeon if location.parent_region.dungeon.is_dungeon_item(location.item):
if location.item is not None and dungeon.is_dungeon_item(location.item):
if location.item.bigkey: if location.item.bigkey:
itemid = 0x32 itemid = 0x32
if location.item.smallkey: if location.item.smallkey:
@ -507,7 +508,11 @@ def patch_rom(world, rom, player, team, enemized):
itemid = 0x33 itemid = 0x33
if location.item.compass: if location.item.compass:
itemid = 0x25 itemid = 0x25
if location.item and location.item.player != player: 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: if location.player_address is not None:
rom.write_byte(location.player_address, location.item.player) rom.write_byte(location.player_address, location.item.player)
else: else:
@ -1229,6 +1234,8 @@ def patch_rom(world, rom, player, team, enemized):
write_strings(rom, world, player, team) write_strings(rom, world, player, team)
rom.write_byte(0x18636C, 1 if world.remote_items[player] else 0)
# set rom name # set rom name
# 21 bytes # 21 bytes
from Main import __version__ 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

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
aioconsole==0.1.15
colorama==0.4.3
websockets==8.1