squashed commit of many breaking changes

Dropping Support for Python 3.7; adding support for Python 3.9
This commit is contained in:
Fabian Dill 2020-10-19 08:26:31 +02:00
parent add0762114
commit 4f8c737eec
16 changed files with 149 additions and 150 deletions

View File

@ -11,7 +11,7 @@ def adjust(args):
logger = logging.getLogger('Adjuster') logger = logging.getLogger('Adjuster')
logger.info('Patching ROM.') logger.info('Patching ROM.')
if os.path.splitext(args.rom)[-1].lower() == '.bmbp': if os.path.splitext(args.rom)[-1].lower() == '.apbp':
import Patch import Patch
meta, args.rom = Patch.create_rom_file(args.rom) meta, args.rom = Patch.create_rom_file(args.rom)

2
Gui.py
View File

@ -655,7 +655,7 @@ def guiMain(args=None):
romEntry2 = Entry(romDialogFrame2, textvariable=romVar2) romEntry2 = Entry(romDialogFrame2, textvariable=romVar2)
def RomSelect2(): 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) romVar2.set(rom)
romSelectButton2 = Button(romDialogFrame2, text='Select Rom', command=RomSelect2) romSelectButton2 = Button(romDialogFrame2, text='Select Rom', command=RomSelect2)

39
Main.py
View File

@ -1,7 +1,6 @@
from collections import OrderedDict from collections import OrderedDict
import copy import copy
from itertools import zip_longest from itertools import zip_longest
import json
import logging import logging
import os import os
import random import random
@ -170,7 +169,7 @@ def main(args, seed=None):
logger.info('Patching ROM.') 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 = [] rom_names = []
@ -258,7 +257,7 @@ def main(args, seed=None):
rom.write_to_file(rompath, hide_enemizer=True) rom.write_to_file(rompath, hide_enemizer=True)
if args.create_diff: if args.create_diff:
Patch.create_patch_file(rompath) Patch.create_patch_file(rompath)
return player, team, bytes(rom.name).decode() return player, team, bytes(rom.name)
pool = concurrent.futures.ThreadPoolExecutor() pool = concurrent.futures.ThreadPoolExecutor()
multidata_task = None multidata_task = None
@ -293,26 +292,26 @@ def main(args, seed=None):
precollected_items[item.player - 1].append(item.code) precollected_items[item.player - 1].append(item.code)
def write_multidata(roms): def write_multidata(roms):
import base64
import pickle
for future in roms: for future in roms:
rom_name = future.result() rom_name = future.result()
rom_names.append(rom_name) rom_names.append(rom_name)
multidata = zlib.compress(json.dumps({"names": parsed_names, multidata = zlib.compress(pickle.dumps({"names": parsed_names,
# backwards compat for < 2.4.1 "roms": {base64.b64encode(rom_name).decode(): (team, slot) for slot, team, rom_name in rom_names},
"roms": [(slot, team, list(name.encode())) "remote_items": {player for player in range(1, world.players + 1) if
for (slot, team, name) in rom_names], world.remote_items[player]},
"rom_strings": rom_names, "locations": {
"remote_items": [player for player in range(1, world.players + 1) if (location.address, location.player) :
world.remote_items[player]], (location.item.code, location.item.player)
"locations": [((location.address, location.player), for location in world.get_filled_locations() if
(location.item.code, location.item.player)) type(location.address) is int},
for location in world.get_filled_locations() if "server_options": get_options()["server_options"],
type(location.address) is int], "er_hint_data": er_hint_data,
"server_options": get_options()["server_options"], "precollected_items": precollected_items,
"er_hint_data": er_hint_data, "version": _version_tuple,
"precollected_items": precollected_items, "tags": ["AP"]
"version": _version_tuple, }), 9)
"tags": ["ER"]
}).encode("utf-8"), 9)
with open(output_path('%s.multidata' % outfilebase), 'wb') as f: with open(output_path('%s.multidata' % outfilebase), 'wb') as f:
f.write(multidata) f.write(multidata)

View File

@ -3,6 +3,10 @@ import sys
import subprocess import subprocess
import importlib 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 update_ran = hasattr(sys, "frozen") and getattr(sys, "frozen") # don't run update if environment is frozen/compiled

View File

@ -1,6 +1,5 @@
import argparse import argparse
import asyncio import asyncio
import json
import logging import logging
import urllib.parse import urllib.parse
import atexit import atexit
@ -13,6 +12,8 @@ import sys
import typing import typing
import os import os
import subprocess import subprocess
import base64
from json import loads, dumps
from random import randrange from random import randrange
@ -113,7 +114,7 @@ class Context():
async def send_msgs(self, msgs): async def send_msgs(self, msgs):
if not self.server or not self.server.socket.open or self.server.socket.closed: if not self.server or not self.server.socket.open or self.server.socket.closed:
return 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, 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,
@ -427,17 +428,17 @@ async def get_snes_devices(ctx: Context):
"Opcode": "DeviceList", "Opcode": "DeviceList",
"Space": "SNES" "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 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.') ctx.ui_node.log_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(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 devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
ctx.ui_node.send_device_list(devices) ctx.ui_node.send_device_list(devices)
@ -481,7 +482,7 @@ async def snes_connect(ctx: Context, address):
"Space": "SNES", "Space": "SNES",
"Operands": [device] "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_state = SNES_ATTACHED
ctx.snes_attached_device = (devices.index(device), device) ctx.snes_attached_device = (devices.index(device), device)
ctx.ui_node.send_connection_status(ctx) 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'): if 'sd2snes' in device.lower() or (len(device) == 4 and device[:3] == 'COM'):
ctx.ui_node.log_info("SD2SNES Detected") ctx.ui_node.log_info("SD2SNES Detected")
ctx.is_sd2snes = True ctx.is_sd2snes = True
await ctx.snes_socket.send(json.dumps({"Opcode" : "Info", "Space" : "SNES"})) await ctx.snes_socket.send(dumps({"Opcode" : "Info", "Space" : "SNES"}))
reply = json.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']) ctx.ui_node.log_info(reply['Results'])
else: else:
@ -501,7 +502,6 @@ async def snes_connect(ctx: Context, address):
SNES_RECONNECT_DELAY = START_RECONNECT_DELAY SNES_RECONNECT_DELAY = START_RECONNECT_DELAY
except Exception as e: except Exception as e:
if recv_task is not None: if recv_task is not None:
if not ctx.snes_socket.closed: if not ctx.snes_socket.closed:
await ctx.snes_socket.close() await ctx.snes_socket.close()
@ -576,7 +576,7 @@ async def snes_read(ctx : Context, address, size):
"Operands" : [hex(address)[2:], hex(size)[2:]] "Operands" : [hex(address)[2:], hex(size)[2:]]
} }
try: try:
await ctx.snes_socket.send(json.dumps(GetAddress_Request)) await ctx.snes_socket.send(dumps(GetAddress_Request))
except websockets.ConnectionClosed: except websockets.ConnectionClosed:
return None 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"] PutAddress_Request['Operands'] = ["2C00", hex(len(cmd)-1)[2:], "2C00", "1"]
try: try:
if ctx.snes_socket is not None: 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: if ctx.snes_socket is not None:
await ctx.snes_socket.send(cmd) await ctx.snes_socket.send(cmd)
except websockets.ConnectionClosed: except websockets.ConnectionClosed:
@ -645,7 +645,7 @@ async def snes_write(ctx : Context, write_list):
for address, data in write_list: for address, data in write_list:
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]] PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
if ctx.snes_socket is not None: 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: if ctx.snes_socket is not None:
await ctx.snes_socket.send(data) await ctx.snes_socket.send(data)
except websockets.ConnectionClosed: except websockets.ConnectionClosed:
@ -674,7 +674,7 @@ async def snes_flush_writes(ctx : Context):
async def send_msgs(websocket, msgs): async def send_msgs(websocket, msgs):
if not websocket or not websocket.open or websocket.closed: if not websocket or not websocket.open or websocket.closed:
return return
await websocket.send(json.dumps(msgs)) await websocket.send(dumps(msgs))
async def server_loop(ctx: Context, address=None): 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') ctx.ui_node.log_error('Already connected')
return return
if address is None: # set through CLI or BMBP if address is None: # set through CLI or APBP
address = ctx.server_address address = ctx.server_address
if address is None: # see if this is an old connection if address is None: # see if this is an old connection
await asyncio.sleep(0.5) # wait for snes connection to succeed if possible. await asyncio.sleep(0.5) # wait for snes connection to succeed if possible.
rom = ctx.rom if ctx.rom else None rom = ctx.rom if ctx.rom else None
try:
servers = cached_address = Utils.persistent_load()["servers"] servers = Utils.persistent_load()["servers"]
address = servers[rom] if rom and rom in servers else servers["default"] if rom in servers:
except Exception as e: address = servers[rom]
logging.debug(f"Could not find cached server address. {e}") cached_address = True
# 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:
@ -714,7 +714,7 @@ async def server_loop(ctx: Context, address=None):
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
async for data in ctx.server.socket: 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) 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') 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( raise Exception(
'Invalid ROM detected, please verify that you have loaded the correct rom and reconnect your snes (/snes)') 'Invalid ROM detected, please verify that you have loaded the correct rom and reconnect your snes (/snes)')
if 'SlotAlreadyTaken' in args: if 'SlotAlreadyTaken' in args:
Utils.persistent_store("servers", "default", ctx.server_address)
Utils.persistent_store("servers", ctx.rom, ctx.server_address) Utils.persistent_store("servers", ctx.rom, ctx.server_address)
raise Exception('Player slot already in use for that team') raise Exception('Player slot already in use for that team')
if 'IncompatibleVersion' in args: 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') raise Exception('Connection refused by the multiworld host')
elif cmd == 'Connected': elif cmd == 'Connected':
Utils.persistent_store("servers", "default", ctx.server_address)
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]}
@ -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), logging.info('%s found %s (%s)' % (player_sent, item, color(get_location_name_from_address(found.location),
'blue_bg', 'white'))) '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': elif cmd == 'Hint':
hints = [Utils.Hint(*hint) for hint in args] hints = [Utils.Hint(*hint) for hint in args]
@ -931,7 +923,7 @@ async def server_auth(ctx: Context, password_requested):
return return
ctx.awaiting_rom = False ctx.awaiting_rom = False
ctx.auth = ctx.rom 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', { await ctx.send_msgs([['Connect', {
'password': ctx.password, 'rom': auth, 'version': Utils._version_tuple, 'tags': get_tags(ctx), 'password': ctx.password, 'rom': auth, 'version': Utils._version_tuple, 'tags': get_tags(ctx),
'uuid': Utils.get_unique_identifier() '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): if rom is None or rom == bytes([0] * ROMNAME_SIZE):
continue continue
ctx.rom = rom.decode() ctx.rom = rom
if not ctx.prev_rom or ctx.prev_rom != ctx.rom: if not ctx.prev_rom or ctx.prev_rom != ctx.rom:
ctx.locations_checked = set() ctx.locations_checked = set()
ctx.locations_scouted = set() ctx.locations_scouted = set()
@ -1251,45 +1243,43 @@ async def websocket_server(websocket: websockets.WebSocketServerProtocol, path,
process_command = ClientCommandProcessor(ctx) process_command = ClientCommandProcessor(ctx)
try: try:
async for incoming_data in websocket: async for incoming_data in websocket:
try: data = loads(incoming_data)
data = json.loads(incoming_data) logging.debug(f"WebUIData:{data}")
logging.debug(f"WebUIData:{data}") if ('type' not in data) or ('content' not in data):
if ('type' not in data) or ('content' not in data): raise Exception('Invalid data received in websocket')
raise Exception('Invalid data received in websocket')
elif data['type'] == 'webStatus': elif data['type'] == 'webStatus':
if data['content'] == 'connections': if data['content'] == 'connections':
ctx.ui_node.send_connection_status(ctx) ctx.ui_node.send_connection_status(ctx)
elif data['content'] == 'devices': elif data['content'] == 'devices':
await get_snes_devices(ctx) await get_snes_devices(ctx)
elif data['content'] == 'gameInfo': elif data['content'] == 'gameInfo':
ctx.ui_node.send_game_info(ctx) ctx.ui_node.send_game_info(ctx)
elif data['content'] == 'checkData': elif data['content'] == 'checkData':
ctx.ui_node.send_location_check(ctx, 'Waiting for check...') ctx.ui_node.send_location_check(ctx, 'Waiting for check...')
elif data['type'] == 'webConfig': elif data['type'] == 'webConfig':
if 'serverAddress' in data['content']: if 'serverAddress' in data['content']:
ctx.server_address = data['content']['serverAddress'] ctx.server_address = data['content']['serverAddress']
await connect(ctx, data['content']['serverAddress']) await connect(ctx, data['content']['serverAddress'])
elif 'deviceId' in data['content']: elif 'deviceId' in data['content']:
# Allow a SNES disconnect via UI sending -1 as new device # Allow a SNES disconnect via UI sending -1 as new device
if data['content']['deviceId'] == "-1": if data['content']['deviceId'] == "-1":
ctx.ui_node.manual_snes = None ctx.ui_node.manual_snes = None
ctx.snes_reconnect_address = None ctx.snes_reconnect_address = None
await snes_disconnect(ctx) await snes_disconnect(ctx)
else: else:
await snes_disconnect(ctx) await snes_disconnect(ctx)
ctx.ui_node.manual_snes = data['content']['deviceId'] ctx.ui_node.manual_snes = data['content']['deviceId']
await snes_connect(ctx, ctx.snes_address) await snes_connect(ctx, ctx.snes_address)
elif data['type'] == 'webControl': elif data['type'] == 'webControl':
if 'disconnect' in data['content']: if 'disconnect' in data['content']:
await ctx.disconnect() 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: except Exception as e:
if not isinstance(e, websockets.WebSocketException): if not isinstance(e, websockets.WebSocketException):
logging.exception(e) logging.exception(e)

View File

@ -121,8 +121,8 @@ if __name__ == "__main__":
seedname = segment seedname = segment
break break
multidataname = f"BM_{seedname}.multidata" multidataname = f"AP_{seedname}.multidata"
spoilername = f"BM_{seedname}_Spoiler.txt" spoilername = f"AP_{seedname}_Spoiler.txt"
romfilename = "" romfilename = ""
if player_name: if player_name:
@ -158,7 +158,7 @@ if __name__ == "__main__":
print(f"Removed {file} which is now present in the zipfile") 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}") print(f"Creating zipfile {zipname}")
ipv4 = (host if host else get_public_ipv4()) + ":" + str(port) ipv4 = (host if host else get_public_ipv4()) + ":" + str(port)
@ -185,7 +185,7 @@ if __name__ == "__main__":
if seedname in file: if seedname in file:
if file.endswith(".sfc"): if file.endswith(".sfc"):
futures.append(pool.submit(_handle_sfc_file, file)) futures.append(pool.submit(_handle_sfc_file, file))
elif file.endswith(".bmbp"): elif file.endswith(".apbp"):
futures.append(pool.submit(_handle_diff_file, file)) futures.append(pool.submit(_handle_diff_file, file))
if zip_multidata and os.path.exists(os.path.join(output_path, multidataname)): if zip_multidata and os.path.exists(os.path.join(output_path, multidataname)):

View File

@ -3,7 +3,6 @@ from __future__ import annotations
import argparse import argparse
import asyncio import asyncio
import functools import functools
import json
import logging import logging
import zlib import zlib
import collections import collections
@ -13,6 +12,8 @@ import weakref
import datetime import datetime
import threading import threading
import random import random
import pickle
from json import loads, dumps
import ModuleUpdate import ModuleUpdate
@ -26,7 +27,8 @@ from fuzzywuzzy import process as fuzzy_process
import Items import Items
import Regions import Regions
import Utils 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 from NetUtils import Node, Endpoint
console_names = frozenset(set(Items.item_table) | set(Regions.location_table) | set(Items.item_name_groups)) 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): def load(self, multidatapath: str, use_embedded_server_options: bool = False):
with open(multidatapath, 'rb') as f: 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) use_embedded_server_options)
self.data_filename = multidatapath self.data_filename = multidatapath
def _load(self, jsonobj: dict, use_embedded_server_options: bool): def _load(self, decoded_obj: dict, use_embedded_server_options: bool):
for team, names in enumerate(jsonobj['names']): for team, names in enumerate(decoded_obj['names']):
for player, name in enumerate(names, 1): for player, name in enumerate(names, 1):
self.player_names[(team, player)] = name self.player_names[(team, player)] = name
if "rom_strings" in jsonobj: self.rom_names = decoded_obj['roms']
self.rom_names = {rom: (team, slot) for slot, team, rom in jsonobj['rom_strings']} self.remote_items = decoded_obj['remote_items']
else: self.locations = decoded_obj['locations']
self.rom_names = {bytes(letter for letter in rom).decode(): (team, slot) for slot, team, rom in self.er_hint_data = {int(player): {int(address): name for address, name in loc_data.items()}
jsonobj['roms']} for player, loc_data in decoded_obj["er_hint_data"].items()}
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()}
if use_embedded_server_options: if use_embedded_server_options:
server_options = jsonobj.get("server_options", {}) server_options = decoded_obj.get("server_options", {})
self._set_options(server_options) self._set_options(server_options)
def _set_options(self, server_options: dict): def _set_options(self, server_options: dict):
@ -154,9 +151,9 @@ class Context(Node):
def _save(self, exit_save:bool=False) -> bool: def _save(self, exit_save:bool=False) -> bool:
try: try:
jsonstr = json.dumps(self.get_save()) encoded_save = pickle.dumps(self.get_save())
with open(self.save_filename, "wb") as f: 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: except Exception as e:
logging.exception(e) logging.exception(e)
return False return False
@ -171,8 +168,8 @@ class Context(Node):
self.data_filename + '_')) + 'multisave' self.data_filename + '_')) + 'multisave'
try: try:
with open(self.save_filename, 'rb') as f: with open(self.save_filename, 'rb') as f:
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8")) save_data = restricted_loads(zlib.decompress(f.read()))
self.set_save(jsonobj) self.set_save(save_data)
except FileNotFoundError: except FileNotFoundError:
logging.error('No save data found, starting a new game') logging.error('No save data found, starting a new game')
except Exception as e: except Exception as e:
@ -280,16 +277,21 @@ class Context(Node):
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text)) logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
asyncio.create_task(self.send_msgs(client, [['Print', 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): def broadcast_team(self, team, msgs):
for client in self.endpoints: for client in self.endpoints:
if client.auth and client.team == team: if client.auth and client.team == team:
asyncio.create_task(self.send_msgs(client, msgs)) asyncio.create_task(self.send_msgs(client, msgs))
def broadcast_all(self, msgs): def broadcast_all(self, msgs):
msgs = json.dumps(msgs) msgs = dumps(msgs)
for endpoint in self.endpoints: for endpoint in self.endpoints:
if endpoint.auth: 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): async def disconnect(self, endpoint):
await super(Context, self).disconnect(endpoint) await super(Context, self).disconnect(endpoint)
@ -298,26 +300,25 @@ class Context(Node):
# separated out, due to compatibilty between clients # separated out, due to compatibilty between clients
def notify_hints(ctx: Context, team: int, hints: typing.List[Utils.Hint]): 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] texts = [['Print', format_hint(ctx, team, hint)] for hint in hints]
for _, text in texts: for _, text in texts:
logging.info("Notice (Team #%d): %s" % (team + 1, text)) logging.info("Notice (Team #%d): %s" % (team + 1, text))
for client in ctx.endpoints: for client in ctx.endpoints:
if client.auth and client.team == team: 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): 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[1], ctx.get_aliased_name(*key)) for key, value in ctx.player_names.items() if
key[0] == team]]]) key[0] == team]]])
if client is None: if client is None:
for client in ctx.endpoints: for client in ctx.endpoints:
if client.team == team and client.auth and client.version > [2, 0, 3]: if client.team == team and client.auth:
asyncio.create_task(ctx.send_json_msgs(client, cmd)) asyncio.create_task(ctx.send_encoded_msgs(client, cmd))
else: 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): async def server(websocket, path, ctx: Context):
@ -327,7 +328,7 @@ async def server(websocket, path, ctx: Context):
try: try:
await on_client_connected(ctx, client) await on_client_connected(ctx, client)
async for data in websocket: async for data in websocket:
for msg in json.loads(data): for msg in loads(data):
if len(msg) == 1: if len(msg) == 1:
cmd = msg cmd = msg
args = None args = None
@ -392,7 +393,7 @@ async def countdown(ctx: Context, timer):
async def missing(ctx: Context, client: Client, locations: list): async def missing(ctx: Context, client: Client, locations: list):
await ctx.send_msgs(client, [['Missing', { await ctx.send_msgs(client, [['Missing', {
'locations': json.dumps(locations) 'locations': dumps(locations)
}]]) }]])
@ -762,6 +763,9 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.output( self.output(
"Sorry, client forfeiting requires you to have beaten the game on this server." "Sorry, client forfeiting requires you to have beaten the game on this server."
" You can ask the server admin for a /forfeit") " 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 return False
def _cmd_remaining(self) -> bool: def _cmd_remaining(self) -> bool:
@ -805,13 +809,9 @@ class ClientMessageProcessor(CommonCommandProcessor):
locations.append(location_name) locations.append(location_name)
if len(locations) > 0: if len(locations) > 0:
if self.client.version < [2, 3, 0]: texts = [f'Missing: {location}\n' for location in locations]
buffer = "" texts.append(f"Found {len(locations)} missing location checks")
for location in locations: self.ctx.notify_client_multiple(self.client, texts)
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))
else: else:
self.output("No missing location checks found.") self.output("No missing location checks found.")
return True return True
@ -954,6 +954,7 @@ async def process_client_cmd(ctx: Context, client: Client, cmd, args):
if type(args["rom"]) == list: if type(args["rom"]) == list:
args["rom"] = bytes(letter for letter in args["rom"]).decode() args["rom"] = bytes(letter for letter in args["rom"]).decode()
if args['rom'] not in ctx.rom_names: if args['rom'] not in ctx.rom_names:
logging.info((args["rom"], ctx.rom_names))
errors.add('InvalidRom') errors.add('InvalidRom')
else: else:
team, slot = ctx.rom_names[args['rom']] 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.name = ctx.player_names[(team, slot)]
client.team = team client.team = team
client.slot = slot 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') errors.add('IncompatibleVersion')
elif ctx.compatibility == 0 and args.get('version', Client.version) != list(_version_tuple): elif ctx.compatibility == 0 and args.get('version', Client.version) != list(_version_tuple):
errors.add('IncompatibleVersion') errors.add('IncompatibleVersion')

View File

@ -1,33 +1,34 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import json
import logging import logging
import typing import typing
from json import loads, dumps
import websockets import websockets
class Node: class Node:
endpoints: typing.List endpoints: typing.List
dumper = staticmethod(dumps)
loader = staticmethod(loads)
def __init__(self): def __init__(self):
self.endpoints = [] self.endpoints = []
def broadcast_all(self, msgs): def broadcast_all(self, msgs):
msgs = json.dumps(msgs) msgs = self.dumper(msgs)
for endpoint in self.endpoints: 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): async def send_msgs(self, endpoint: Endpoint, msgs):
if not endpoint.socket or not endpoint.socket.open or endpoint.socket.closed: if not endpoint.socket or not endpoint.socket.open or endpoint.socket.closed:
return return
try: try:
await endpoint.socket.send(json.dumps(msgs)) await endpoint.socket.send(self.dumper(msgs))
except websockets.ConnectionClosed: except websockets.ConnectionClosed:
logging.exception("Exception during send_msgs") logging.exception("Exception during send_msgs")
await self.disconnect(endpoint) 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: if not endpoint.socket or not endpoint.socket.open or endpoint.socket.closed:
return return
try: try:

View File

@ -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), bytes = generate_patch(load_bytes(rom_file_to_patch),
{ {
"server": server}) # allow immediate connection to server in multiworld. Empty string otherwise "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) write_lzma(bytes, target)
return target return target
@ -110,7 +110,7 @@ if __name__ == "__main__":
result = pool.submit(create_patch_file, rom, address) result = pool.submit(create_patch_file, rom, address)
result.add_done_callback(lambda task: print(f"Created patch {task.result()}")) 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}") print(f"Applying patch {rom}")
data, target = create_rom_file(rom) data, target = create_rom_file(rom)
romfile, adjusted = Utils.get_adjuster_settings(target) romfile, adjusted = Utils.get_adjuster_settings(target)
@ -147,7 +147,7 @@ if __name__ == "__main__":
def _handle_zip_file_entry(zfinfo : zipfile.ZipInfo, server: str): def _handle_zip_file_entry(zfinfo : zipfile.ZipInfo, server: str):
data = zfr.read(zfinfo) data = zfr.read(zfinfo)
if zfinfo.filename.endswith(".bmbp"): if zfinfo.filename.endswith(".apbp"):
data = update_patch_data(data, server) data = update_patch_data(data, server)
with ziplock: with ziplock:
zfw.writestr(zfinfo, data) zfw.writestr(zfinfo, data)

7
Rom.py
View File

@ -79,12 +79,12 @@ class LocalRom(object):
if self.verify(buffer): if self.verify(buffer):
self.buffer = 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')) Patch.create_patch_file(local_path('basepatch.sfc'))
return return
if os.path.isfile(local_path('data', 'basepatch.bmbp')): if os.path.isfile(local_path('data', 'basepatch.apbp')):
_, target, buffer = Patch.create_rom_bytes(local_path('data', 'basepatch.bmbp')) _, target, buffer = Patch.create_rom_bytes(local_path('data', 'basepatch.apbp'))
if self.verify(buffer): if self.verify(buffer):
self.buffer = bytearray(buffer) self.buffer = bytearray(buffer)
with open(local_path('basepatch.sfc'), 'wb') as stream: with open(local_path('basepatch.sfc'), 'wb') as stream:
@ -1398,6 +1398,7 @@ def patch_rom(world, rom, player, team, enemized):
# set rom name # set rom name
# 21 bytes # 21 bytes
from Main import __version__ 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 = 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.name.extend([0] * (21 - len(rom.name)))
rom.write_bytes(0x7FC0, rom.name) rom.write_bytes(0x7FC0, rom.name)

View File

@ -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 = update_patch_data(patch.data, server="berserkermulti.world:" + str(last_port))
patch_data = io.BytesIO(patch_data) 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) 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 = update_patch_data(patch.data, server="")
patch_data = io.BytesIO(patch_data) 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) return send_file(patch_data, as_attachment=True, attachment_filename=fname)

View File

@ -123,7 +123,7 @@ def upload_to_db(folder, owner, sid):
multidata = None multidata = None
for file in os.listdir(folder): for file in os.listdir(folder):
file = os.path.join(folder, file) file = os.path.join(folder, file)
if file.endswith(".bmbp"): if file.endswith(".apbp"):
player = int(file.split("P")[1].split(".")[0].split("_")[0]) player = int(file.split("P")[1].split(".")[0].split("_")[0])
patches.add(Patch(data=open(file, "rb").read(), player=player)) patches.add(Patch(data=open(file, "rb").read(), player=player))
elif file.endswith(".txt"): elif file.endswith(".txt"):

View File

@ -8,7 +8,7 @@ from pony.orm import commit, select
from WebHostLib import app, Seed, Room, Patch from WebHostLib import app, Seed, Room, Patch
accepted_zip_contents = {"patches": ".bmbp", accepted_zip_contents = {"patches": ".apbp",
"spoiler": ".txt", "spoiler": ".txt",
"multidata": "multidata"} "multidata": "multidata"}
@ -38,7 +38,7 @@ def uploads():
for file in infolist: for file in infolist:
if file.filename.endswith(banned_zip_contents): 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." 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]) player = int(file.filename.split("P")[1].split(".")[0].split("_")[0])
patches.add(Patch(data=zfile.open(file, "r").read(), player=player)) patches.add(Patch(data=zfile.open(file, "r").read(), player=player))
elif file.filename.endswith(".txt"): elif file.filename.endswith(".txt"):

View File

@ -1,6 +1,6 @@
import http.server import http.server
import logging import logging
import os import json
import socket import socket
import socketserver import socketserver
import threading import threading
@ -16,6 +16,9 @@ logger = logging.getLogger("WebUIRelay")
class WebUiClient(Node): class WebUiClient(Node):
loader = staticmethod(json.loads)
dumper = staticmethod(json.dumps)
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.manual_snes = None self.manual_snes = None

View File

@ -57,7 +57,7 @@ Type: dirifempty; Name: "{app}"
[Registry] [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"; 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\DefaultIcon"; ValueData: "{app}\{#MyAppExeName},0"; ValueType: string; ValueName: ""
Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; ValueType: string; ValueName: ""