From 4f8c737eecd5738bcae0a1018b906bfc92c9e6a9 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 19 Oct 2020 08:26:31 +0200 Subject: [PATCH] squashed commit of many breaking changes Dropping Support for Python 3.7; adding support for Python 3.9 --- AdjusterMain.py | 2 +- Gui.py | 2 +- Main.py | 39 ++++---- ModuleUpdate.py | 4 + MultiClient.py | 122 +++++++++++------------- MultiMystery.py | 8 +- MultiServer.py | 79 +++++++-------- NetUtils.py | 13 +-- Patch.py | 6 +- Rom.py | 7 +- WebHostLib/downloads.py | 4 +- WebHostLib/generate.py | 2 +- WebHostLib/upload.py | 4 +- WebUI.py | 5 +- data/{basepatch.bmbp => basepatch.apbp} | Bin inno_setup.iss | 2 +- 16 files changed, 149 insertions(+), 150 deletions(-) rename data/{basepatch.bmbp => basepatch.apbp} (100%) diff --git a/AdjusterMain.py b/AdjusterMain.py index 6eac8e7c..167ab96a 100644 --- a/AdjusterMain.py +++ b/AdjusterMain.py @@ -11,7 +11,7 @@ def adjust(args): logger = logging.getLogger('Adjuster') logger.info('Patching ROM.') - if os.path.splitext(args.rom)[-1].lower() == '.bmbp': + if os.path.splitext(args.rom)[-1].lower() == '.apbp': import Patch meta, args.rom = Patch.create_rom_file(args.rom) diff --git a/Gui.py b/Gui.py index 1b8fb6a6..a488aae7 100755 --- a/Gui.py +++ b/Gui.py @@ -655,7 +655,7 @@ def guiMain(args=None): romEntry2 = Entry(romDialogFrame2, textvariable=romVar2) def RomSelect2(): - rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc", ".bmbp")), ("All Files", "*")]) + rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc", ".apbp")), ("All Files", "*")]) romVar2.set(rom) romSelectButton2 = Button(romDialogFrame2, text='Select Rom', command=RomSelect2) diff --git a/Main.py b/Main.py index 75d2bf19..51f76a3d 100644 --- a/Main.py +++ b/Main.py @@ -1,7 +1,6 @@ from collections import OrderedDict import copy from itertools import zip_longest -import json import logging import os import random @@ -170,7 +169,7 @@ def main(args, seed=None): logger.info('Patching ROM.') - outfilebase = 'BM_%s' % (args.outputname if args.outputname else world.seed) + outfilebase = 'AP_%s' % (args.outputname if args.outputname else world.seed) rom_names = [] @@ -258,7 +257,7 @@ def main(args, seed=None): rom.write_to_file(rompath, hide_enemizer=True) if args.create_diff: Patch.create_patch_file(rompath) - return player, team, bytes(rom.name).decode() + return player, team, bytes(rom.name) pool = concurrent.futures.ThreadPoolExecutor() multidata_task = None @@ -293,26 +292,26 @@ def main(args, seed=None): precollected_items[item.player - 1].append(item.code) def write_multidata(roms): + import base64 + import pickle for future in roms: rom_name = future.result() rom_names.append(rom_name) - multidata = zlib.compress(json.dumps({"names": parsed_names, - # backwards compat for < 2.4.1 - "roms": [(slot, team, list(name.encode())) - for (slot, team, name) in rom_names], - "rom_strings": rom_names, - "remote_items": [player for player in range(1, world.players + 1) if - world.remote_items[player]], - "locations": [((location.address, location.player), - (location.item.code, location.item.player)) - for location in world.get_filled_locations() if - type(location.address) is int], - "server_options": get_options()["server_options"], - "er_hint_data": er_hint_data, - "precollected_items": precollected_items, - "version": _version_tuple, - "tags": ["ER"] - }).encode("utf-8"), 9) + multidata = zlib.compress(pickle.dumps({"names": parsed_names, + "roms": {base64.b64encode(rom_name).decode(): (team, slot) for slot, team, rom_name in rom_names}, + "remote_items": {player for player in range(1, world.players + 1) if + world.remote_items[player]}, + "locations": { + (location.address, location.player) : + (location.item.code, location.item.player) + for location in world.get_filled_locations() if + type(location.address) is int}, + "server_options": get_options()["server_options"], + "er_hint_data": er_hint_data, + "precollected_items": precollected_items, + "version": _version_tuple, + "tags": ["AP"] + }), 9) with open(output_path('%s.multidata' % outfilebase), 'wb') as f: f.write(multidata) diff --git a/ModuleUpdate.py b/ModuleUpdate.py index 4093d537..e8f9846a 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -3,6 +3,10 @@ import sys import subprocess import importlib + +if sys.version_info < (3, 8, 6): + raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.") + update_ran = hasattr(sys, "frozen") and getattr(sys, "frozen") # don't run update if environment is frozen/compiled diff --git a/MultiClient.py b/MultiClient.py index dfb1e00d..2efcf4c9 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -1,6 +1,5 @@ import argparse import asyncio -import json import logging import urllib.parse import atexit @@ -13,6 +12,8 @@ import sys import typing import os import subprocess +import base64 +from json import loads, dumps from random import randrange @@ -113,7 +114,7 @@ class Context(): async def send_msgs(self, msgs): if not self.server or not self.server.socket.open or self.server.socket.closed: return - await self.server.socket.send(json.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, 'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43, @@ -427,17 +428,17 @@ async def get_snes_devices(ctx: Context): "Opcode": "DeviceList", "Space": "SNES" } - await socket.send(json.dumps(DeviceList_Request)) + await socket.send(dumps(DeviceList_Request)) - reply = json.loads(await socket.recv()) + reply = loads(await socket.recv()) devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None if not devices: ctx.ui_node.log_info('No SNES device found. Ensure QUsb2Snes is running and connect it to the multibridge.') while not devices: await asyncio.sleep(1) - await socket.send(json.dumps(DeviceList_Request)) - reply = json.loads(await socket.recv()) + await socket.send(dumps(DeviceList_Request)) + reply = loads(await socket.recv()) devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None ctx.ui_node.send_device_list(devices) @@ -481,7 +482,7 @@ async def snes_connect(ctx: Context, address): "Space": "SNES", "Operands": [device] } - await ctx.snes_socket.send(json.dumps(Attach_Request)) + await ctx.snes_socket.send(dumps(Attach_Request)) ctx.snes_state = SNES_ATTACHED ctx.snes_attached_device = (devices.index(device), device) ctx.ui_node.send_connection_status(ctx) @@ -489,8 +490,8 @@ async def snes_connect(ctx: Context, address): if 'sd2snes' in device.lower() or (len(device) == 4 and device[:3] == 'COM'): ctx.ui_node.log_info("SD2SNES Detected") ctx.is_sd2snes = True - await ctx.snes_socket.send(json.dumps({"Opcode" : "Info", "Space" : "SNES"})) - reply = json.loads(await ctx.snes_socket.recv()) + await ctx.snes_socket.send(dumps({"Opcode" : "Info", "Space" : "SNES"})) + reply = loads(await ctx.snes_socket.recv()) if reply and 'Results' in reply: ctx.ui_node.log_info(reply['Results']) else: @@ -501,7 +502,6 @@ async def snes_connect(ctx: Context, address): SNES_RECONNECT_DELAY = START_RECONNECT_DELAY except Exception as e: - if recv_task is not None: if not ctx.snes_socket.closed: await ctx.snes_socket.close() @@ -576,7 +576,7 @@ async def snes_read(ctx : Context, address, size): "Operands" : [hex(address)[2:], hex(size)[2:]] } try: - await ctx.snes_socket.send(json.dumps(GetAddress_Request)) + await ctx.snes_socket.send(dumps(GetAddress_Request)) except websockets.ConnectionClosed: return None @@ -633,7 +633,7 @@ async def snes_write(ctx : Context, write_list): PutAddress_Request['Operands'] = ["2C00", hex(len(cmd)-1)[2:], "2C00", "1"] try: if ctx.snes_socket is not None: - await ctx.snes_socket.send(json.dumps(PutAddress_Request)) + await ctx.snes_socket.send(dumps(PutAddress_Request)) if ctx.snes_socket is not None: await ctx.snes_socket.send(cmd) except websockets.ConnectionClosed: @@ -645,7 +645,7 @@ async def snes_write(ctx : Context, write_list): for address, data in write_list: PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]] if ctx.snes_socket is not None: - await ctx.snes_socket.send(json.dumps(PutAddress_Request)) + await ctx.snes_socket.send(dumps(PutAddress_Request)) if ctx.snes_socket is not None: await ctx.snes_socket.send(data) except websockets.ConnectionClosed: @@ -674,7 +674,7 @@ async def snes_flush_writes(ctx : Context): async def send_msgs(websocket, msgs): if not websocket or not websocket.open or websocket.closed: return - await websocket.send(json.dumps(msgs)) + await websocket.send(dumps(msgs)) async def server_loop(ctx: Context, address=None): @@ -685,16 +685,16 @@ async def server_loop(ctx: Context, address=None): ctx.ui_node.log_error('Already connected') return - if address is None: # set through CLI or BMBP + if address is None: # set through CLI or APBP address = ctx.server_address if address is None: # see if this is an old connection await asyncio.sleep(0.5) # wait for snes connection to succeed if possible. rom = ctx.rom if ctx.rom else None - try: - servers = cached_address = Utils.persistent_load()["servers"] - address = servers[rom] if rom and rom in servers else servers["default"] - except Exception as e: - logging.debug(f"Could not find cached server address. {e}") + + servers = Utils.persistent_load()["servers"] + if rom in servers: + address = servers[rom] + cached_address = True # Wait for the user to provide a multiworld server address if not address: @@ -714,7 +714,7 @@ async def server_loop(ctx: Context, address=None): ctx.ui_node.send_connection_status(ctx) SERVER_RECONNECT_DELAY = START_RECONNECT_DELAY async for data in ctx.server.socket: - for msg in json.loads(data): + for msg in loads(data): cmd, args = (msg[0], msg[1]) if len(msg) > 1 else (msg, None) await process_server_cmd(ctx, cmd, args) ctx.ui_node.log_warning('Disconnected from multiworld server, type /connect to reconnect') @@ -806,7 +806,6 @@ async def process_server_cmd(ctx: Context, cmd, args): raise Exception( 'Invalid ROM detected, please verify that you have loaded the correct rom and reconnect your snes (/snes)') if 'SlotAlreadyTaken' in args: - Utils.persistent_store("servers", "default", ctx.server_address) Utils.persistent_store("servers", ctx.rom, ctx.server_address) raise Exception('Player slot already in use for that team') if 'IncompatibleVersion' in args: @@ -814,7 +813,6 @@ async def process_server_cmd(ctx: Context, cmd, args): raise Exception('Connection refused by the multiworld host') elif cmd == 'Connected': - Utils.persistent_store("servers", "default", ctx.server_address) Utils.persistent_store("servers", ctx.rom, ctx.server_address) ctx.team, ctx.slot = args[0] ctx.player_names = {p: n for p, n in args[1]} @@ -873,12 +871,6 @@ async def process_server_cmd(ctx: Context, cmd, args): logging.info('%s found %s (%s)' % (player_sent, item, color(get_location_name_from_address(found.location), 'blue_bg', 'white'))) - elif cmd == 'Missing': - if 'locations' in args: - locations = json.loads(args['locations']) - for location in locations: - ctx.ui_node.log_info(f'Missing: {location}') - ctx.ui_node.log_info(f'Found {len(locations)} missing location checks') elif cmd == 'Hint': hints = [Utils.Hint(*hint) for hint in args] @@ -931,7 +923,7 @@ async def server_auth(ctx: Context, password_requested): return ctx.awaiting_rom = False ctx.auth = ctx.rom - auth = ctx.rom if ctx.server_version > (2, 4, 0) else list(ctx.rom.encode()) + auth = base64.b64encode(ctx.rom).decode() await ctx.send_msgs([['Connect', { 'password': ctx.password, 'rom': auth, 'version': Utils._version_tuple, 'tags': get_tags(ctx), 'uuid': Utils.get_unique_identifier() @@ -1156,7 +1148,7 @@ async def game_watcher(ctx : Context): if rom is None or rom == bytes([0] * ROMNAME_SIZE): continue - ctx.rom = rom.decode() + ctx.rom = rom if not ctx.prev_rom or ctx.prev_rom != ctx.rom: ctx.locations_checked = set() ctx.locations_scouted = set() @@ -1251,45 +1243,43 @@ async def websocket_server(websocket: websockets.WebSocketServerProtocol, path, process_command = ClientCommandProcessor(ctx) try: async for incoming_data in websocket: - try: - data = json.loads(incoming_data) - logging.debug(f"WebUIData:{data}") - if ('type' not in data) or ('content' not in data): - raise Exception('Invalid data received in websocket') + data = loads(incoming_data) + logging.debug(f"WebUIData:{data}") + if ('type' not in data) or ('content' not in data): + raise Exception('Invalid data received in websocket') - elif data['type'] == 'webStatus': - if data['content'] == 'connections': - ctx.ui_node.send_connection_status(ctx) - elif data['content'] == 'devices': - await get_snes_devices(ctx) - elif data['content'] == 'gameInfo': - ctx.ui_node.send_game_info(ctx) - elif data['content'] == 'checkData': - ctx.ui_node.send_location_check(ctx, 'Waiting for check...') + elif data['type'] == 'webStatus': + if data['content'] == 'connections': + ctx.ui_node.send_connection_status(ctx) + elif data['content'] == 'devices': + await get_snes_devices(ctx) + elif data['content'] == 'gameInfo': + ctx.ui_node.send_game_info(ctx) + elif data['content'] == 'checkData': + ctx.ui_node.send_location_check(ctx, 'Waiting for check...') - elif data['type'] == 'webConfig': - if 'serverAddress' in data['content']: - ctx.server_address = data['content']['serverAddress'] - await connect(ctx, data['content']['serverAddress']) - elif 'deviceId' in data['content']: - # Allow a SNES disconnect via UI sending -1 as new device - if data['content']['deviceId'] == "-1": - ctx.ui_node.manual_snes = None - ctx.snes_reconnect_address = None - await snes_disconnect(ctx) - else: - await snes_disconnect(ctx) - ctx.ui_node.manual_snes = data['content']['deviceId'] - await snes_connect(ctx, ctx.snes_address) + elif data['type'] == 'webConfig': + if 'serverAddress' in data['content']: + ctx.server_address = data['content']['serverAddress'] + await connect(ctx, data['content']['serverAddress']) + elif 'deviceId' in data['content']: + # Allow a SNES disconnect via UI sending -1 as new device + if data['content']['deviceId'] == "-1": + ctx.ui_node.manual_snes = None + ctx.snes_reconnect_address = None + await snes_disconnect(ctx) + else: + await snes_disconnect(ctx) + ctx.ui_node.manual_snes = data['content']['deviceId'] + await snes_connect(ctx, ctx.snes_address) - elif data['type'] == 'webControl': - if 'disconnect' in data['content']: - await ctx.disconnect() + elif data['type'] == 'webControl': + if 'disconnect' in data['content']: + await ctx.disconnect() + + elif data['type'] == 'webCommand': + process_command(data['content']) - elif data['type'] == 'webCommand': - process_command(data['content']) - except json.JSONDecodeError: - pass except Exception as e: if not isinstance(e, websockets.WebSocketException): logging.exception(e) diff --git a/MultiMystery.py b/MultiMystery.py index e51ba534..b460d267 100644 --- a/MultiMystery.py +++ b/MultiMystery.py @@ -121,8 +121,8 @@ if __name__ == "__main__": seedname = segment break - multidataname = f"BM_{seedname}.multidata" - spoilername = f"BM_{seedname}_Spoiler.txt" + multidataname = f"AP_{seedname}.multidata" + spoilername = f"AP_{seedname}_Spoiler.txt" romfilename = "" if player_name: @@ -158,7 +158,7 @@ if __name__ == "__main__": print(f"Removed {file} which is now present in the zipfile") - zipname = os.path.join(output_path, f"BM_{seedname}.{typical_zip_ending}") + zipname = os.path.join(output_path, f"AP_{seedname}.{typical_zip_ending}") print(f"Creating zipfile {zipname}") ipv4 = (host if host else get_public_ipv4()) + ":" + str(port) @@ -185,7 +185,7 @@ if __name__ == "__main__": if seedname in file: if file.endswith(".sfc"): futures.append(pool.submit(_handle_sfc_file, file)) - elif file.endswith(".bmbp"): + elif file.endswith(".apbp"): futures.append(pool.submit(_handle_diff_file, file)) if zip_multidata and os.path.exists(os.path.join(output_path, multidataname)): diff --git a/MultiServer.py b/MultiServer.py index 707e3142..69923d78 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -3,7 +3,6 @@ from __future__ import annotations import argparse import asyncio import functools -import json import logging import zlib import collections @@ -13,6 +12,8 @@ import weakref import datetime import threading import random +import pickle +from json import loads, dumps import ModuleUpdate @@ -26,7 +27,8 @@ from fuzzywuzzy import process as fuzzy_process import Items import Regions import Utils -from Utils import get_item_name_from_id, get_location_name_from_address, ReceivedItem, _version_tuple +from Utils import get_item_name_from_id, get_location_name_from_address, \ + ReceivedItem, _version_tuple, restricted_loads from NetUtils import Node, Endpoint console_names = frozenset(set(Items.item_table) | set(Regions.location_table) | set(Items.item_name_groups)) @@ -106,28 +108,23 @@ class Context(Node): def load(self, multidatapath: str, use_embedded_server_options: bool = False): with open(multidatapath, 'rb') as f: - self._load(json.loads(zlib.decompress(f.read()).decode("utf-8-sig")), + self._load(restricted_loads(zlib.decompress(f.read())), use_embedded_server_options) self.data_filename = multidatapath - def _load(self, jsonobj: dict, use_embedded_server_options: bool): - for team, names in enumerate(jsonobj['names']): + def _load(self, decoded_obj: dict, use_embedded_server_options: bool): + for team, names in enumerate(decoded_obj['names']): for player, name in enumerate(names, 1): self.player_names[(team, player)] = name - if "rom_strings" in jsonobj: - self.rom_names = {rom: (team, slot) for slot, team, rom in jsonobj['rom_strings']} - else: - self.rom_names = {bytes(letter for letter in rom).decode(): (team, slot) for slot, team, rom in - jsonobj['roms']} - self.remote_items = set(jsonobj['remote_items']) - self.locations = {tuple(k): tuple(v) for k, v in jsonobj['locations']} - if "er_hint_data" in jsonobj: - self.er_hint_data = {int(player): {int(address): name for address, name in loc_data.items()} - for player, loc_data in jsonobj["er_hint_data"].items()} + self.rom_names = decoded_obj['roms'] + self.remote_items = decoded_obj['remote_items'] + self.locations = decoded_obj['locations'] + self.er_hint_data = {int(player): {int(address): name for address, name in loc_data.items()} + for player, loc_data in decoded_obj["er_hint_data"].items()} if use_embedded_server_options: - server_options = jsonobj.get("server_options", {}) + server_options = decoded_obj.get("server_options", {}) self._set_options(server_options) def _set_options(self, server_options: dict): @@ -154,9 +151,9 @@ class Context(Node): def _save(self, exit_save:bool=False) -> bool: try: - jsonstr = json.dumps(self.get_save()) + encoded_save = pickle.dumps(self.get_save()) with open(self.save_filename, "wb") as f: - f.write(zlib.compress(jsonstr.encode("utf-8"))) + f.write(zlib.compress(encoded_save)) except Exception as e: logging.exception(e) return False @@ -171,8 +168,8 @@ class Context(Node): self.data_filename + '_')) + 'multisave' try: with open(self.save_filename, 'rb') as f: - jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8")) - self.set_save(jsonobj) + save_data = restricted_loads(zlib.decompress(f.read())) + self.set_save(save_data) except FileNotFoundError: logging.error('No save data found, starting a new game') except Exception as e: @@ -280,16 +277,21 @@ class Context(Node): logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text)) asyncio.create_task(self.send_msgs(client, [['Print', text]])) + def notify_client_multiple(self, client: Client, texts: typing.List[str]): + if not client.auth: + return + asyncio.create_task(self.send_msgs(client, [['Print', text] for text in texts])) + def broadcast_team(self, team, msgs): for client in self.endpoints: if client.auth and client.team == team: asyncio.create_task(self.send_msgs(client, msgs)) def broadcast_all(self, msgs): - msgs = json.dumps(msgs) + msgs = dumps(msgs) for endpoint in self.endpoints: if endpoint.auth: - asyncio.create_task(self.send_json_msgs(endpoint, msgs)) + asyncio.create_task(self.send_encoded_msgs(endpoint, msgs)) async def disconnect(self, endpoint): await super(Context, self).disconnect(endpoint) @@ -298,26 +300,25 @@ class Context(Node): # separated out, due to compatibilty between clients def notify_hints(ctx: Context, team: int, hints: typing.List[Utils.Hint]): - cmd = json.dumps([["Hint", hints]]) # make sure it is a list, as it can be set internally + cmd = dumps([["Hint", hints]]) # make sure it is a list, as it can be set internally texts = [['Print', format_hint(ctx, team, hint)] for hint in hints] for _, text in texts: logging.info("Notice (Team #%d): %s" % (team + 1, text)) - for client in ctx.endpoints: if client.auth and client.team == team: - asyncio.create_task(ctx.send_json_msgs(client, cmd)) + asyncio.create_task(ctx.send_encoded_msgs(client, cmd)) def update_aliases(ctx: Context, team: int, client: typing.Optional[Client] = None): - cmd = json.dumps([["AliasUpdate", + cmd = dumps([["AliasUpdate", [(key[1], ctx.get_aliased_name(*key)) for key, value in ctx.player_names.items() if key[0] == team]]]) if client is None: for client in ctx.endpoints: - if client.team == team and client.auth and client.version > [2, 0, 3]: - asyncio.create_task(ctx.send_json_msgs(client, cmd)) + if client.team == team and client.auth: + asyncio.create_task(ctx.send_encoded_msgs(client, cmd)) else: - asyncio.create_task(ctx.send_json_msgs(client, cmd)) + asyncio.create_task(ctx.send_encoded_msgs(client, cmd)) async def server(websocket, path, ctx: Context): @@ -327,7 +328,7 @@ async def server(websocket, path, ctx: Context): try: await on_client_connected(ctx, client) async for data in websocket: - for msg in json.loads(data): + for msg in loads(data): if len(msg) == 1: cmd = msg args = None @@ -392,7 +393,7 @@ async def countdown(ctx: Context, timer): async def missing(ctx: Context, client: Client, locations: list): await ctx.send_msgs(client, [['Missing', { - 'locations': json.dumps(locations) + 'locations': dumps(locations) }]]) @@ -762,6 +763,9 @@ class ClientMessageProcessor(CommonCommandProcessor): self.output( "Sorry, client forfeiting requires you to have beaten the game on this server." " You can ask the server admin for a /forfeit") + if self.client.version < [2, 1, 0]: + self.output( + "Your client is too old to send game beaten information. Please update, load you savegame and reconnect.") return False def _cmd_remaining(self) -> bool: @@ -805,13 +809,9 @@ class ClientMessageProcessor(CommonCommandProcessor): locations.append(location_name) if len(locations) > 0: - if self.client.version < [2, 3, 0]: - buffer = "" - for location in locations: - buffer += f'Missing: {location}\n' - self.output(buffer + f"Found {len(locations)} missing location checks") - else: - asyncio.create_task(missing(self.ctx, self.client, locations)) + texts = [f'Missing: {location}\n' for location in locations] + texts.append(f"Found {len(locations)} missing location checks") + self.ctx.notify_client_multiple(self.client, texts) else: self.output("No missing location checks found.") return True @@ -954,6 +954,7 @@ async def process_client_cmd(ctx: Context, client: Client, cmd, args): if type(args["rom"]) == list: args["rom"] = bytes(letter for letter in args["rom"]).decode() if args['rom'] not in ctx.rom_names: + logging.info((args["rom"], ctx.rom_names)) errors.add('InvalidRom') else: team, slot = ctx.rom_names[args['rom']] @@ -972,7 +973,7 @@ async def process_client_cmd(ctx: Context, client: Client, cmd, args): client.name = ctx.player_names[(team, slot)] client.team = team client.slot = slot - if "AP" not in args.get('tags', Client.tags): + if ctx.compatibility == 1 and "AP" not in args.get('tags', Client.tags): errors.add('IncompatibleVersion') elif ctx.compatibility == 0 and args.get('version', Client.version) != list(_version_tuple): errors.add('IncompatibleVersion') diff --git a/NetUtils.py b/NetUtils.py index d99b2086..5d996795 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -1,33 +1,34 @@ from __future__ import annotations import asyncio -import json import logging import typing +from json import loads, dumps import websockets - class Node: endpoints: typing.List + dumper = staticmethod(dumps) + loader = staticmethod(loads) def __init__(self): self.endpoints = [] def broadcast_all(self, msgs): - msgs = json.dumps(msgs) + msgs = self.dumper(msgs) for endpoint in self.endpoints: - asyncio.create_task(self.send_json_msgs(endpoint, msgs)) + asyncio.create_task(self.send_encoded_msgs(endpoint, msgs)) async def send_msgs(self, endpoint: Endpoint, msgs): if not endpoint.socket or not endpoint.socket.open or endpoint.socket.closed: return try: - await endpoint.socket.send(json.dumps(msgs)) + await endpoint.socket.send(self.dumper(msgs)) except websockets.ConnectionClosed: logging.exception("Exception during send_msgs") await self.disconnect(endpoint) - async def send_json_msgs(self, endpoint: Endpoint, msg: str): + async def send_encoded_msgs(self, endpoint: Endpoint, msg: str): if not endpoint.socket or not endpoint.socket.open or endpoint.socket.closed: return try: diff --git a/Patch.py b/Patch.py index 6c56dfe1..d8b1f500 100644 --- a/Patch.py +++ b/Patch.py @@ -57,7 +57,7 @@ def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str bytes = generate_patch(load_bytes(rom_file_to_patch), { "server": server}) # allow immediate connection to server in multiworld. Empty string otherwise - target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + ".bmbp" + target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + ".apbp" write_lzma(bytes, target) return target @@ -110,7 +110,7 @@ if __name__ == "__main__": result = pool.submit(create_patch_file, rom, address) result.add_done_callback(lambda task: print(f"Created patch {task.result()}")) - elif rom.endswith(".bmbp"): + elif rom.endswith(".apbp"): print(f"Applying patch {rom}") data, target = create_rom_file(rom) romfile, adjusted = Utils.get_adjuster_settings(target) @@ -147,7 +147,7 @@ if __name__ == "__main__": def _handle_zip_file_entry(zfinfo : zipfile.ZipInfo, server: str): data = zfr.read(zfinfo) - if zfinfo.filename.endswith(".bmbp"): + if zfinfo.filename.endswith(".apbp"): data = update_patch_data(data, server) with ziplock: zfw.writestr(zfinfo, data) diff --git a/Rom.py b/Rom.py index f2130913..b37ce38a 100644 --- a/Rom.py +++ b/Rom.py @@ -79,12 +79,12 @@ class LocalRom(object): if self.verify(buffer): self.buffer = buffer - if not os.path.exists(local_path('data', 'basepatch.bmbp')): + if not os.path.exists(local_path('data', 'basepatch.apbp')): Patch.create_patch_file(local_path('basepatch.sfc')) return - if os.path.isfile(local_path('data', 'basepatch.bmbp')): - _, target, buffer = Patch.create_rom_bytes(local_path('data', 'basepatch.bmbp')) + if os.path.isfile(local_path('data', 'basepatch.apbp')): + _, target, buffer = Patch.create_rom_bytes(local_path('data', 'basepatch.apbp')) if self.verify(buffer): self.buffer = bytearray(buffer) with open(local_path('basepatch.sfc'), 'wb') as stream: @@ -1398,6 +1398,7 @@ def patch_rom(world, rom, player, team, enemized): # set rom name # 21 bytes from Main import __version__ + # TODO: Adjust Enemizer to accept AP and AD rom.name = bytearray(f'BM{__version__.replace(".", "")[0:3]}_{team + 1}_{player}_{world.seed:09}\0', 'utf8')[:21] rom.name.extend([0] * (21 - len(rom.name))) rom.write_bytes(0x7FC0, rom.name) diff --git a/WebHostLib/downloads.py b/WebHostLib/downloads.py index 5dc71788..b7a8c1c3 100644 --- a/WebHostLib/downloads.py +++ b/WebHostLib/downloads.py @@ -20,7 +20,7 @@ def download_patch(room_id, patch_id): patch_data = update_patch_data(patch.data, server="berserkermulti.world:" + str(last_port)) patch_data = io.BytesIO(patch_data) - fname = f"P{patch.player}_{pname}_{app.jinja_env.filters['suuid'](room_id)}.bmbp" + fname = f"P{patch.player}_{pname}_{app.jinja_env.filters['suuid'](room_id)}.apbp" return send_file(patch_data, as_attachment=True, attachment_filename=fname) @@ -43,5 +43,5 @@ def download_raw_patch(seed_id, player_id): patch_data = update_patch_data(patch.data, server="") patch_data = io.BytesIO(patch_data) - fname = f"P{patch.player}_{pname}_{app.jinja_env.filters['suuid'](seed_id)}.bmbp" + fname = f"P{patch.player}_{pname}_{app.jinja_env.filters['suuid'](seed_id)}.apbp" return send_file(patch_data, as_attachment=True, attachment_filename=fname) diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index 53606b92..dd0b72cc 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -123,7 +123,7 @@ def upload_to_db(folder, owner, sid): multidata = None for file in os.listdir(folder): file = os.path.join(folder, file) - if file.endswith(".bmbp"): + if file.endswith(".apbp"): player = int(file.split("P")[1].split(".")[0].split("_")[0]) patches.add(Patch(data=open(file, "rb").read(), player=player)) elif file.endswith(".txt"): diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index 229e03cb..0613b291 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -8,7 +8,7 @@ from pony.orm import commit, select from WebHostLib import app, Seed, Room, Patch -accepted_zip_contents = {"patches": ".bmbp", +accepted_zip_contents = {"patches": ".apbp", "spoiler": ".txt", "multidata": "multidata"} @@ -38,7 +38,7 @@ def uploads(): for file in infolist: if file.filename.endswith(banned_zip_contents): return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted." - elif file.filename.endswith(".bmbp"): + elif file.filename.endswith(".apbp"): player = int(file.filename.split("P")[1].split(".")[0].split("_")[0]) patches.add(Patch(data=zfile.open(file, "r").read(), player=player)) elif file.filename.endswith(".txt"): diff --git a/WebUI.py b/WebUI.py index 015c7118..e0743c7c 100644 --- a/WebUI.py +++ b/WebUI.py @@ -1,6 +1,6 @@ import http.server import logging -import os +import json import socket import socketserver import threading @@ -16,6 +16,9 @@ logger = logging.getLogger("WebUIRelay") class WebUiClient(Node): + loader = staticmethod(json.loads) + dumper = staticmethod(json.dumps) + def __init__(self): super().__init__() self.manual_snes = None diff --git a/data/basepatch.bmbp b/data/basepatch.apbp similarity index 100% rename from data/basepatch.bmbp rename to data/basepatch.apbp diff --git a/inno_setup.iss b/inno_setup.iss index 3d7f2eef..9f1e75f1 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -57,7 +57,7 @@ Type: dirifempty; Name: "{app}" [Registry] -Root: HKCR; Subkey: ".bmbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "" +Root: HKCR; Subkey: ".apbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Berserker's Multiworld Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\{#MyAppExeName},0"; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; ValueType: string; ValueName: ""