From 90359b62e60c894ed8a6206cbe73121a9f4a7294 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 12 Apr 2020 04:38:57 +0200 Subject: [PATCH 1/4] fix qusb2snes launch pathing --- MultiClient.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/MultiClient.py b/MultiClient.py index a18e7fc4..4d3cee04 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -347,15 +347,19 @@ async def snes_connect(ctx : Context, address): seen_problems.add(problem) logging.error(f"Error connecting to QUsb2snes ({problem})") 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 qusb2snes_path = Utils.get_options()["general_options"]["qusb2snes"] import os if os.path.isfile(qusb2snes_path): logging.info(f"Attempting to start {qusb2snes_path}") + else: + qusb2snes_path = Utils.local_path(qusb2snes_path) + if os.path.isfile(qusb2snes_path): import subprocess subprocess.Popen(qusb2snes_path, cwd=os.path.dirname(qusb2snes_path)) else: - logging.info(f"Attempt to start (Q)Usb2Snes was aborted as path {qusb2snes_path} was not found, please start it yourself if it is not running") + logging.info( + f"Attempt to start (Q)Usb2Snes was aborted as path {qusb2snes_path} was not found, please start it yourself if it is not running") await asyncio.sleep(1) else: From 1f8dc8d31732898f38973e0b22e8c785b7e0cff0 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 12 Apr 2020 04:44:03 +0200 Subject: [PATCH 2/4] clean qusb2snes launch code a bit --- MultiClient.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/MultiClient.py b/MultiClient.py index 4d3cee04..b09815a3 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -349,12 +349,13 @@ async def snes_connect(ctx : Context, address): if len(seen_problems) == 1: # this is the first problem. Let's try launching QUsb2snes if it isn't already running qusb2snes_path = Utils.get_options()["general_options"]["qusb2snes"] + import os + if not os.path.isfile(qusb2snes_path): + qusb2snes_path = Utils.local_path(qusb2snes_path) + if os.path.isfile(qusb2snes_path): logging.info(f"Attempting to start {qusb2snes_path}") - else: - qusb2snes_path = Utils.local_path(qusb2snes_path) - if os.path.isfile(qusb2snes_path): import subprocess subprocess.Popen(qusb2snes_path, cwd=os.path.dirname(qusb2snes_path)) else: From 34df144f8c564c7f6c7192a1afc1500d7fcce1d7 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sun, 12 Apr 2020 15:46:32 -0700 Subject: [PATCH 3/4] Changes (#64) * Fix bug where collected maps show on item menu if compass shuffle is on, (and collected compasses if map shuffle is on.) * Add Dungeon Counter Options --- EntranceRandomizer.py | 20 +++++++++++++++++++- Gui.py | 16 ++++++++++++++++ Main.py | 1 + Mystery.py | 2 ++ Rom.py | 10 ++++++---- easy.yaml | 5 +++++ 6 files changed, 49 insertions(+), 5 deletions(-) diff --git a/EntranceRandomizer.py b/EntranceRandomizer.py index f8f96a5e..71a8e474 100755 --- a/EntranceRandomizer.py +++ b/EntranceRandomizer.py @@ -111,6 +111,20 @@ def parse_arguments(argv, no_defaults=False): Timed mode. If time runs out, you lose (but can still keep playing). ''') + parser.add_argument('--dungeon_counters', default=defval('default'), const='default', nargs='?', choices=['default', 'on', 'pickup', 'off'], + help='''\ + Select dungeon counter display settings. (default: %(default)s) + (Note, since timer takes up the same space on the hud as dungeon + counters, timer settings override dungeon counter settings.) + Default: Dungeon counters only show when the compass is + picked up, or otherwise sent, only when compass + shuffle is turned on. + On: Dungeon counters are always displayed. + Pickup: Dungeon counters are shown when the compass is + picked up, even when compass shuffle is turned + off. + Off: Dungeon counters are never shown. + ''') parser.add_argument('--progressive', default=defval('on'), const='normal', nargs='?', choices=['on', 'off', 'random'], help='''\ Select progressive equipment setting. Affects available itempool. (default: %(default)s) @@ -285,6 +299,10 @@ def parse_arguments(argv, no_defaults=False): ret = parser.parse_args(argv) if ret.timer == "none": ret.timer = False + if ret.dungeon_counters == 'on': + ret.dungeon_counters = True + elif ret.dungeon_counters == 'off': + ret.dungeon_counters = False if ret.keysanity: ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = [True] * 4 @@ -300,7 +318,7 @@ def parse_arguments(argv, no_defaults=False): 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', - 'remote_items', 'progressive', 'extendedmsu']: + 'remote_items', 'progressive', 'extendedmsu', 'dungeon_counters']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) diff --git a/Gui.py b/Gui.py index 94b6e7c3..52e58f4b 100755 --- a/Gui.py +++ b/Gui.py @@ -289,6 +289,14 @@ def guiMain(args=None): timerLabel = Label(timerFrame, text='Timer setting') timerLabel.pack(side=LEFT) + dungeonCounterFrame = Frame(drowDownFrame) + dungeonCounterVar = StringVar() + dungeonCounterVar.set('auto') + dungeonCounterOptionMenu = OptionMenu(dungeonCounterFrame, dungeonCounterVar, 'auto', 'off', 'on', 'on_compass_pickup') + dungeonCounterOptionMenu.pack(side=RIGHT) + dungeonCounterLabel = Label(dungeonCounterFrame, text='Dungeon Chest Counters') + dungeonCounterLabel.pack(side=LEFT) + progressiveFrame = Frame(drowDownFrame) progressiveVar = StringVar() progressiveVar.set('on') @@ -330,6 +338,7 @@ def guiMain(args=None): difficultyFrame.pack(expand=True, anchor=E) itemfunctionFrame.pack(expand=True, anchor=E) timerFrame.pack(expand=True, anchor=E) + dungeonCounterFrame.pack(expand=True, anchor=E) progressiveFrame.pack(expand=True, anchor=E) accessibilityFrame.pack(expand=True, anchor=E) algorithmFrame.pack(expand=True, anchor=E) @@ -427,6 +436,13 @@ def guiMain(args=None): guiargs.timer = timerVar.get() if guiargs.timer == "none": guiargs.timer = False + guiargs.dungeon_counters = dungeonCounterVar.get() + if guiargs.dungeon_counters == "on_compass_pickup": + guiargs.dungeon_counters = "pickup" + elif guiargs.dungeon_counters == "on": + guiargs.dungeon_counters = True + elif guiargs.dungeon_counters == "off": + guiargs.dungeon_counters = False guiargs.progressive = progressiveVar.get() guiargs.accessibility = accessibilityVar.get() guiargs.algorithm = algorithmVar.get() diff --git a/Main.py b/Main.py index a68c29e3..afe1cbb3 100644 --- a/Main.py +++ b/Main.py @@ -57,6 +57,7 @@ def main(args, seed=None): world.timer = args.timer.copy() world.shufflepots = args.shufflepots.copy() world.progressive = args.progressive.copy() + world.dungeon_counters = args.dungeon_counters.copy() world.extendedmsu = args.extendedmsu.copy() world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)} diff --git a/Mystery.py b/Mystery.py index 114da3db..4a7ccd04 100644 --- a/Mystery.py +++ b/Mystery.py @@ -268,6 +268,8 @@ def roll_settings(weights): 'timed_countdown': 'timed-countdown', 'display': 'display'}[get_choice('timer', weights)] if 'timer' in weights.keys() else False + ret.dungeon_counters = get_choice('dungeon_counters', weights) if 'dungeon_counters' in weights else 'default' + ret.progressive = convert_to_on_off(get_choice('progressive', weights)) if "progressive" in weights else 'on' inventoryweights = weights.get('startinventory', {}) startitems = [] diff --git a/Rom.py b/Rom.py index ffc082ef..1b77af9e 100644 --- a/Rom.py +++ b/Rom.py @@ -1126,9 +1126,11 @@ def patch_rom(world, rom, player, team, enemized): rom.write_byte(0x18003B, 0x01 if world.mapshuffle[player] else 0x00) # maps showing crystals on overworld # compasses showing dungeon count - if world.clock_mode[player]: + if world.clock_mode[player] or not world.dungeon_counters[player]: rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location - elif world.compassshuffle[player]: + elif world.dungeon_counters[player] == True: + rom.write_byte(0x18003C, 0x02) # always on + elif world.compassshuffle[player] or world.dungeon_counters[player] == 'pickup': rom.write_byte(0x18003C, 0x01) # show on pickup else: rom.write_byte(0x18003C, 0x00) @@ -1143,8 +1145,8 @@ def patch_rom(world, rom, player, team, enemized): # rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] else 0x00) | (0x02 if world.bigkeyshuffle[player] else 0x00) - | (0x04 if world.compassshuffle[player] else 0x00) - | (0x08 if world.mapshuffle[player] else 0x00))) # free roaming items in menu + | (0x04 if world.mapshuffle[player] else 0x00) + | (0x08 if world.compassshuffle[player] else 0x00))) # free roaming items in menu # Map reveals reveal_bytes = { diff --git a/easy.yaml b/easy.yaml index 90bba920..bc9fd8a3 100644 --- a/easy.yaml +++ b/easy.yaml @@ -46,6 +46,11 @@ dungeon_items: # alternative to the 4 shuffles above this, does nothing until th mc: 0 # shuffle Maps and Compass none: 1 # shuffle none of the 4 mcsb: 0 # shuffle all of the 4, any combination of m, c, s and b will shuffle the respective item, or not if it's missing, so you can add more options here +dungeon_counters: + on: 0 # always display amount of items checked in a dungeon + pickup: 0 # show when compass is picked up + default: 1 # show when compass is picked up if the compass itself is shuffled + off: 0 # never show item count in dungeons accessibility: items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations locations: 1 # Guarantees you will be able to access all locations, and therefore all items From 8b8e2790158c91d46f840511ed0ba52832afa781 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 13 Apr 2020 11:26:50 +0200 Subject: [PATCH 4/4] server command processor some commands were renamed at this opportunity --- MultiServer.py | 214 ++++++++++++++++++++++++++++++------------------- 1 file changed, 133 insertions(+), 81 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 79778564..1fdd90ec 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -6,6 +6,7 @@ import logging import zlib import collections import typing +import inspect import ModuleUpdate @@ -66,6 +67,7 @@ class Context: self.hints_used = collections.defaultdict(int) self.hints_sent = collections.defaultdict(set) self.item_cheat = item_cheat + self.running = True def get_save(self) -> dict: return { @@ -124,7 +126,7 @@ def notify_client(client: Client, text: str): asyncio.create_task(send_msgs(client.socket, [['Print', text]])) -# separated out, due to compatibilty between client's +# separated out, due to compatibilty between clients def notify_hints(ctx: Context, team: int, hints: typing.List[Utils.Hint]): cmd = [["Hint", hints]] texts = [['Print', format_hint(ctx, team, hint)] for hint in hints] @@ -499,94 +501,145 @@ async def process_client_cmd(ctx: Context, client: Client, cmd, args): notify_client(client, response) -def set_password(ctx : Context, password): +def set_password(ctx: Context, password): ctx.password = password logging.warning('Password set to ' + password if password else 'Password disabled') +class CommandProcessor(): + commands: typing.Dict[str, typing.Callable] + + def __init__(self): + self.commands = {name[5:].lower(): method for name, method in inspect.getmembers(self) if + name.startswith("_cmd_")} + + def output(self, text: str): + print(text) + + def __call__(self, raw: str): + if not raw: + return + command = raw.split() + basecommand = command[0] + if basecommand[0] == "/": + method = self.commands.get(basecommand[1:].lower(), None) + if not method: + self._error_unknown_command(basecommand[1:]) + else: + method(*command[1:]) + else: + self.default(raw) + + def get_help_text(self) -> str: + s = "" + for command, method in self.commands.items(): + spec = inspect.signature(method).parameters + s += f"/{command} {' '.join(spec)}\n {method.__doc__}\n" + return s + + def _cmd_help(self): + """Returns the help listing""" + self.output(self.get_help_text()) + + def default(self, raw: str): + self.output("Echo: " + raw) + + def _error_unknown_command(self, raw: str): + self.output(f"Could not find command {raw}. Known commands: {', '.join(self.commands)}") + + +class ServerCommandProcessor(CommandProcessor): + ctx: Context + + def __init__(self, ctx: Context): + self.ctx = ctx + super(ServerCommandProcessor, self).__init__() + + def default(self, raw: str): + notify_all(self.ctx, '[Server]: ' + raw) + + def _cmd_kick(self, player_name: str): + """Kick specified player from the server""" + for client in self.ctx.clients: + if client.auth and client.name.lower() == player_name.lower() and client.socket and not client.socket.closed: + asyncio.create_task(client.socket.close()) + self.output(f"Kicked {client.name}") + break + else: + self.output("Could not find player to kick") + + def _cmd_players(self): + """Get information about connected players""" + self.output(get_connected_players_string(self.ctx)) + + def _cmd_exit(self): + """Shutdown the server""" + asyncio.create_task(self.ctx.server.ws_server._close()) + self.ctx.running = False + + def _cmd_password(self, new_password: str = ""): + """Set the server password. Leave the password text empty to remove the password""" + set_password(self.ctx, new_password if new_password else None) + + def _cmd_forfeit(self, player_name: str): + """Send out the remaining items from a player's game to their intended recipients""" + seeked_player = player_name.lower() + for (team, slot), name in self.ctx.player_names.items(): + if name.lower() == seeked_player: + forfeit_player(self.ctx, team, slot) + break + else: + self.output("Could not find player to forfeit") + + def _cmd_send(self, player_name: str, *item_name: str): + """Sends an item to the specified player""" + seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) + if usable: + item = " ".join(item_name) + item, usable, response = get_intended_text(item, Items.item_table.keys()) + if usable: + for client in self.ctx.clients: + if client.name == seeked_player: + new_item = ReceivedItem(Items.item_table[item][3], -1, client.slot) + get_received_items(self.ctx, client.team, client.slot).append(new_item) + notify_all(self.ctx, 'Cheat console: sending "' + item + '" to ' + client.name) + send_new_items(self.ctx) + return + else: + self.output(response) + else: + self.output(response) + + def _cmd_hint(self, player_name: str, *item_or_location: str): + """Send out a hint for a player's item or location to their team""" + seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) + if usable: + for (team, slot), name in self.ctx.player_names.items(): + if name == seeked_player: + item = " ".join(item_or_location) + item, usable, response = get_intended_text(item) + if usable: + if item in Items.item_table: # item name + hints = collect_hints(self.ctx, team, slot, item) + notify_hints(self.ctx, team, hints) + else: # location name + hints = collect_hints_location(self.ctx, team, slot, item) + notify_hints(self.ctx, team, hints) + else: + self.output(response) + return + else: + self.output(response) + + async def console(ctx: Context): session = prompt_toolkit.PromptSession() - running = True - while running: + cmd_processor = ServerCommandProcessor(ctx) + while ctx.running: with patch_stdout(): input_text = await session.prompt_async() try: - - command = input_text.split() - if not command: - continue - - if command[0] == '/exit': - await ctx.server.ws_server._close() - running = False - - if command[0] == '/players': - logging.info(get_connected_players_string(ctx)) - if command[0] == '/password': - set_password(ctx, command[1] if len(command) > 1 else None) - if command[0] == '/kick' and len(command) > 1: - team = int(command[2]) - 1 if len(command) > 2 and command[2].isdigit() else None - for client in ctx.clients: - if client.auth and client.name.lower() == command[1].lower() and (team is None or team == client.team): - if client.socket and not client.socket.closed: - await client.socket.close() - - if command[0] == '/forfeitslot' and len(command) > 1 and command[1].isdigit(): - if len(command) > 2 and command[2].isdigit(): - team = int(command[1]) - 1 - slot = int(command[2]) - else: - team = 0 - slot = int(command[1]) - forfeit_player(ctx, team, slot) - if command[0] == '/forfeitplayer' and len(command) > 1: - seeked_player = command[1].lower() - for (team, slot), name in ctx.player_names.items(): - if name.lower() == seeked_player: - forfeit_player(ctx, team, slot) - if command[0] == '/senditem': - if len(command) <= 2: - logging.info("Use /senditem {Playername} {itemname}\nFor example /senditem Berserker Lamp") - else: - seeked_player, usable, response = get_intended_text(command[1], ctx.player_names.values()) - if usable: - item = " ".join(command[2:]) - item, usable, response = get_intended_text(item, Items.item_table.keys()) - if usable: - for client in ctx.clients: - if client.name == seeked_player: - new_item = ReceivedItem(Items.item_table[item][3], -1, client.slot) - get_received_items(ctx, client.team, client.slot).append(new_item) - notify_all(ctx, 'Cheat console: sending "' + item + '" to ' + client.name) - send_new_items(ctx) - else: - logging.warning(response) - else: - logging.warning(response) - if command[0] == '/hint': - if len(command) <= 2: - logging.info("Use /hint {Playername} {itemname/locationname}\nFor example /hint Berserker Lamp") - else: - seeked_player, usable, response = get_intended_text(command[1], ctx.player_names.values()) - if usable: - for (team, slot), name in ctx.player_names.items(): - if name == seeked_player: - item = " ".join(command[2:]) - item, usable, response = get_intended_text(item) - if usable: - if item in Items.item_table: #item name - hints = collect_hints(ctx, team, slot, item) - notify_hints(ctx, team, hints) - else: #location name - hints = collect_hints_location(ctx, team, slot, item) - notify_hints(ctx, team, hints) - else: - logging.warning(response) - else: - logging.warning(response) - - if command[0][0] != '/': - notify_all(ctx, '[Server]: ' + input_text) + cmd_processor(input_text) except: import traceback traceback.print_exc() @@ -702,4 +755,3 @@ async def main(args: argparse.Namespace): if __name__ == '__main__': loop = asyncio.get_event_loop() loop.run_until_complete(main(parse_args())) - loop.close()