implement binary patching for multimystery (for now no gui/cli support)

This commit is contained in:
Fabian Dill 2020-03-06 00:48:23 +01:00
parent 9b82f220bb
commit d44acfdaaf
7 changed files with 118 additions and 37 deletions

View File

@ -934,12 +934,16 @@ async def game_watcher(ctx : Context):
async def main():
parser = argparse.ArgumentParser()
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a Berserker Multiworld Binary Patch file')
parser.add_argument('--snes', default='localhost:8080', help='Address of the QUsb2snes server.')
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
args = parser.parse_args()
if args.diff_file:
import Patch
args.connect = Patch.create_rom_file(args.diff_file)["server"]
logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
ctx = Context(args.snes, args.connect, args.password)

View File

@ -26,17 +26,22 @@ if __name__ == "__main__":
try:
print(f"{__author__}'s MultiMystery Launcher V{__version__}")
import ModuleUpdate
ModuleUpdate.update()
from Utils import parse_yaml
from Utils import parse_yaml, get_public_ipv4
from Patch import create_patch_file
multi_mystery_options = parse_yaml(open("host.yaml").read())["multi_mystery_options"]
options = parse_yaml(open("host.yaml").read())
multi_mystery_options = options["multi_mystery_options"]
output_path = multi_mystery_options["output_path"]
enemizer_path = multi_mystery_options["enemizer_path"]
player_files_path = multi_mystery_options["player_files_path"]
race = multi_mystery_options["race"]
create_spoiler = multi_mystery_options["create_spoiler"]
zip_roms = multi_mystery_options["zip_roms"]
zip_diffs = multi_mystery_options["zip_diffs"]
zip_spoiler = multi_mystery_options["zip_spoiler"]
zip_multidata = multi_mystery_options["zip_multidata"]
zip_format = multi_mystery_options["zip_format"]
@ -44,7 +49,7 @@ if __name__ == "__main__":
player_name = multi_mystery_options["player_name"]
meta_file_path = multi_mystery_options["meta_file_path"]
teams = multi_mystery_options["teams"]
rom_file = multi_mystery_options["rom_file"]
rom_file = options["general_options"]["rom_file"]
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
@ -136,13 +141,19 @@ if __name__ == "__main__":
zipname = os.path.join(output_path, f"ER_{seedname}.{typical_zip_ending}")
print(f"Creating zipfile {zipname}")
ipv4 = get_public_ipv4()
with zipfile.ZipFile(zipname, "w", compression=compression, compresslevel=9) as zf:
for file in os.listdir(output_path):
if zip_roms and file.endswith(".sfc") and seedname in file:
pack_file(file)
if zip_roms == 2 and player_name.lower() not in file.lower():
remove_zipped_file(file)
if file.endswith(".sfc") and seedname in file:
if zip_diffs:
diff = os.path.split(create_patch_file(os.path.join(output_path, file), ipv4))[1]
pack_file(diff)
if zip_diffs == 2:
remove_zipped_file(diff)
if zip_roms:
pack_file(file)
if zip_roms == 2 and player_name.lower() not in file.lower():
remove_zipped_file(file)
if zip_multidata and os.path.exists(os.path.join(output_path, multidataname)):
pack_file(multidataname)
if zip_multidata == 2:

View File

@ -3,7 +3,6 @@ import asyncio
import functools
import json
import logging
import urllib.request
import zlib
import collections
import typing
@ -613,23 +612,17 @@ async def main():
except Exception as e:
logging.error('Failed to read multiworld data (%s)' % e)
return
import socket
ip = socket.gethostbyname(socket.gethostname())
try:
ip = urllib.request.urlopen('https://checkip.amazonaws.com/').read().decode('utf8').strip()
except Exception as e:
try:
ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8').strip()
except:
logging.exception(e)
pass # we could be offline, in a local game, so no point in erroring out
logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port, 'No password' if not ctx.password else 'Password: %s' % ctx.password))
ip = Utils.get_public_ipv4()
logging.info('Hosting game at %s:%d (%s)' % (
ip, ctx.port, 'No password' if not ctx.password else 'Password: %s' % ctx.password))
ctx.disable_save = args.disable_save
if not ctx.disable_save:
if not ctx.save_filename:
ctx.save_filename = (ctx.data_filename[:-9] if ctx.data_filename[-9:] == 'multidata' else (ctx.data_filename + '_')) + 'multisave'
ctx.save_filename = (ctx.data_filename[:-9] if ctx.data_filename[-9:] == 'multidata' else (
ctx.data_filename + '_')) + 'multisave'
try:
with open(ctx.save_filename, 'rb') as f:
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))

54
Patch.py Normal file
View File

@ -0,0 +1,54 @@
import bsdiff4
import yaml
import os
import lzma
import Utils
base_rom_bytes = None
def get_base_rom_bytes() -> bytes:
global base_rom_bytes
if not base_rom_bytes:
with open("host.yaml") as f:
options = Utils.parse_yaml(f.read())
file_name = options["general_options"]["rom_file"]
base_rom_bytes = load_bytes(file_name)
return base_rom_bytes
def generate_patch(rom: bytes, metadata=None) -> bytes:
if metadata is None:
metadata = {}
patch = bsdiff4.diff(get_base_rom_bytes(), rom)
patch = yaml.dump({"meta": metadata,
"patch": patch})
return patch.encode()
def create_patch_file(rom_file_to_patch: str, server: str = "") -> str:
bytes = generate_patch(load_bytes(rom_file_to_patch),
{
"server": server}) # allow immediate connection to server in multiworld. Empty string otherwise
target = os.path.splitext(rom_file_to_patch)[0] + ".bbp"
write_lzma(bytes, target)
return target
def create_rom_file(patch_file) -> dict:
data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig"))
patched_data = bsdiff4.patch(get_base_rom_bytes(), data["patch"])
with open(os.path.splitext(patch_file)[0] + ".sfc", "wb") as f:
f.write(patched_data)
return data["meta"]
def load_bytes(path: str):
with open(path, "rb") as f:
return f.read()
def write_lzma(data: bytes, path: str):
with lzma.LZMAFile(path, 'wb') as f:
f.write(data)

View File

@ -151,3 +151,18 @@ class Hint(typing.NamedTuple):
location: int
item: int
found: bool
def get_public_ipv4() -> str:
import socket
import urllib.request
import logging
ip = socket.gethostbyname(socket.gethostname())
try:
ip = urllib.request.urlopen('https://checkip.amazonaws.com/').read().decode('utf8').strip()
except Exception as e:
try:
ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8').strip()
except:
logging.exception(e)
pass # we could be offline, in a local game, so no point in erroring out
return ip

View File

@ -1,6 +1,9 @@
#options for MultiServer
#null means nothing, for the server this means to default the value
#these overwrite command line arguments!
general_options:
#File name of the v1.0 J rom
rom_file: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
server_options:
host: null
port: null
@ -15,11 +18,9 @@ server_options:
#points given to player for each acquired item
location_check_points: 1
#point cost to receive a hint via !hint for players
hint_cost: 1000 #set to 0 if you want free hints
hint_cost: 50 #set to 0 if you want free hints
#options for MultiMystery.py
multi_mystery_options:
#File name of the v1.0 J rom
rom_file: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
#teams, however, note that there is currently no way to supply names for teams 2+ through MultiMystery
teams: 1
#Where to place the resulting files
@ -31,21 +32,23 @@ multi_mystery_options:
#meta file name, within players folder
meta_file_path: "meta.yaml"
#automatically launches {player_name}.yaml's ROM file using the OS's default program once generation completes. (likely your emulator)
#does nothing if the name is not found
#example: player_name = "Berserker"
#does nothing if the name is not found
#example: player_name = "Berserker"
player_name: "" # the hosts name
#create a spoiler file
#create a spoiler file
create_spoiler: 1
#Zip the resulting roms
#0 -> Don't
#1 -> Create a zip
#2 -> Create a zip and delete the ROMs that will be in it, except the hosts (requires player_name to be set correctly)
#Zip the resulting roms
#0 -> Don't
#1 -> Create a zip
#2 -> Create a zip and delete the ROMs that will be in it, except the hosts (requires player_name to be set correctly)
zip_roms: 1
#include the spoiler log in the zip, 2 -> delete the non-zipped one
# zip diff files
zip_diffs: 2
#include the spoiler log in the zip, 2 -> delete the non-zipped one
zip_spoiler: 0
#include the multidata file in the zip, 2 -> delete the non-zipped one, which also means the server won't autostart
#include the multidata file in the zip, 2 -> delete the non-zipped one, which also means the server won't autostart
zip_multidata: 0
#zip algorithm to use
#zip algorithm to use
zip_format: 2 # 1 -> zip, 2 -> 7z, 3->bz2
#create roms flagged as race roms
#create roms flagged as race roms
race: 0

View File

@ -3,4 +3,5 @@ colorama>=0.4.3
websockets>=8.1
PyYAML>=5.3
collections_extended>=1.0.3
fuzzywuzzy>=0.18.0
fuzzywuzzy>=0.18.0
bsdiff4>=1.1.9