Merge branch 'main' into breaking_changes
# Conflicts: # MultiClient.py # WebUI.py
This commit is contained in:
commit
670b8b4b11
|
@ -1429,16 +1429,16 @@ class PlandoItem(NamedTuple):
|
||||||
location: str
|
location: str
|
||||||
world: Union[bool, str] = False # False -> own world, True -> not own world
|
world: Union[bool, str] = False # False -> own world, True -> not own world
|
||||||
from_pool: bool = True # if item should be removed from item pool
|
from_pool: bool = True # if item should be removed from item pool
|
||||||
force: Union[bool, str] = 'silent' # False -> warns if item not successfully placed. True -> errors out on failure to place item.
|
force: str = 'silent' # false -> warns if item not successfully placed. true -> errors out on failure to place item.
|
||||||
|
|
||||||
def warn(self, warning: str):
|
def warn(self, warning: str):
|
||||||
if str(self.force).lower() in ['true', 'fail', 'failure', 'none', 'false', 'warn', 'warning']:
|
if self.force in ['true', 'fail', 'failure', 'none', 'false', 'warn', 'warning']:
|
||||||
logging.warning(f'{warning}')
|
logging.warning(f'{warning}')
|
||||||
else:
|
else:
|
||||||
logging.debug(f'{warning}')
|
logging.debug(f'{warning}')
|
||||||
|
|
||||||
def failed(self, warning: str, exception=Exception):
|
def failed(self, warning: str, exception=Exception):
|
||||||
if str(self.force).lower() in ['true', 'fail', 'failure']:
|
if self.force in ['true', 'fail', 'failure']:
|
||||||
raise exception(warning)
|
raise exception(warning)
|
||||||
else:
|
else:
|
||||||
self.warn(warning)
|
self.warn(warning)
|
||||||
|
|
12
Fill.py
12
Fill.py
|
@ -257,6 +257,11 @@ def balance_multiworld_progression(world):
|
||||||
|
|
||||||
reachable_locations_count = {player: 0 for player in range(1, world.players + 1)}
|
reachable_locations_count = {player: 0 for player in range(1, world.players + 1)}
|
||||||
|
|
||||||
|
def event_key(location):
|
||||||
|
return location.event and (
|
||||||
|
world.keyshuffle[location.item.player] or not location.item.smallkey) and (
|
||||||
|
world.bigkeyshuffle[location.item.player] or not location.item.bigkey)
|
||||||
|
|
||||||
def get_sphere_locations(sphere_state, locations):
|
def get_sphere_locations(sphere_state, locations):
|
||||||
sphere_state.sweep_for_events(key_only=True, locations=locations)
|
sphere_state.sweep_for_events(key_only=True, locations=locations)
|
||||||
return [loc for loc in locations if sphere_state.can_reach(loc)]
|
return [loc for loc in locations if sphere_state.can_reach(loc)]
|
||||||
|
@ -279,9 +284,7 @@ def balance_multiworld_progression(world):
|
||||||
candidate_items = []
|
candidate_items = []
|
||||||
while True:
|
while True:
|
||||||
for location in balancing_sphere:
|
for location in balancing_sphere:
|
||||||
if location.event and (
|
if event_key(location):
|
||||||
world.keyshuffle[location.item.player] or not location.item.smallkey) and (
|
|
||||||
world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
|
|
||||||
balancing_state.collect(location.item, True, location)
|
balancing_state.collect(location.item, True, location)
|
||||||
if location.item.player in balancing_players and not location.locked:
|
if location.item.player in balancing_players and not location.locked:
|
||||||
candidate_items.append(location)
|
candidate_items.append(location)
|
||||||
|
@ -342,8 +345,7 @@ def balance_multiworld_progression(world):
|
||||||
sphere_locations.append(location)
|
sphere_locations.append(location)
|
||||||
|
|
||||||
for location in sphere_locations:
|
for location in sphere_locations:
|
||||||
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (
|
if event_key(location):
|
||||||
world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
|
|
||||||
state.collect(location.item, True, location)
|
state.collect(location.item, True, location)
|
||||||
checked_locations.extend(sphere_locations)
|
checked_locations.extend(sphere_locations)
|
||||||
|
|
||||||
|
|
126
MultiClient.py
126
MultiClient.py
|
@ -36,6 +36,10 @@ import WebUI
|
||||||
from worlds.alttp import Regions
|
from worlds.alttp import Regions
|
||||||
import Utils
|
import Utils
|
||||||
|
|
||||||
|
# logging note:
|
||||||
|
# logging.* gets send to only the text console, logger.* gets send to the WebUI as well, if it's initialized.
|
||||||
|
logger = logging.getLogger("Client")
|
||||||
|
|
||||||
|
|
||||||
def create_named_task(coro, *args, name=None):
|
def create_named_task(coro, *args, name=None):
|
||||||
if not name:
|
if not name:
|
||||||
|
@ -53,6 +57,7 @@ class Context():
|
||||||
|
|
||||||
# WebUI Stuff
|
# WebUI Stuff
|
||||||
self.ui_node = WebUI.WebUiClient()
|
self.ui_node = WebUI.WebUiClient()
|
||||||
|
logger.addHandler(self.ui_node)
|
||||||
self.custom_address = None
|
self.custom_address = None
|
||||||
self.webui_socket_port: typing.Optional[int] = port
|
self.webui_socket_port: typing.Optional[int] = port
|
||||||
self.hint_cost = 0
|
self.hint_cost = 0
|
||||||
|
@ -119,6 +124,7 @@ class Context():
|
||||||
return
|
return
|
||||||
await self.server.socket.send(dumps(msgs))
|
await self.server.socket.send(dumps(msgs))
|
||||||
|
|
||||||
|
|
||||||
color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
|
color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
|
||||||
'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43,
|
'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43,
|
||||||
'blue_bg': 44, 'purple_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
|
'blue_bg': 44, 'purple_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
|
||||||
|
@ -423,11 +429,11 @@ def launch_qusb2snes(ctx: Context):
|
||||||
qusb2snes_path = Utils.local_path(qusb2snes_path)
|
qusb2snes_path = Utils.local_path(qusb2snes_path)
|
||||||
|
|
||||||
if os.path.isfile(qusb2snes_path):
|
if os.path.isfile(qusb2snes_path):
|
||||||
ctx.ui_node.log_info(f"Attempting to start {qusb2snes_path}")
|
logger.info(f"Attempting to start {qusb2snes_path}")
|
||||||
import subprocess
|
import subprocess
|
||||||
subprocess.Popen(qusb2snes_path, cwd=os.path.dirname(qusb2snes_path))
|
subprocess.Popen(qusb2snes_path, cwd=os.path.dirname(qusb2snes_path))
|
||||||
else:
|
else:
|
||||||
ctx.ui_node.log_info(
|
logger.info(
|
||||||
f"Attempt to start (Q)Usb2Snes was aborted as path {qusb2snes_path} was not found, "
|
f"Attempt to start (Q)Usb2Snes was aborted as path {qusb2snes_path} was not found, "
|
||||||
f"please start it yourself if it is not running")
|
f"please start it yourself if it is not running")
|
||||||
|
|
||||||
|
@ -435,7 +441,7 @@ def launch_qusb2snes(ctx: Context):
|
||||||
async def _snes_connect(ctx: Context, address: str):
|
async def _snes_connect(ctx: Context, address: str):
|
||||||
address = f"ws://{address}" if "://" not in address else address
|
address = f"ws://{address}" if "://" not in address else address
|
||||||
|
|
||||||
ctx.ui_node.log_info("Connecting to QUsb2snes at %s ..." % address)
|
logger.info("Connecting to QUsb2snes at %s ..." % address)
|
||||||
seen_problems = set()
|
seen_problems = set()
|
||||||
succesful = False
|
succesful = False
|
||||||
while not succesful:
|
while not succesful:
|
||||||
|
@ -447,7 +453,7 @@ async def _snes_connect(ctx: Context, address: str):
|
||||||
# only tell the user about new problems, otherwise silently lay in wait for a working connection
|
# only tell the user about new problems, otherwise silently lay in wait for a working connection
|
||||||
if problem not in seen_problems:
|
if problem not in seen_problems:
|
||||||
seen_problems.add(problem)
|
seen_problems.add(problem)
|
||||||
ctx.ui_node.log_error(f"Error connecting to QUsb2snes ({problem})")
|
logger.error(f"Error connecting to QUsb2snes ({problem})")
|
||||||
|
|
||||||
if len(seen_problems) == 1:
|
if len(seen_problems) == 1:
|
||||||
# this is the first problem. Let's try launching QUsb2snes if it isn't already running
|
# this is the first problem. Let's try launching QUsb2snes if it isn't already running
|
||||||
|
@ -470,7 +476,7 @@ async def get_snes_devices(ctx: Context):
|
||||||
devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
|
devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
|
||||||
|
|
||||||
if not devices:
|
if not devices:
|
||||||
ctx.ui_node.log_info('No SNES device found. Ensure QUsb2Snes is running and connect it to the multibridge.')
|
logger.info('No SNES device found. Ensure QUsb2Snes is running and connect it to the multibridge.')
|
||||||
while not devices:
|
while not devices:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
await socket.send(dumps(DeviceList_Request))
|
await socket.send(dumps(DeviceList_Request))
|
||||||
|
@ -485,7 +491,7 @@ async def get_snes_devices(ctx: Context):
|
||||||
async def snes_connect(ctx: Context, address):
|
async def snes_connect(ctx: Context, address):
|
||||||
global SNES_RECONNECT_DELAY
|
global SNES_RECONNECT_DELAY
|
||||||
if ctx.snes_socket is not None and ctx.snes_state == SNES_CONNECTED:
|
if ctx.snes_socket is not None and ctx.snes_state == SNES_CONNECTED:
|
||||||
ctx.ui_node.log_error('Already connected to snes')
|
logger.error('Already connected to snes')
|
||||||
return
|
return
|
||||||
|
|
||||||
recv_task = None
|
recv_task = None
|
||||||
|
@ -511,7 +517,7 @@ async def snes_connect(ctx: Context, address):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
ctx.ui_node.log_info("Attaching to " + device)
|
logger.info("Attaching to " + device)
|
||||||
|
|
||||||
Attach_Request = {
|
Attach_Request = {
|
||||||
"Opcode": "Attach",
|
"Opcode": "Attach",
|
||||||
|
@ -524,12 +530,12 @@ async def snes_connect(ctx: Context, address):
|
||||||
ctx.ui_node.send_connection_status(ctx)
|
ctx.ui_node.send_connection_status(ctx)
|
||||||
|
|
||||||
if 'sd2snes' in device.lower() or 'COM' in device:
|
if 'sd2snes' in device.lower() or 'COM' in device:
|
||||||
ctx.ui_node.log_info("SD2SNES/FXPAK Detected")
|
logger.info("SD2SNES/FXPAK Detected")
|
||||||
ctx.is_sd2snes = True
|
ctx.is_sd2snes = True
|
||||||
await ctx.snes_socket.send(dumps({"Opcode" : "Info", "Space" : "SNES"}))
|
await ctx.snes_socket.send(dumps({"Opcode" : "Info", "Space" : "SNES"}))
|
||||||
reply = loads(await ctx.snes_socket.recv())
|
reply = loads(await ctx.snes_socket.recv())
|
||||||
if reply and 'Results' in reply:
|
if reply and 'Results' in reply:
|
||||||
ctx.ui_node.log_info(reply['Results'])
|
logger.info(reply['Results'])
|
||||||
else:
|
else:
|
||||||
ctx.is_sd2snes = False
|
ctx.is_sd2snes = False
|
||||||
|
|
||||||
|
@ -548,9 +554,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:
|
||||||
ctx.ui_node.log_error("Error connecting to snes (%s)" % e)
|
logger.error("Error connecting to snes (%s)" % e)
|
||||||
else:
|
else:
|
||||||
ctx.ui_node.log_error(f"Error connecting to snes, attempt again in {SNES_RECONNECT_DELAY}s")
|
logger.error(f"Error connecting to snes, attempt again in {SNES_RECONNECT_DELAY}s")
|
||||||
asyncio.create_task(snes_autoreconnect(ctx))
|
asyncio.create_task(snes_autoreconnect(ctx))
|
||||||
SNES_RECONNECT_DELAY *= 2
|
SNES_RECONNECT_DELAY *= 2
|
||||||
|
|
||||||
|
@ -577,11 +583,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)
|
||||||
ctx.ui_node.log_warning("Snes disconnected")
|
logger.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)
|
logger.exception(e)
|
||||||
ctx.ui_node.log_error("Lost connection to the snes, type /snes to reconnect")
|
logger.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:
|
||||||
|
@ -595,7 +601,7 @@ async def snes_recv_loop(ctx: Context):
|
||||||
ctx.rom = None
|
ctx.rom = None
|
||||||
|
|
||||||
if ctx.snes_reconnect_address:
|
if ctx.snes_reconnect_address:
|
||||||
ctx.ui_node.log_info(f"...reconnecting in {SNES_RECONNECT_DELAY}s")
|
logger.info(f"...reconnecting in {SNES_RECONNECT_DELAY}s")
|
||||||
asyncio.create_task(snes_autoreconnect(ctx))
|
asyncio.create_task(snes_autoreconnect(ctx))
|
||||||
|
|
||||||
|
|
||||||
|
@ -624,10 +630,10 @@ async def snes_read(ctx : Context, address, size):
|
||||||
break
|
break
|
||||||
|
|
||||||
if len(data) != size:
|
if len(data) != size:
|
||||||
logging.error('Error reading %s, requested %d bytes, received %d' % (hex(address), size, len(data)))
|
logger.error('Error reading %s, requested %d bytes, received %d' % (hex(address), size, len(data)))
|
||||||
if len(data):
|
if len(data):
|
||||||
ctx.ui_node.log_error(str(data))
|
logger.error(str(data))
|
||||||
ctx.ui_node.log_warning('Unable to connect to SNES Device because QUsb2Snes broke temporarily.'
|
logger.warning('Unable to connect to SNES Device because QUsb2Snes broke temporarily.'
|
||||||
'Try un-selecting and re-selecting the SNES Device.')
|
'Try un-selecting and re-selecting the SNES Device.')
|
||||||
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()
|
||||||
|
@ -652,7 +658,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)):
|
||||||
ctx.ui_node.log_error("SD2SNES: Write out of range %s (%d)" % (hex(address), len(data)))
|
logger.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
|
||||||
|
@ -669,7 +675,7 @@ async def snes_write(ctx : Context, write_list):
|
||||||
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
||||||
await ctx.snes_socket.send(cmd)
|
await ctx.snes_socket.send(cmd)
|
||||||
else:
|
else:
|
||||||
logging.warning(f"Could not send data to SNES: {cmd}")
|
logger.warning(f"Could not send data to SNES: {cmd}")
|
||||||
except websockets.ConnectionClosed:
|
except websockets.ConnectionClosed:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
|
@ -682,7 +688,7 @@ async def snes_write(ctx : Context, write_list):
|
||||||
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
||||||
await ctx.snes_socket.send(data)
|
await ctx.snes_socket.send(data)
|
||||||
else:
|
else:
|
||||||
logging.warning(f"Could not send data to SNES: {data}")
|
logger.warning(f"Could not send data to SNES: {data}")
|
||||||
except websockets.ConnectionClosed:
|
except websockets.ConnectionClosed:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -719,7 +725,7 @@ async def server_loop(ctx: Context, address=None):
|
||||||
ctx.ui_node.send_connection_status(ctx)
|
ctx.ui_node.send_connection_status(ctx)
|
||||||
cached_address = None
|
cached_address = None
|
||||||
if ctx.server and ctx.server.socket:
|
if ctx.server and ctx.server.socket:
|
||||||
ctx.ui_node.log_error('Already connected')
|
logger.error('Already connected')
|
||||||
return
|
return
|
||||||
|
|
||||||
if address is None: # set through CLI or APBP
|
if address is None: # set through CLI or APBP
|
||||||
|
@ -735,18 +741,18 @@ async def server_loop(ctx: Context, address=None):
|
||||||
|
|
||||||
# Wait for the user to provide a multiworld server address
|
# Wait for the user to provide a multiworld server address
|
||||||
if not address:
|
if not address:
|
||||||
logging.info('Please connect to a multiworld server.')
|
logger.info('Please connect to a multiworld server.')
|
||||||
ctx.ui_node.poll_for_server_ip()
|
ctx.ui_node.poll_for_server_ip()
|
||||||
return
|
return
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
ctx.ui_node.log_info('Connecting to multiworld server at %s' % address)
|
logger.info('Connecting to multiworld server at %s' % address)
|
||||||
try:
|
try:
|
||||||
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None)
|
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None)
|
||||||
ctx.server = Endpoint(socket)
|
ctx.server = Endpoint(socket)
|
||||||
ctx.ui_node.log_info('Connected')
|
logger.info('Connected')
|
||||||
ctx.server_address = address
|
ctx.server_address = address
|
||||||
ctx.ui_node.send_connection_status(ctx)
|
ctx.ui_node.send_connection_status(ctx)
|
||||||
SERVER_RECONNECT_DELAY = START_RECONNECT_DELAY
|
SERVER_RECONNECT_DELAY = START_RECONNECT_DELAY
|
||||||
|
@ -754,21 +760,21 @@ async def server_loop(ctx: Context, address=None):
|
||||||
for msg in loads(data):
|
for msg in 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)
|
||||||
ctx.ui_node.log_warning('Disconnected from multiworld server, type /connect to reconnect')
|
logger.warning('Disconnected from multiworld server, type /connect to reconnect')
|
||||||
except WebUI.WaitingForUiException:
|
except WebUI.WaitingForUiException:
|
||||||
pass
|
pass
|
||||||
except ConnectionRefusedError:
|
except ConnectionRefusedError:
|
||||||
if cached_address:
|
if cached_address:
|
||||||
ctx.ui_node.log_error('Unable to connect to multiworld server at cached address. '
|
logger.error('Unable to connect to multiworld server at cached address. '
|
||||||
'Please use the connect button above.')
|
'Please use the connect button above.')
|
||||||
else:
|
else:
|
||||||
ctx.ui_node.log_error('Connection refused by the multiworld server')
|
logger.error('Connection refused by the multiworld server')
|
||||||
except (OSError, websockets.InvalidURI):
|
except (OSError, websockets.InvalidURI):
|
||||||
ctx.ui_node.log_error('Failed to connect to the multiworld server')
|
logger.error('Failed to connect to the multiworld server')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
ctx.ui_node.log_error('Lost connection to the multiworld server, type /connect to reconnect')
|
logger.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)
|
logger.exception(e)
|
||||||
finally:
|
finally:
|
||||||
ctx.awaiting_rom = False
|
ctx.awaiting_rom = False
|
||||||
ctx.auth = None
|
ctx.auth = None
|
||||||
|
@ -780,7 +786,7 @@ async def server_loop(ctx: Context, address=None):
|
||||||
ctx.server = None
|
ctx.server = None
|
||||||
ctx.server_task = None
|
ctx.server_task = None
|
||||||
if ctx.server_address:
|
if ctx.server_address:
|
||||||
ctx.ui_node.log_info(f"... reconnecting in {SERVER_RECONNECT_DELAY}s")
|
logger.info(f"... reconnecting in {SERVER_RECONNECT_DELAY}s")
|
||||||
ctx.ui_node.send_connection_status(ctx)
|
ctx.ui_node.send_connection_status(ctx)
|
||||||
asyncio.create_task(server_autoreconnect(ctx))
|
asyncio.create_task(server_autoreconnect(ctx))
|
||||||
SERVER_RECONNECT_DELAY *= 2
|
SERVER_RECONNECT_DELAY *= 2
|
||||||
|
@ -797,18 +803,18 @@ 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':
|
||||||
ctx.ui_node.log_info('--------------------------------')
|
logger.info('--------------------------------')
|
||||||
ctx.ui_node.log_info('Room Information:')
|
logger.info('Room Information:')
|
||||||
ctx.ui_node.log_info('--------------------------------')
|
logger.info('--------------------------------')
|
||||||
version = args["version"]
|
version = args["version"]
|
||||||
ctx.server_version = tuple(version)
|
ctx.server_version = tuple(version)
|
||||||
version = ".".join(str(item) for item in version)
|
version = ".".join(str(item) for item in version)
|
||||||
|
|
||||||
ctx.ui_node.log_info(f'Server protocol version: {version}')
|
logger.info(f'Server protocol version: {version}')
|
||||||
if "tags" in args:
|
if "tags" in args:
|
||||||
ctx.ui_node.log_info("Server protocol tags: " + ", ".join(args["tags"]))
|
logger.info("Server protocol tags: " + ", ".join(args["tags"]))
|
||||||
if args['password']:
|
if args['password']:
|
||||||
ctx.ui_node.log_info('Password required')
|
logger.info('Password required')
|
||||||
if "forfeit_mode" in args: # could also be version > 2.2.1, but going with implicit content here
|
if "forfeit_mode" in args: # could also be version > 2.2.1, but going with implicit content here
|
||||||
logging.info(f"Forfeit setting: {args['forfeit_mode']}")
|
logging.info(f"Forfeit setting: {args['forfeit_mode']}")
|
||||||
logging.info(f"Remaining setting: {args['remaining_mode']}")
|
logging.info(f"Remaining setting: {args['remaining_mode']}")
|
||||||
|
@ -820,16 +826,16 @@ async def process_server_cmd(ctx: Context, cmd, args):
|
||||||
ctx.remaining_mode = args['remaining_mode']
|
ctx.remaining_mode = args['remaining_mode']
|
||||||
ctx.ui_node.send_game_info(ctx)
|
ctx.ui_node.send_game_info(ctx)
|
||||||
if len(args['players']) < 1:
|
if len(args['players']) < 1:
|
||||||
ctx.ui_node.log_info('No player connected')
|
logger.info('No player connected')
|
||||||
else:
|
else:
|
||||||
args['players'].sort()
|
args['players'].sort()
|
||||||
current_team = -1
|
current_team = -1
|
||||||
ctx.ui_node.log_info('Connected players:')
|
logger.info('Connected players:')
|
||||||
for team, slot, name in args['players']:
|
for team, slot, name in args['players']:
|
||||||
if team != current_team:
|
if team != current_team:
|
||||||
ctx.ui_node.log_info(f' Team #{team + 1}')
|
logger.info(f' Team #{team + 1}')
|
||||||
current_team = team
|
current_team = team
|
||||||
ctx.ui_node.log_info(' %s (Player %d)' % (name, slot))
|
logger.info(' %s (Player %d)' % (name, slot))
|
||||||
await server_auth(ctx, args['password'])
|
await server_auth(ctx, args['password'])
|
||||||
|
|
||||||
elif cmd == 'ConnectionRefused':
|
elif cmd == 'ConnectionRefused':
|
||||||
|
@ -845,7 +851,7 @@ async def process_server_cmd(ctx: Context, cmd, args):
|
||||||
raise Exception('Server reported your client version as incompatible')
|
raise Exception('Server reported your client version as incompatible')
|
||||||
#last to check, recoverable problem
|
#last to check, recoverable problem
|
||||||
elif 'InvalidPassword' in args:
|
elif 'InvalidPassword' in args:
|
||||||
ctx.ui_node.log_error('Invalid password')
|
logger.error('Invalid password')
|
||||||
ctx.password = None
|
ctx.password = None
|
||||||
await server_auth(ctx, True)
|
await server_auth(ctx, True)
|
||||||
else:
|
else:
|
||||||
|
@ -855,7 +861,7 @@ async def process_server_cmd(ctx: Context, cmd, args):
|
||||||
elif cmd == 'Connected':
|
elif cmd == 'Connected':
|
||||||
if ctx.send_unsafe:
|
if ctx.send_unsafe:
|
||||||
ctx.send_unsafe = False
|
ctx.send_unsafe = False
|
||||||
ctx.ui_node.log_info(f'Turning off sending of ALL location checks not declared as missing. If you want it on, please use /send_unsafe true')
|
logger.info(f'Turning off sending of ALL location checks not declared as missing. If you want it on, please use /send_unsafe true')
|
||||||
Utils.persistent_store("servers", ctx.rom, ctx.server_address)
|
Utils.persistent_store("servers", ctx.rom, ctx.server_address)
|
||||||
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]}
|
||||||
|
@ -894,7 +900,7 @@ async def process_server_cmd(ctx: Context, cmd, args):
|
||||||
if location not in ctx.locations_info:
|
if location not in ctx.locations_info:
|
||||||
replacements = {0xA2: 'Small Key', 0x9D: 'Big Key', 0x8D: 'Compass', 0x7D: 'Map'}
|
replacements = {0xA2: 'Small Key', 0x9D: 'Big Key', 0x8D: 'Compass', 0x7D: 'Map'}
|
||||||
item_name = replacements.get(item, get_item_name_from_id(item))
|
item_name = replacements.get(item, get_item_name_from_id(item))
|
||||||
ctx.ui_node.log_info(
|
logger.info(
|
||||||
f"Saw {color(item_name, 'red', 'bold')} at {list(Regions.location_table.keys())[location - 1]}")
|
f"Saw {color(item_name, 'red', 'bold')} at {list(Regions.location_table.keys())[location - 1]}")
|
||||||
ctx.locations_info[location] = (item, player)
|
ctx.locations_info[location] = (item, player)
|
||||||
ctx.watcher_event.set()
|
ctx.watcher_event.set()
|
||||||
|
@ -945,13 +951,13 @@ async def process_server_cmd(ctx: Context, cmd, args):
|
||||||
ctx.player_names = {p: n for p, n in args}
|
ctx.player_names = {p: n for p, n in args}
|
||||||
|
|
||||||
elif cmd == 'Print':
|
elif cmd == 'Print':
|
||||||
ctx.ui_node.log_info(args)
|
logger.info(args)
|
||||||
|
|
||||||
elif cmd == 'HintPointUpdate':
|
elif cmd == 'HintPointUpdate':
|
||||||
ctx.hint_points = args[0]
|
ctx.hint_points = args[0]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logging.debug(f"unknown command {args}")
|
logger.debug(f"unknown command {args}")
|
||||||
|
|
||||||
|
|
||||||
def get_tags(ctx: Context):
|
def get_tags(ctx: Context):
|
||||||
|
@ -963,11 +969,11 @@ def get_tags(ctx: Context):
|
||||||
|
|
||||||
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:
|
||||||
ctx.ui_node.log_info('Enter the password required to join this game:')
|
logger.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
|
||||||
ctx.ui_node.log_info(
|
logger.info(
|
||||||
'No ROM detected, awaiting snes connection to authenticate to the multiworld server (/snes)')
|
'No ROM detected, awaiting snes connection to authenticate to the multiworld server (/snes)')
|
||||||
return
|
return
|
||||||
ctx.awaiting_rom = False
|
ctx.awaiting_rom = False
|
||||||
|
@ -997,7 +1003,7 @@ class ClientCommandProcessor(CommandProcessor):
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
|
|
||||||
def output(self, text: str):
|
def output(self, text: str):
|
||||||
self.ctx.ui_node.log_info(text)
|
logger.info(text)
|
||||||
|
|
||||||
def _cmd_exit(self) -> bool:
|
def _cmd_exit(self) -> bool:
|
||||||
"""Close connections and client"""
|
"""Close connections and client"""
|
||||||
|
@ -1034,7 +1040,7 @@ class ClientCommandProcessor(CommandProcessor):
|
||||||
|
|
||||||
def _cmd_received(self) -> bool:
|
def _cmd_received(self) -> bool:
|
||||||
"""List all received items"""
|
"""List all received items"""
|
||||||
self.ctx.ui_node.log_info('Received items:')
|
logger.info('Received items:')
|
||||||
for index, item in enumerate(self.ctx.items_received, 1):
|
for index, item in enumerate(self.ctx.items_received, 1):
|
||||||
self.ctx.ui_node.notify_item_received(self.ctx.player_names[item.player], get_item_name_from_id(item.item),
|
self.ctx.ui_node.notify_item_received(self.ctx.player_names[item.player], get_item_name_from_id(item.item),
|
||||||
get_location_name_from_address(item.location), index,
|
get_location_name_from_address(item.location), index,
|
||||||
|
@ -1086,7 +1092,7 @@ class ClientCommandProcessor(CommandProcessor):
|
||||||
self.ctx.found_items = toggle.lower() in {"1", "true", "on"}
|
self.ctx.found_items = toggle.lower() in {"1", "true", "on"}
|
||||||
else:
|
else:
|
||||||
self.ctx.found_items = not self.ctx.found_items
|
self.ctx.found_items = not self.ctx.found_items
|
||||||
self.ctx.ui_node.log_info(f"Set showing team items to {self.ctx.found_items}")
|
logger.info(f"Set showing team items to {self.ctx.found_items}")
|
||||||
asyncio.create_task(self.ctx.send_msgs([['UpdateTags', get_tags(self.ctx)]]))
|
asyncio.create_task(self.ctx.send_msgs([['UpdateTags', get_tags(self.ctx)]]))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -1097,7 +1103,7 @@ class ClientCommandProcessor(CommandProcessor):
|
||||||
else:
|
else:
|
||||||
self.ctx.slow_mode = not self.ctx.slow_mode
|
self.ctx.slow_mode = not self.ctx.slow_mode
|
||||||
|
|
||||||
self.ctx.ui_node.log_info(f"Setting slow mode to {self.ctx.slow_mode}")
|
logger.info(f"Setting slow mode to {self.ctx.slow_mode}")
|
||||||
|
|
||||||
def _cmd_web(self):
|
def _cmd_web(self):
|
||||||
if self.ctx.webui_socket_port:
|
if self.ctx.webui_socket_port:
|
||||||
|
@ -1109,9 +1115,9 @@ class ClientCommandProcessor(CommandProcessor):
|
||||||
"""Force sending of locations the server did not specify was actually missing. WARNING: This may brick online trackers. Turned off on reconnect."""
|
"""Force sending of locations the server did not specify was actually missing. WARNING: This may brick online trackers. Turned off on reconnect."""
|
||||||
if toggle:
|
if toggle:
|
||||||
self.ctx.send_unsafe = toggle.lower() in {"1", "true", "on"}
|
self.ctx.send_unsafe = toggle.lower() in {"1", "true", "on"}
|
||||||
self.ctx.ui_node.log_info(f'Turning {("on" if self.ctx.send_unsafe else "off")} the option to send ALL location checks to the multiserver.')
|
logger.info(f'Turning {("on" if self.ctx.send_unsafe else "off")} the option to send ALL location checks to the multiserver.')
|
||||||
else:
|
else:
|
||||||
self.ctx.ui_node.log_info("You must specify /send_unsafe true explicitly.")
|
logger.info("You must specify /send_unsafe true explicitly.")
|
||||||
self.ctx.send_unsafe = False
|
self.ctx.send_unsafe = False
|
||||||
|
|
||||||
def default(self, raw: str):
|
def default(self, raw: str):
|
||||||
|
@ -1144,7 +1150,7 @@ async def track_locations(ctx : Context, roomid, roomdata):
|
||||||
|
|
||||||
def new_check(location):
|
def new_check(location):
|
||||||
ctx.unsafe_locations_checked.add(location)
|
ctx.unsafe_locations_checked.add(location)
|
||||||
ctx.ui_node.log_info("New check: %s (%d/216)" % (location, len(ctx.unsafe_locations_checked)))
|
logging.info("New check: %s (%d/216)" % (location, len(ctx.unsafe_locations_checked)))
|
||||||
ctx.ui_node.send_location_check(ctx, location)
|
ctx.ui_node.send_location_check(ctx, location)
|
||||||
|
|
||||||
for location, (loc_roomid, loc_mask) in location_table_uw.items():
|
for location, (loc_roomid, loc_mask) in location_table_uw.items():
|
||||||
|
@ -1152,7 +1158,7 @@ async def track_locations(ctx : Context, roomid, roomdata):
|
||||||
if location not in ctx.unsafe_locations_checked and loc_roomid == roomid and (roomdata << 4) & loc_mask != 0:
|
if location not in ctx.unsafe_locations_checked and loc_roomid == roomid and (roomdata << 4) & loc_mask != 0:
|
||||||
new_check(location)
|
new_check(location)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
ctx.ui_node.log_info(f"Exception: {e}")
|
logger.exception(f"Exception: {e}")
|
||||||
|
|
||||||
uw_begin = 0x129
|
uw_begin = 0x129
|
||||||
uw_end = 0
|
uw_end = 0
|
||||||
|
@ -1215,7 +1221,7 @@ async def send_finished_game(ctx: Context):
|
||||||
await ctx.send_msgs([['GameFinished', '']])
|
await ctx.send_msgs([['GameFinished', '']])
|
||||||
ctx.finished_game = True
|
ctx.finished_game = True
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logging.exception(ex)
|
logger.exception(ex)
|
||||||
|
|
||||||
|
|
||||||
async def game_watcher(ctx : Context):
|
async def game_watcher(ctx : Context):
|
||||||
|
@ -1245,7 +1251,7 @@ async def game_watcher(ctx : Context):
|
||||||
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:
|
||||||
ctx.ui_node.log_warning("ROM change detected, please reconnect to the multiworld server")
|
logger.warning("ROM change detected, please reconnect to the multiworld server")
|
||||||
await ctx.disconnect()
|
await ctx.disconnect()
|
||||||
|
|
||||||
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
||||||
|
@ -1309,7 +1315,7 @@ async def game_watcher(ctx : Context):
|
||||||
|
|
||||||
if scout_location > 0 and scout_location not in ctx.locations_scouted:
|
if scout_location > 0 and scout_location not in ctx.locations_scouted:
|
||||||
ctx.locations_scouted.add(scout_location)
|
ctx.locations_scouted.add(scout_location)
|
||||||
ctx.ui_node.log_info(f'Scouting item at {list(Regions.location_table.keys())[scout_location - 1]}')
|
logger.info(f'Scouting item at {list(Regions.location_table.keys())[scout_location - 1]}')
|
||||||
await ctx.send_msgs([['LocationScouts', [scout_location]]])
|
await ctx.send_msgs([['LocationScouts', [scout_location]]])
|
||||||
await track_locations(ctx, roomid, roomdata)
|
await track_locations(ctx, roomid, roomdata)
|
||||||
|
|
||||||
|
|
|
@ -560,7 +560,7 @@ def roll_settings(weights, plando_options: typing.Set[str] = frozenset(("bosses"
|
||||||
|
|
||||||
ret.plando_items = []
|
ret.plando_items = []
|
||||||
if "items" in plando_options:
|
if "items" in plando_options:
|
||||||
default_placement = PlandoItem(item="", location="")
|
|
||||||
def add_plando_item(item: str, location: str):
|
def add_plando_item(item: str, location: str):
|
||||||
if item not in item_table:
|
if item not in item_table:
|
||||||
raise Exception(f"Could not plando item {item} as the item was not recognized")
|
raise Exception(f"Could not plando item {item} as the item was not recognized")
|
||||||
|
@ -571,9 +571,9 @@ def roll_settings(weights, plando_options: typing.Set[str] = frozenset(("bosses"
|
||||||
options = weights.get("plando_items", [])
|
options = weights.get("plando_items", [])
|
||||||
for placement in options:
|
for placement in options:
|
||||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||||
from_pool = get_choice("from_pool", placement, default_placement.from_pool)
|
from_pool = get_choice("from_pool", placement, PlandoItem._field_defaults["from_pool"])
|
||||||
location_world = get_choice("world", placement, default_placement.world)
|
location_world = get_choice("world", placement, PlandoItem._field_defaults["world"])
|
||||||
force = get_choice("force", placement, default_placement.force)
|
force = str(get_choice("force", placement, PlandoItem._field_defaults["force"])).lower()
|
||||||
if "items" in placement and "locations" in placement:
|
if "items" in placement and "locations" in placement:
|
||||||
items = placement["items"]
|
items = placement["items"]
|
||||||
locations = placement["locations"]
|
locations = placement["locations"]
|
||||||
|
|
|
@ -13,6 +13,7 @@ class Node:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.endpoints = []
|
self.endpoints = []
|
||||||
|
super(Node, self).__init__()
|
||||||
|
|
||||||
def broadcast_all(self, msgs):
|
def broadcast_all(self, msgs):
|
||||||
msgs = self.dumper(msgs)
|
msgs = self.dumper(msgs)
|
||||||
|
|
|
@ -69,6 +69,20 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Plando Tutorial",
|
||||||
|
"description": "A guide to creating Multiworld Plandos",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"language": "English",
|
||||||
|
"filename": "zelda3/plando_en.md",
|
||||||
|
"link": "zelda3/plando/en",
|
||||||
|
"authors": [
|
||||||
|
"Berserker"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,17 +18,17 @@
|
||||||
it defaults to vanilla
|
it defaults to vanilla
|
||||||
- Instructions are separated by a semicolon
|
- Instructions are separated by a semicolon
|
||||||
- Available Instructions:
|
- Available Instructions:
|
||||||
- Direct Placement:
|
- Direct Placement:
|
||||||
- Example: "Eastern Palace-Trinexx"
|
- Example: "Eastern Palace-Trinexx"
|
||||||
- Takes a particular Arena and particular boss, then places that boss into that arena
|
- Takes a particular Arena and particular boss, then places that boss into that arena
|
||||||
- Ganons Tower has 3 placements, "Ganons Tower Top", "Ganons Tower Middle" and "Ganons Tower Bottom"
|
- Ganons Tower has 3 placements, "Ganons Tower Top", "Ganons Tower Middle" and "Ganons Tower Bottom"
|
||||||
- Boss Placement:
|
- Boss Placement:
|
||||||
- Example: "Trinexx"
|
- Example: "Trinexx"
|
||||||
- Takes a particular boss and places that boss in any remaining slots in which this boss can function.
|
- Takes a particular boss and places that boss in any remaining slots in which this boss can function.
|
||||||
- In this example, it would fill Desert Palace, but not Tower of Hera.
|
- In this example, it would fill Desert Palace, but not Tower of Hera.
|
||||||
- Boss Shuffle:
|
- Boss Shuffle:
|
||||||
- Example: "simple"
|
- Example: "simple"
|
||||||
- Runs a particular boss shuffle mode to finish construction instead of vanilla placement, typically used as a last instruction.
|
- Runs a particular boss shuffle mode to finish construction instead of vanilla placement, typically used as a last instruction.
|
||||||
- [Available Bosses](https://github.com/Berserker66/MultiWorld-Utilities/blob/65fa39df95c90c9b66141aee8b16b7e560d00819/Bosses.py#L135)
|
- [Available Bosses](https://github.com/Berserker66/MultiWorld-Utilities/blob/65fa39df95c90c9b66141aee8b16b7e560d00819/Bosses.py#L135)
|
||||||
- [Available Arenas](https://github.com/Berserker66/MultiWorld-Utilities/blob/65fa39df95c90c9b66141aee8b16b7e560d00819/Bosses.py#L186)
|
- [Available Arenas](https://github.com/Berserker66/MultiWorld-Utilities/blob/65fa39df95c90c9b66141aee8b16b7e560d00819/Bosses.py#L186)
|
||||||
|
|
||||||
|
@ -47,20 +47,83 @@ boss_shuffle:
|
||||||
4. A Trinexx -> Kholdstare Singularity that prevents ice Trinexx in GT
|
4. A Trinexx -> Kholdstare Singularity that prevents ice Trinexx in GT
|
||||||
|
|
||||||
|
|
||||||
|
### Items
|
||||||
|
- This module is disabled by default.
|
||||||
|
- Has the options from_pool, world, percentage, force and either item and location or items and locations
|
||||||
|
- All of these options support subweights
|
||||||
|
- percentage is the percentage chance for this block to trigger
|
||||||
|
- is a number in the range [0, 100], can be omitted entirely for 100%
|
||||||
|
- from_pool denotes if the item should be taken from the item pool, or be an additional item entirely.
|
||||||
|
- can be true or false, defaults to true when omitted
|
||||||
|
- world is the target world to place the item
|
||||||
|
- ignored if only one world is generated
|
||||||
|
- can be a number, to target that slot in the multiworld
|
||||||
|
- can be a name, to target that player's world
|
||||||
|
- can be true, to target any other player's world
|
||||||
|
- can be false, to target own world and is the default
|
||||||
|
- can be null, to target a random world
|
||||||
|
- force is either "silent", "true" or "false".
|
||||||
|
- "true" means the item has to be placed, or the generator aborts with an exception.
|
||||||
|
- "false" means the generator logs a warning if the placement can't be done.
|
||||||
|
- "silent" means that this entry is entirely ignored if the placement fails and is the default.
|
||||||
|
- Single Placement
|
||||||
|
- place a single item at a single location
|
||||||
|
- item denotes the Item to place
|
||||||
|
- location denotes the Location to place it into
|
||||||
|
- Multi Placement
|
||||||
|
- place multiple items into multiple locations, until either list is exhausted.
|
||||||
|
- items denotes the items to use, can be given a number to have multiple of that item
|
||||||
|
- locations lists the possible locations those items can be placed in
|
||||||
|
- placements are picked randomly, not sorted in any way
|
||||||
|
- [Available Items](https://github.com/Berserker66/MultiWorld-Utilities/blob/3b5ba161dea223b96e9b1fc890e03469d9c6eb59/Items.py#L26)
|
||||||
|
- [Available Locations](https://github.com/Berserker66/MultiWorld-Utilities/blob/3b5ba161dea223b96e9b1fc890e03469d9c6eb59/Regions.py#L418)
|
||||||
|
|
||||||
### Text
|
#### Examples
|
||||||
|
```yaml
|
||||||
|
plando_items:
|
||||||
|
- item:
|
||||||
|
Lamp: 1
|
||||||
|
Fire Rod: 1
|
||||||
|
location: Link's House
|
||||||
|
from_pool: true
|
||||||
|
world: true
|
||||||
|
percentage: 50
|
||||||
|
- items:
|
||||||
|
Progressive Sword: 4
|
||||||
|
Progressive Bow: 1
|
||||||
|
Progressive Bow (Alt): 1
|
||||||
|
locations:
|
||||||
|
- Desert Palace - Big Chest
|
||||||
|
- Eastern Palace - Big Chest
|
||||||
|
- Tower of Hera - Big Chest
|
||||||
|
- Swamp Palace - Big Chest
|
||||||
|
- Thieves' Town - Big Chest
|
||||||
|
- Skull Woods - Big Chest
|
||||||
|
- Ice Palace - Big Chest
|
||||||
|
- Misery Mire - Big Chest
|
||||||
|
- Turtle Rock - Big Chest
|
||||||
|
- Palace of Darkness - Big Chest
|
||||||
|
world: false
|
||||||
|
```
|
||||||
|
|
||||||
|
The first example has a 50% chance to occur, which if it does places either the Lamp or Fire Rod in one's own
|
||||||
|
Link's House and removes the picked item from the item pool.
|
||||||
|
|
||||||
|
The second example always triggers and places the Swords and Bows into one's own Big Chests
|
||||||
|
|
||||||
|
### Texts
|
||||||
- This module is disabled by default.
|
- This module is disabled by default.
|
||||||
- Has the options "text", "at" and "percentage"
|
- Has the options "text", "at" and "percentage"
|
||||||
- percentage is the percentage chance for this text to be placed, can be omitted entirely for 100%
|
- percentage is the percentage chance for this text to be placed, can be omitted entirely for 100%
|
||||||
- text is the text to be placed.
|
- text is the text to be placed.
|
||||||
- can be weighted.
|
- can be weighted.
|
||||||
- \n is a newline.
|
- \n is a newline.
|
||||||
- @ is the entered player's name.
|
- @ is the entered player's name.
|
||||||
- Warning: Text Mapper does not support full unicode.
|
- Warning: Text Mapper does not support full unicode.
|
||||||
- [Alphabet](https://github.com/Berserker66/MultiWorld-Utilities/blob/65fa39df95c90c9b66141aee8b16b7e560d00819/Text.py#L756)
|
- [Alphabet](https://github.com/Berserker66/MultiWorld-Utilities/blob/65fa39df95c90c9b66141aee8b16b7e560d00819/Text.py#L756)
|
||||||
- at is the location within the game to attach the text to.
|
- at is the location within the game to attach the text to.
|
||||||
- can be weighted.
|
- can be weighted.
|
||||||
- [List of targets](https://github.com/Berserker66/MultiWorld-Utilities/blob/65fa39df95c90c9b66141aee8b16b7e560d00819/Text.py#L1498)
|
- [List of targets](https://github.com/Berserker66/MultiWorld-Utilities/blob/65fa39df95c90c9b66141aee8b16b7e560d00819/Text.py#L1498)
|
||||||
|
|
||||||
#### Example
|
#### Example
|
||||||
```yaml
|
```yaml
|
||||||
|
@ -75,3 +138,32 @@ plando_texts:
|
||||||
This has a 50% chance to trigger at all, if it does,
|
This has a 50% chance to trigger at all, if it does,
|
||||||
it throws a coin between "uncle_leaving_text" and "uncle_dying_sewer", then places the text
|
it throws a coin between "uncle_leaving_text" and "uncle_dying_sewer", then places the text
|
||||||
"This is a plando.\nYou've been warned." at that location.
|
"This is a plando.\nYou've been warned." at that location.
|
||||||
|
|
||||||
|
### Connections
|
||||||
|
- This module is disabled by default.
|
||||||
|
- Has the options "percentage", "entrance", "exit" and "direction".
|
||||||
|
- All options support subweights
|
||||||
|
- percentage is the percentage chance for this to be connected, can be omitted entirely for 100%
|
||||||
|
- Any Door has 4 total directions, as a door can be unlinked like in insanity ER
|
||||||
|
- entrance is the overworld door
|
||||||
|
- exit is the underworld exit
|
||||||
|
- direction can be "both", "entrance" or "exit"
|
||||||
|
- doors can be found in [this file](https://github.com/Berserker66/MultiWorld-Utilities/blob/main/EntranceShuffle.py)
|
||||||
|
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```yaml
|
||||||
|
plando_connections:
|
||||||
|
- entrance: Links House
|
||||||
|
exit: Hyrule Castle Exit (West)
|
||||||
|
direction: both
|
||||||
|
- entrance: Hyrule Castle Entrance (West)
|
||||||
|
exit: Links House Exit
|
||||||
|
direction: both
|
||||||
|
```
|
||||||
|
|
||||||
|
The first block connects the overworld entrance that normally leads to Link's House
|
||||||
|
to put you into the HC West Wing instead, exiting from within there will put you at the Overworld exiting Link's House.
|
||||||
|
|
||||||
|
Without the second block, you'd still exit from within Link's House to outside Link's House and the left side
|
||||||
|
Balcony Entrance would still lead into HC West Wing
|
27
WebUI.py
27
WebUI.py
|
@ -1,6 +1,7 @@
|
||||||
import http.server
|
import http.server
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
import typing
|
||||||
import socket
|
import socket
|
||||||
import socketserver
|
import socketserver
|
||||||
import threading
|
import threading
|
||||||
|
@ -12,36 +13,20 @@ from NetUtils import Node
|
||||||
from MultiClient import Context
|
from MultiClient import Context
|
||||||
import Utils
|
import Utils
|
||||||
|
|
||||||
logger = logging.getLogger("WebUIRelay")
|
|
||||||
|
|
||||||
|
class WebUiClient(Node, logging.Handler):
|
||||||
class WebUiClient(Node):
|
|
||||||
loader = staticmethod(json.loads)
|
loader = staticmethod(json.loads)
|
||||||
dumper = staticmethod(json.dumps)
|
dumper = staticmethod(json.dumps)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super(WebUiClient, self).__init__()
|
||||||
self.manual_snes = None
|
self.manual_snes = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def build_message(msg_type: str, content: dict) -> dict:
|
def build_message(msg_type: str, content: typing.Union[str, dict]) -> dict:
|
||||||
return {'type': msg_type, 'content': content}
|
return {'type': msg_type, 'content': content}
|
||||||
|
|
||||||
def log_info(self, message, *args, **kwargs):
|
def emit(self, record: logging.LogRecord) -> None:
|
||||||
self.broadcast_all(self.build_message('info', message))
|
self.broadcast_all({"type": record.levelname.lower(), "content": str(record.msg)})
|
||||||
logger.info(message, *args, **kwargs)
|
|
||||||
|
|
||||||
def log_warning(self, message, *args, **kwargs):
|
|
||||||
self.broadcast_all(self.build_message('warning', message))
|
|
||||||
logger.warning(message, *args, **kwargs)
|
|
||||||
|
|
||||||
def log_error(self, message, *args, **kwargs):
|
|
||||||
self.broadcast_all(self.build_message('error', message))
|
|
||||||
logger.error(message, *args, **kwargs)
|
|
||||||
|
|
||||||
def log_critical(self, message, *args, **kwargs):
|
|
||||||
self.broadcast_all(self.build_message('critical', message))
|
|
||||||
logger.critical(message, *args, **kwargs)
|
|
||||||
|
|
||||||
def send_chat_message(self, message):
|
def send_chat_message(self, message):
|
||||||
self.broadcast_all(self.build_message('chat', message))
|
self.broadcast_all(self.build_message('chat', message))
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
colorama>=0.4.4
|
colorama>=0.4.4
|
||||||
websockets>=8.1
|
websockets>=8.1
|
||||||
PyYAML>=5.3.1
|
PyYAML>=5.4
|
||||||
fuzzywuzzy>=0.18.0
|
fuzzywuzzy>=0.18.0
|
||||||
bsdiff4>=1.2.0
|
bsdiff4>=1.2.0
|
||||||
prompt_toolkit>=3.0.8
|
prompt_toolkit>=3.0.10
|
||||||
appdirs>=1.4.4
|
appdirs>=1.4.4
|
||||||
maseya-z3pr>=1.0.0rc1
|
maseya-z3pr>=1.0.0rc1
|
||||||
xxtea>=2.0.0.post0
|
xxtea>=2.0.0.post0
|
||||||
|
|
|
@ -2251,7 +2251,10 @@ def plando_connect(world, player: int):
|
||||||
if world.plando_connections[player]:
|
if world.plando_connections[player]:
|
||||||
for connection in world.plando_connections[player]:
|
for connection in world.plando_connections[player]:
|
||||||
func = lookup[connection.direction]
|
func = lookup[connection.direction]
|
||||||
func(world, connection.entrance, connection.exit, player)
|
try:
|
||||||
|
func(world, connection.entrance, connection.exit, player)
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(f"Could not connect using {connection}") from e
|
||||||
|
|
||||||
|
|
||||||
LW_Dungeon_Entrances = ['Desert Palace Entrance (South)',
|
LW_Dungeon_Entrances = ['Desert Palace Entrance (South)',
|
||||||
|
|
|
@ -452,6 +452,7 @@ def copy_world(world):
|
||||||
ret.timer = world.timer.copy()
|
ret.timer = world.timer.copy()
|
||||||
ret.shufflepots = world.shufflepots.copy()
|
ret.shufflepots = world.shufflepots.copy()
|
||||||
ret.shuffle_prizes = world.shuffle_prizes.copy()
|
ret.shuffle_prizes = world.shuffle_prizes.copy()
|
||||||
|
ret.shop_shuffle = world.shop_shuffle.copy()
|
||||||
ret.dark_room_logic = world.dark_room_logic.copy()
|
ret.dark_room_logic = world.dark_room_logic.copy()
|
||||||
ret.restrict_dungeon_item_on_boss = world.restrict_dungeon_item_on_boss.copy()
|
ret.restrict_dungeon_item_on_boss = world.restrict_dungeon_item_on_boss.copy()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue