Patch: update to version 4 (#312)
This commit is contained in:
parent
b02a710bc5
commit
7394598aff
187
Patch.py
187
Patch.py
|
@ -1,6 +1,7 @@
|
|||
# TODO: convert this into a system like AutoWorld
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import json
|
||||
import bsdiff4
|
||||
import yaml
|
||||
import os
|
||||
|
@ -9,12 +10,155 @@ import threading
|
|||
import concurrent.futures
|
||||
import zipfile
|
||||
import sys
|
||||
from typing import Tuple, Optional
|
||||
from typing import Tuple, Optional, Dict, Any, Union, BinaryIO
|
||||
|
||||
import Utils
|
||||
|
||||
current_patch_version = 3
|
||||
current_patch_version = 4
|
||||
|
||||
|
||||
class AutoPatchRegister(type):
|
||||
patch_types: Dict[str, APDeltaPatch] = {}
|
||||
file_endings: Dict[str, APDeltaPatch] = {}
|
||||
|
||||
def __new__(cls, name: str, bases, dct: Dict[str, Any]):
|
||||
# construct class
|
||||
new_class = super().__new__(cls, name, bases, dct)
|
||||
if "game" in dct:
|
||||
AutoPatchRegister.patch_types[dct["game"]] = new_class
|
||||
if not dct["patch_file_ending"]:
|
||||
raise Exception(f"Need an expected file ending for {name}")
|
||||
AutoPatchRegister.file_endings[dct["patch_file_ending"]] = new_class
|
||||
return new_class
|
||||
|
||||
@staticmethod
|
||||
def get_handler(file: str) -> Optional[type(APDeltaPatch)]:
|
||||
for file_ending, handler in AutoPatchRegister.file_endings.items():
|
||||
if file.endswith(file_ending):
|
||||
return handler
|
||||
|
||||
|
||||
class APContainer:
|
||||
"""A zipfile containing at least archipelago.json"""
|
||||
version: int = current_patch_version
|
||||
compression_level: int = 9
|
||||
compression_method: int = zipfile.ZIP_DEFLATED
|
||||
game: Optional[str] = None
|
||||
|
||||
# instance attributes:
|
||||
path: Optional[str]
|
||||
player: Optional[int]
|
||||
player_name: str
|
||||
server: str
|
||||
|
||||
def __init__(self, path: Optional[str] = None, player: Optional[int] = None,
|
||||
player_name: str = "", server: str = ""):
|
||||
self.path = path
|
||||
self.player = player
|
||||
self.player_name = player_name
|
||||
self.server = server
|
||||
|
||||
def write(self, file: Optional[Union[str, BinaryIO]] = None):
|
||||
if not self.path and not file:
|
||||
raise FileNotFoundError(f"Cannot write {self.__class__.__name__} due to no path provided.")
|
||||
with zipfile.ZipFile(file if file else self.path, "w", self.compression_method, True, self.compression_level) \
|
||||
as zf:
|
||||
if file:
|
||||
self.path = zf.filename
|
||||
self.write_contents(zf)
|
||||
|
||||
def write_contents(self, opened_zipfile: zipfile.ZipFile):
|
||||
manifest = self.get_manifest()
|
||||
try:
|
||||
manifest = json.dumps(manifest)
|
||||
except Exception as e:
|
||||
raise Exception(f"Manifest {manifest} did not convert to json.") from e
|
||||
else:
|
||||
opened_zipfile.writestr("archipelago.json", manifest)
|
||||
|
||||
def read(self, file: Optional[Union[str, BinaryIO]] = None):
|
||||
"""Read data into patch object. file can be file-like, such as an outer zip file's stream."""
|
||||
if not self.path and not file:
|
||||
raise FileNotFoundError(f"Cannot read {self.__class__.__name__} due to no path provided.")
|
||||
with zipfile.ZipFile(file if file else self.path, "r") as zf:
|
||||
if file:
|
||||
self.path = zf.filename
|
||||
self.read_contents(zf)
|
||||
|
||||
def read_contents(self, opened_zipfile: zipfile.ZipFile):
|
||||
with opened_zipfile.open("archipelago.json", "r") as f:
|
||||
manifest = json.load(f)
|
||||
if manifest["compatible_version"] > self.version:
|
||||
raise Exception(f"File (version: {manifest['compatible_version']}) too new "
|
||||
f"for this handler (version: {self.version})")
|
||||
self.player = manifest["player"]
|
||||
self.server = manifest["server"]
|
||||
self.player_name = manifest["player_name"]
|
||||
|
||||
def get_manifest(self) -> dict:
|
||||
return {
|
||||
"server": self.server, # allow immediate connection to server in multiworld. Empty string otherwise
|
||||
"player": self.player,
|
||||
"player_name": self.player_name,
|
||||
"game": self.game,
|
||||
# minimum version of patch system expected for patching to be successful
|
||||
"compatible_version": 4,
|
||||
"version": current_patch_version,
|
||||
}
|
||||
|
||||
|
||||
class APDeltaPatch(APContainer, metaclass=AutoPatchRegister):
|
||||
"""An APContainer that additionally has delta.bsdiff4
|
||||
containing a delta patch to get the desired file, often a rom."""
|
||||
|
||||
hash = Optional[str] # base checksum of source file
|
||||
patch_file_ending: str = ""
|
||||
delta: Optional[bytes] = None
|
||||
result_file_ending: str = ".sfc"
|
||||
source_data: bytes
|
||||
|
||||
def __init__(self, *args, patched_path: str = "", **kwargs):
|
||||
self.patched_path = patched_path
|
||||
super(APDeltaPatch, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_manifest(self) -> dict:
|
||||
manifest = super(APDeltaPatch, self).get_manifest()
|
||||
manifest["base_checksum"] = self.hash
|
||||
manifest["result_file_ending"] = self.result_file_ending
|
||||
return manifest
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
"""Get Base data"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def get_source_data_with_cache(cls) -> bytes:
|
||||
if not hasattr(cls, "source_data"):
|
||||
cls.source_data = cls.get_source_data()
|
||||
return cls.source_data
|
||||
|
||||
def write_contents(self, opened_zipfile: zipfile.ZipFile):
|
||||
super(APDeltaPatch, self).write_contents(opened_zipfile)
|
||||
# write Delta
|
||||
opened_zipfile.writestr("delta.bsdiff4",
|
||||
bsdiff4.diff(self.get_source_data_with_cache(), open(self.patched_path, "rb").read()),
|
||||
compress_type=zipfile.ZIP_STORED) # bsdiff4 is a format with integrated compression
|
||||
|
||||
def read_contents(self, opened_zipfile: zipfile.ZipFile):
|
||||
super(APDeltaPatch, self).read_contents(opened_zipfile)
|
||||
self.delta = opened_zipfile.read("delta.bsdiff4")
|
||||
|
||||
def patch(self, target: str):
|
||||
"""Base + Delta -> Patched"""
|
||||
if not self.delta:
|
||||
self.read()
|
||||
result = bsdiff4.patch(self.get_source_data_with_cache(), self.delta)
|
||||
with open(target, "wb") as f:
|
||||
f.write(result)
|
||||
|
||||
|
||||
# legacy patch handling follows:
|
||||
GAME_ALTTP = "A Link to the Past"
|
||||
GAME_SM = "Super Metroid"
|
||||
GAME_SOE = "Secret of Evermore"
|
||||
|
@ -104,10 +248,19 @@ def get_base_rom_data(game: str):
|
|||
|
||||
|
||||
def create_rom_file(patch_file: str) -> Tuple[dict, str]:
|
||||
data, target, patched_data = create_rom_bytes(patch_file)
|
||||
with open(target, "wb") as f:
|
||||
f.write(patched_data)
|
||||
return data, target
|
||||
auto_handler = AutoPatchRegister.get_handler(patch_file)
|
||||
if auto_handler:
|
||||
handler: APDeltaPatch = auto_handler(patch_file)
|
||||
target = os.path.splitext(patch_file)[0]+handler.result_file_ending
|
||||
handler.patch(target)
|
||||
return {"server": handler.server,
|
||||
"player": handler.player,
|
||||
"player_name": handler.player_name}, target
|
||||
else:
|
||||
data, target, patched_data = create_rom_bytes(patch_file)
|
||||
with open(target, "wb") as f:
|
||||
f.write(patched_data)
|
||||
return data, target
|
||||
|
||||
|
||||
def update_patch_data(patch_data: bytes, server: str = "") -> bytes:
|
||||
|
@ -233,24 +386,6 @@ if __name__ == "__main__":
|
|||
if 'server' in data:
|
||||
Utils.persistent_store("servers", data['hash'], data['server'])
|
||||
print(f"Host is {data['server']}")
|
||||
elif rom.endswith(".archipelago"):
|
||||
import json
|
||||
import zlib
|
||||
|
||||
with open(rom, 'rb') as fr:
|
||||
|
||||
multidata = zlib.decompress(fr.read()).decode("utf-8")
|
||||
with open(rom + '.txt', 'w') as fw:
|
||||
fw.write(multidata)
|
||||
multidata = json.loads(multidata)
|
||||
for romname in multidata['roms']:
|
||||
Utils.persistent_store("servers", "".join(chr(byte) for byte in romname[2]), address)
|
||||
from Utils import get_options
|
||||
|
||||
multidata["server_options"] = get_options()["server_options"]
|
||||
multidata = zlib.compress(json.dumps(multidata).encode("utf-8"), 9)
|
||||
with open(rom + "_updated.archipelago", 'wb') as f:
|
||||
f.write(multidata)
|
||||
|
||||
elif rom.endswith(".zip"):
|
||||
print(f"Updating host in patch files contained in {rom}")
|
||||
|
@ -279,4 +414,4 @@ if __name__ == "__main__":
|
|||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
input("Press enter to close.")
|
||||
input("Press enter to close.")
|
||||
|
|
2
Utils.py
2
Utils.py
|
@ -72,7 +72,7 @@ def is_frozen() -> bool:
|
|||
return getattr(sys, 'frozen', False)
|
||||
|
||||
|
||||
def local_path(*path):
|
||||
def local_path(*path: str):
|
||||
if local_path.cached_path:
|
||||
return os.path.join(local_path.cached_path, *path)
|
||||
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import zipfile
|
||||
import json
|
||||
from io import BytesIO
|
||||
|
||||
from flask import send_file, Response, render_template
|
||||
from pony.orm import select
|
||||
|
||||
from Patch import update_patch_data, preferred_endings
|
||||
from Patch import update_patch_data, preferred_endings, AutoPatchRegister
|
||||
from WebHostLib import app, Slot, Room, Seed, cache
|
||||
import zipfile
|
||||
|
||||
|
||||
@app.route("/dl_patch/<suuid:room_id>/<int:patch_id>")
|
||||
|
@ -12,16 +15,34 @@ def download_patch(room_id, patch_id):
|
|||
if not patch:
|
||||
return "Patch not found"
|
||||
else:
|
||||
import io
|
||||
|
||||
room = Room.get(id=room_id)
|
||||
last_port = room.last_port
|
||||
filelike = BytesIO(patch.data)
|
||||
greater_than_version_3 = zipfile.is_zipfile(filelike)
|
||||
if greater_than_version_3:
|
||||
# Python's zipfile module cannot overwrite/delete files in a zip, so we recreate the whole thing in ram
|
||||
new_file = BytesIO()
|
||||
with zipfile.ZipFile(filelike, "a") as zf:
|
||||
with zf.open("archipelago.json", "r") as f:
|
||||
manifest = json.load(f)
|
||||
manifest["server"] = f"{app.config['PATCH_TARGET']}:{last_port}"
|
||||
with zipfile.ZipFile(new_file, "w") as new_zip:
|
||||
for file in zf.infolist():
|
||||
if file.filename == "archipelago.json":
|
||||
new_zip.writestr("archipelago.json", json.dumps(manifest))
|
||||
else:
|
||||
new_zip.writestr(file.filename, zf.read(file), file.compress_type, 9)
|
||||
|
||||
patch_data = update_patch_data(patch.data, server=f"{app.config['PATCH_TARGET']}:{last_port}")
|
||||
patch_data = io.BytesIO(patch_data)
|
||||
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}." \
|
||||
f"{AutoPatchRegister.patch_types[patch.game].patch_file_ending}"
|
||||
new_file.seek(0)
|
||||
return send_file(new_file, as_attachment=True, attachment_filename=fname)
|
||||
else:
|
||||
patch_data = update_patch_data(patch.data, server=f"{app.config['PATCH_TARGET']}:{last_port}")
|
||||
patch_data = BytesIO(patch_data)
|
||||
|
||||
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}." \
|
||||
f"{preferred_endings[patch.game]}"
|
||||
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}." \
|
||||
f"{preferred_endings[patch.game]}"
|
||||
return send_file(patch_data, as_attachment=True, attachment_filename=fname)
|
||||
|
||||
|
||||
|
|
|
@ -5,13 +5,14 @@ import json
|
|||
import base64
|
||||
import MultiServer
|
||||
import uuid
|
||||
from io import BytesIO
|
||||
|
||||
from flask import request, flash, redirect, url_for, session, render_template
|
||||
from pony.orm import flush, select
|
||||
|
||||
from WebHostLib import app, Seed, Room, Slot
|
||||
from Utils import parse_yaml, VersionException, __version__
|
||||
from Patch import preferred_endings
|
||||
from Patch import preferred_endings, AutoPatchRegister
|
||||
from NetUtils import NetworkSlot, SlotType
|
||||
|
||||
banned_zip_contents = (".sfc",)
|
||||
|
@ -25,9 +26,18 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
|||
spoiler = ""
|
||||
multidata = None
|
||||
for file in infolist:
|
||||
handler = AutoPatchRegister.get_handler(file.filename)
|
||||
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 handler:
|
||||
raw = zfile.open(file, "r").read()
|
||||
patch = handler(BytesIO(raw))
|
||||
patch.read()
|
||||
slots.add(Slot(data=raw,
|
||||
player_name=patch.player_name,
|
||||
player_id=patch.player,
|
||||
game=patch.game))
|
||||
elif file.filename.endswith(tuple(preferred_endings.values())):
|
||||
data = zfile.open(file, "r").read()
|
||||
yaml_data = parse_yaml(lzma.decompress(data).decode("utf-8-sig"))
|
||||
|
@ -43,7 +53,8 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
|||
elif file.filename.endswith(".apmc"):
|
||||
data = zfile.open(file, "r").read()
|
||||
metadata = json.loads(base64.b64decode(data).decode("utf-8"))
|
||||
slots.add(Slot(data=data, player_name=metadata["player_name"],
|
||||
slots.add(Slot(data=data,
|
||||
player_name=metadata["player_name"],
|
||||
player_id=metadata["player_id"],
|
||||
game="Minecraft"))
|
||||
|
||||
|
@ -51,6 +62,7 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
|||
_, seed_name, slot_id, slot_name = file.filename.split('.')[0].split('_', 3)
|
||||
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
|
||||
player_id=int(slot_id[1:]), game="VVVVVV"))
|
||||
|
||||
elif file.filename.endswith(".apsm64ex"):
|
||||
_, seed_name, slot_id, slot_name = file.filename.split('.')[0].split('_', 3)
|
||||
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
|
||||
|
@ -70,6 +82,7 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
|||
|
||||
elif file.filename.endswith(".txt"):
|
||||
spoiler = zfile.open(file, "r").read().decode("utf-8-sig")
|
||||
|
||||
elif file.filename.endswith(".archipelago"):
|
||||
try:
|
||||
multidata = zfile.open(file).read()
|
||||
|
|
|
@ -36,4 +36,5 @@ network_data_package = {
|
|||
if any(not world.data_version for world in AutoWorldRegister.world_types.values()):
|
||||
network_data_package["version"] = 0
|
||||
import logging
|
||||
logging.warning("Datapackage is in custom mode.")
|
||||
logging.warning(f"Datapackage is in custom mode. Custom Worlds: "
|
||||
f"{[world for world in AutoWorldRegister.world_types.values() if not world.data_version]}")
|
||||
|
|
|
@ -2884,6 +2884,16 @@ hash_alphabet = [
|
|||
]
|
||||
|
||||
|
||||
class LttPDeltaPatch(Patch.APDeltaPatch):
|
||||
hash = JAP10HASH
|
||||
game = "A Link to the Past"
|
||||
patch_file_ending = ".aplttp"
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
return get_base_rom_bytes()
|
||||
|
||||
|
||||
def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
|
||||
if not base_rom_bytes:
|
||||
|
|
|
@ -15,7 +15,7 @@ from .ItemPool import generate_itempool, difficulties
|
|||
from .Shops import create_shops, ShopSlotFill
|
||||
from .Dungeons import create_dungeons
|
||||
from .Rom import LocalRom, patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, get_hash_string, \
|
||||
get_base_rom_path
|
||||
get_base_rom_path, LttPDeltaPatch
|
||||
import Patch
|
||||
|
||||
from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||
|
@ -303,7 +303,9 @@ class ALTTPWorld(World):
|
|||
|
||||
rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc')
|
||||
rom.write_to_file(rompath)
|
||||
Patch.create_patch_file(rompath, player=player, player_name=world.player_name[player])
|
||||
patch = LttPDeltaPatch(os.path.splitext(rompath)[0]+LttPDeltaPatch.patch_file_ending, player=player,
|
||||
player_name=world.player_name[player], patched_path=rompath)
|
||||
patch.write()
|
||||
os.unlink(rompath)
|
||||
self.rom_name = rom.name
|
||||
except:
|
||||
|
|
|
@ -7,12 +7,14 @@ import threading
|
|||
import json
|
||||
|
||||
import jinja2
|
||||
import Utils
|
||||
import shutil
|
||||
|
||||
import Utils
|
||||
import Patch
|
||||
from . import Options
|
||||
from BaseClasses import MultiWorld
|
||||
from .Technologies import tech_table, rocket_recipes, recipes, free_sample_blacklist, progressive_technology_table, \
|
||||
base_tech_table, tech_to_progressive_lookup, progressive_tech_table, liquids
|
||||
|
||||
from .Technologies import tech_table, recipes, free_sample_blacklist, progressive_technology_table, \
|
||||
base_tech_table, tech_to_progressive_lookup, liquids
|
||||
|
||||
template_env: Optional[jinja2.Environment] = None
|
||||
|
||||
|
@ -54,6 +56,22 @@ recipe_time_ranges = {
|
|||
}
|
||||
|
||||
|
||||
class FactorioModFile(Patch.APContainer):
|
||||
game = "Factorio"
|
||||
compression_method = zipfile.ZIP_DEFLATED # Factorio can't load LZMA archives
|
||||
|
||||
def write_contents(self, opened_zipfile: zipfile.ZipFile):
|
||||
# directory containing Factorio mod has to come first, or Factorio won't recognize this file as a mod.
|
||||
mod_dir = self.path[:-4] # cut off .zip
|
||||
for root, dirs, files in os.walk(mod_dir):
|
||||
for file in files:
|
||||
opened_zipfile.write(os.path.join(root, file),
|
||||
os.path.relpath(os.path.join(root, file),
|
||||
os.path.join(mod_dir, '..')))
|
||||
# now we can add extras.
|
||||
super(FactorioModFile, self).write_contents(opened_zipfile)
|
||||
|
||||
|
||||
def generate_mod(world, output_directory: str):
|
||||
player = world.player
|
||||
multiworld = world.world
|
||||
|
@ -159,10 +177,7 @@ def generate_mod(world, output_directory: str):
|
|||
|
||||
# zip the result
|
||||
zf_path = os.path.join(mod_dir + ".zip")
|
||||
with zipfile.ZipFile(zf_path, compression=zipfile.ZIP_DEFLATED, mode='w') as zf:
|
||||
for root, dirs, files in os.walk(mod_dir):
|
||||
for file in files:
|
||||
zf.write(os.path.join(root, file),
|
||||
os.path.relpath(os.path.join(root, file),
|
||||
os.path.join(mod_dir, '..')))
|
||||
mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player])
|
||||
mod.write()
|
||||
|
||||
shutil.rmtree(mod_dir)
|
||||
|
|
|
@ -1,11 +1,21 @@
|
|||
import hashlib
|
||||
import os
|
||||
|
||||
import Utils
|
||||
from Patch import read_rom
|
||||
from Patch import read_rom, APDeltaPatch
|
||||
|
||||
JAP10HASH = '21f3e98df4780ee1c667b84e57d88675'
|
||||
ROM_PLAYER_LIMIT = 65535
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
class SMDeltaPatch(APDeltaPatch):
|
||||
hash = JAP10HASH
|
||||
game = "Super Metroid"
|
||||
patch_file_ending = ".apsm"
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
return get_base_rom_bytes()
|
||||
|
||||
|
||||
def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
|
@ -22,6 +32,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
|||
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
|
||||
return base_rom_bytes
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: str = "") -> str:
|
||||
options = Utils.get_options()
|
||||
if not file_name:
|
||||
|
|
|
@ -2,7 +2,7 @@ import logging
|
|||
import copy
|
||||
import os
|
||||
import threading
|
||||
from typing import Set, List
|
||||
from typing import Set
|
||||
|
||||
logger = logging.getLogger("Super Metroid")
|
||||
|
||||
|
@ -11,12 +11,11 @@ from .Items import lookup_name_to_id as items_lookup_name_to_id
|
|||
from .Regions import create_regions
|
||||
from .Rules import set_rules, add_entrance_rule
|
||||
from .Options import sm_options
|
||||
from .Rom import get_base_rom_path, ROM_PLAYER_LIMIT
|
||||
from .Rom import get_base_rom_path, ROM_PLAYER_LIMIT, SMDeltaPatch
|
||||
import Utils
|
||||
|
||||
from BaseClasses import Region, Entrance, Location, MultiWorld, Item, RegionType, CollectionState
|
||||
from ..AutoWorld import World, AutoLogicRegister
|
||||
import Patch
|
||||
|
||||
from logic.smboolmanager import SMBoolManager
|
||||
from graph.vanilla.graph_locations import locationsDict
|
||||
|
@ -394,23 +393,25 @@ class SMWorld(World):
|
|||
romPatcher.writeRandoSettings(self.variaRando.randoExec.randoSettings, itemLocs)
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
outfilebase = 'AP_' + self.world.seed_name
|
||||
outfilepname = f'_P{self.player}'
|
||||
outfilepname += f"_{self.world.player_name[self.player].replace(' ', '_')}"
|
||||
outputFilename = os.path.join(output_directory, f'{outfilebase}{outfilepname}.sfc')
|
||||
|
||||
try:
|
||||
outfilebase = 'AP_' + self.world.seed_name
|
||||
outfilepname = f'_P{self.player}'
|
||||
outfilepname += f"_{self.world.player_name[self.player].replace(' ', '_')}" \
|
||||
|
||||
outputFilename = os.path.join(output_directory, f'{outfilebase}{outfilepname}.sfc')
|
||||
self.variaRando.PatchRom(outputFilename, self.APPatchRom)
|
||||
|
||||
self.write_crc(outputFilename)
|
||||
|
||||
Patch.create_patch_file(outputFilename, player=self.player, player_name=self.world.player_name[self.player], game=Patch.GAME_SM)
|
||||
os.unlink(outputFilename)
|
||||
self.rom_name = self.romName
|
||||
except:
|
||||
raise
|
||||
else:
|
||||
patch = SMDeltaPatch(os.path.splitext(outputFilename)[0]+SMDeltaPatch.patch_file_ending, player=self.player,
|
||||
player_name=self.world.player_name[self.player], patched_path=outputFilename)
|
||||
patch.write()
|
||||
finally:
|
||||
self.rom_name_available_event.set() # make sure threading continues and errors are collected
|
||||
if os.path.exists(outputFilename):
|
||||
os.unlink(outputFilename)
|
||||
self.rom_name_available_event.set() # make sure threading continues and errors are collected
|
||||
|
||||
def checksum_mirror_sum(self, start, length, mask = 0x800000):
|
||||
while (not(length & mask) and mask):
|
||||
|
@ -427,8 +428,6 @@ class SMWorld(World):
|
|||
next_length += next_length
|
||||
part2 += part2
|
||||
|
||||
length = mask + mask
|
||||
|
||||
return (part1 + part2) & 0xFFFF
|
||||
|
||||
def write_bytes(self, buffer, startaddress: int, values):
|
||||
|
@ -453,7 +452,6 @@ class SMWorld(World):
|
|||
new_name = base64.b64encode(bytes(self.rom_name)).decode()
|
||||
multidata["connect_names"][new_name] = multidata["connect_names"][self.world.player_name[self.player]]
|
||||
|
||||
|
||||
def fill_slot_data(self):
|
||||
slot_data = {}
|
||||
if not self.world.is_race:
|
||||
|
@ -535,10 +533,12 @@ class SMWorld(World):
|
|||
self.world.state.smbm[self.player].onlyBossLeft = True
|
||||
break
|
||||
|
||||
|
||||
def create_locations(self, player: int):
|
||||
for name, id in locations_lookup_name_to_id.items():
|
||||
self.locations[name] = SMLocation(player, name, id)
|
||||
|
||||
|
||||
def create_region(self, world: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||
ret = Region(name, RegionType.LightWorld, name, player)
|
||||
ret.world = world
|
||||
|
|
|
@ -1,15 +1,25 @@
|
|||
import hashlib
|
||||
import os
|
||||
|
||||
import Utils
|
||||
from Patch import read_rom
|
||||
from Patch import read_rom, APDeltaPatch
|
||||
|
||||
SMJAP10HASH = '21f3e98df4780ee1c667b84e57d88675'
|
||||
LTTPJAP10HASH = '03a63945398191337e896e5771f77173'
|
||||
ROM_PLAYER_LIMIT = 256
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
class SMZ3DeltaPatch(APDeltaPatch):
|
||||
hash = "3a177ba9879e3dd04fb623a219d175b2"
|
||||
game = "SMZ3"
|
||||
patch_file_ending = ".smz3"
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
return get_base_rom_bytes()
|
||||
|
||||
|
||||
def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
def get_base_rom_bytes() -> bytes:
|
||||
base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
|
||||
if not base_rom_bytes:
|
||||
sm_file_name = get_sm_base_rom_path()
|
||||
|
@ -18,7 +28,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
|||
basemd5 = hashlib.md5()
|
||||
basemd5.update(sm_base_rom_bytes)
|
||||
if SMJAP10HASH != basemd5.hexdigest():
|
||||
raise Exception('Supplied Base Rom does not match known MD5 for JAP(1.0) release. '
|
||||
raise Exception('Supplied Base Rom does not match known MD5 for SM JAP(1.0) release. '
|
||||
'Get the correct game and version, then dump it')
|
||||
lttp_file_name = get_lttp_base_rom_path()
|
||||
lttp_base_rom_bytes = bytes(read_rom(open(lttp_file_name, "rb")))
|
||||
|
@ -26,12 +36,13 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
|||
basemd5 = hashlib.md5()
|
||||
basemd5.update(lttp_base_rom_bytes)
|
||||
if LTTPJAP10HASH != basemd5.hexdigest():
|
||||
raise Exception('Supplied Base Rom does not match known MD5 for JAP(1.0) release. '
|
||||
raise Exception('Supplied Base Rom does not match known MD5 for LttP JAP(1.0) release. '
|
||||
'Get the correct game and version, then dump it')
|
||||
|
||||
|
||||
get_base_rom_bytes.base_rom_bytes = bytes(combine_smz3_rom(sm_base_rom_bytes, lttp_base_rom_bytes))
|
||||
return get_base_rom_bytes.base_rom_bytes
|
||||
|
||||
|
||||
def get_sm_base_rom_path(file_name: str = "") -> str:
|
||||
options = Utils.get_options()
|
||||
if not file_name:
|
||||
|
@ -40,6 +51,7 @@ def get_sm_base_rom_path(file_name: str = "") -> str:
|
|||
file_name = Utils.local_path(file_name)
|
||||
return file_name
|
||||
|
||||
|
||||
def get_lttp_base_rom_path(file_name: str = "") -> str:
|
||||
options = Utils.get_options()
|
||||
if not file_name:
|
||||
|
@ -48,7 +60,8 @@ def get_lttp_base_rom_path(file_name: str = "") -> str:
|
|||
file_name = Utils.local_path(file_name)
|
||||
return file_name
|
||||
|
||||
def combine_smz3_rom(sm_rom: bytes, lttp_rom: bytes):
|
||||
|
||||
def combine_smz3_rom(sm_rom: bytes, lttp_rom: bytes) -> bytearray:
|
||||
combined = bytearray(0x600000)
|
||||
# SM hi bank
|
||||
pos = 0
|
||||
|
|
|
@ -3,25 +3,25 @@ import copy
|
|||
import os
|
||||
import random
|
||||
import threading
|
||||
import Patch
|
||||
from typing import Dict, Set, TextIO
|
||||
|
||||
from BaseClasses import Region, Entrance, Location, MultiWorld, Item, RegionType, CollectionState
|
||||
from worlds.generic.Rules import add_rule, set_rule
|
||||
from worlds.generic.Rules import set_rule
|
||||
import worlds.smz3.TotalSMZ3.Item as TotalSMZ3Item
|
||||
from worlds.smz3.TotalSMZ3.World import World as TotalSMZ3World
|
||||
from worlds.smz3.TotalSMZ3.Config import Config, GameMode, GanonInvincible, Goal, KeyShuffle, MorphLocation, SMLogic, SwordLocation, Z3Logic
|
||||
from worlds.smz3.TotalSMZ3.Location import LocationType, locations_start_id, Location as TotalSMZ3Location
|
||||
from worlds.smz3.TotalSMZ3.Patch import Patch as TotalSMZ3Patch, getWord, getWordArray
|
||||
from ..AutoWorld import World, AutoLogicRegister
|
||||
from .Rom import get_base_rom_bytes
|
||||
from .Rom import get_base_rom_bytes, SMZ3DeltaPatch
|
||||
from .ips import IPS_Patch
|
||||
from .Options import smz3_options
|
||||
|
||||
world_folder = os.path.dirname(__file__)
|
||||
logger = logging.getLogger("SMZ3")
|
||||
|
||||
class SMCollectionState(metaclass=AutoLogicRegister):
|
||||
|
||||
class SMZ3CollectionState(metaclass=AutoLogicRegister):
|
||||
def init_mixin(self, parent: MultiWorld):
|
||||
# for unit tests where MultiWorld is instantiated before worlds
|
||||
if hasattr(parent, "state"):
|
||||
|
@ -41,7 +41,7 @@ class SMZ3World(World):
|
|||
"""
|
||||
game: str = "SMZ3"
|
||||
topology_present = False
|
||||
data_version = 0
|
||||
data_version = 1
|
||||
options = smz3_options
|
||||
item_names: Set[str] = frozenset(TotalSMZ3Item.lookup_name_to_id)
|
||||
location_names: Set[str]
|
||||
|
@ -208,7 +208,7 @@ class SMZ3World(World):
|
|||
return data
|
||||
|
||||
def convert_to_lttp_item_name(self, itemName):
|
||||
return bytearray(itemName[:19].center(19, " ") , 'utf8') + bytearray(0)
|
||||
return bytearray(itemName[:19].center(19, " "), 'utf8') + bytearray(0)
|
||||
|
||||
def apply_item_names(self):
|
||||
patch = {}
|
||||
|
@ -258,7 +258,9 @@ class SMZ3World(World):
|
|||
filename = os.path.join(output_directory, f'{outfilebase}{outfilepname}.sfc')
|
||||
with open(filename, "wb") as binary_file:
|
||||
binary_file.write(base_combined_rom)
|
||||
Patch.create_patch_file(filename, player=self.player, player_name=self.world.player_name[self.player], game=Patch.GAME_SMZ3)
|
||||
patch = SMZ3DeltaPatch(os.path.splitext(filename)[0]+SMZ3DeltaPatch.patch_file_ending, player=self.player,
|
||||
player_name=self.world.player_name[self.player], patched_path=filename)
|
||||
patch.write()
|
||||
os.remove(filename)
|
||||
self.rom_name = bytearray(patcher.title, 'utf8')
|
||||
except:
|
||||
|
@ -422,12 +424,6 @@ class SMZ3Location(Location):
|
|||
def __init__(self, player: int, name: str, address=None, parent=None):
|
||||
super(SMZ3Location, self).__init__(player, name, address, parent)
|
||||
|
||||
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
|
||||
oldItem = self.item
|
||||
self.item = item
|
||||
result = self.always_allow(state, item) or (self.item_rule(item) and (not check_access or self.can_reach(state)))
|
||||
self.item = oldItem
|
||||
return result
|
||||
|
||||
class SMZ3Item(Item):
|
||||
game = "SMZ3"
|
||||
|
|
|
@ -2,10 +2,25 @@ import bsdiff4
|
|||
import yaml
|
||||
from typing import Optional
|
||||
import Utils
|
||||
from Patch import APDeltaPatch
|
||||
|
||||
|
||||
USHASH = '6e9c94511d04fac6e0a1e582c170be3a'
|
||||
current_patch_version = 2
|
||||
|
||||
|
||||
class SoEDeltaPatch(APDeltaPatch):
|
||||
hash = USHASH
|
||||
game = "Secret of Evermore"
|
||||
patch_file_ending = ".apsoe"
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
with open(get_base_rom_path(), "rb") as stream:
|
||||
return read_rom(stream)
|
||||
|
||||
|
||||
def get_base_rom_path() -> str:
|
||||
return Utils.get_options()['soe_options']['rom_file']
|
||||
|
||||
|
||||
def read_rom(stream, strip_header=True) -> bytes:
|
||||
|
@ -17,17 +32,19 @@ def read_rom(stream, strip_header=True) -> bytes:
|
|||
|
||||
|
||||
def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes:
|
||||
"""Generate old (<4) apbp format yaml"""
|
||||
patch = yaml.dump({"meta": metadata,
|
||||
"patch": patch,
|
||||
"game": "Secret of Evermore",
|
||||
# minimum version of patch system expected for patching to be successful
|
||||
"compatible_version": 1,
|
||||
"version": current_patch_version,
|
||||
"version": 2,
|
||||
"base_checksum": USHASH})
|
||||
return patch.encode(encoding="utf-8-sig")
|
||||
|
||||
|
||||
def generate_patch(vanilla_file, randomized_file, metadata: Optional[dict] = None) -> bytes:
|
||||
"""Generate old (<4) apbp format patch data. Run through lzma to get a complete apbp file."""
|
||||
with open(vanilla_file, "rb") as f:
|
||||
vanilla = read_rom(f)
|
||||
with open(randomized_file, "rb") as f:
|
||||
|
@ -39,19 +56,6 @@ def generate_patch(vanilla_file, randomized_file, metadata: Optional[dict] = Non
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
import pathlib
|
||||
import lzma
|
||||
parser = argparse.ArgumentParser(description='Apply patch to Secret of Evermore.')
|
||||
parser.add_argument('patch', type=pathlib.Path, help='path to .absoe file')
|
||||
args = parser.parse_args()
|
||||
with open(args.patch, "rb") as f:
|
||||
data = Utils.parse_yaml(lzma.decompress(f.read()).decode("utf-8-sig"))
|
||||
if data['game'] != 'Secret of Evermore':
|
||||
raise RuntimeError('Patch is not for Secret of Evermore')
|
||||
with open(Utils.get_options()['soe_options']['rom_file'], 'rb') as f:
|
||||
vanilla_data = read_rom(f)
|
||||
patched_data = bsdiff4.patch(vanilla_data, data["patch"])
|
||||
with open(args.patch.parent / (args.patch.stem + '.sfc'), 'wb') as f:
|
||||
f.write(patched_data)
|
||||
|
||||
import sys
|
||||
print('Please use ../../Patch.py', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
|
|
@ -3,7 +3,6 @@ from ..generic.Rules import set_rule, add_item_rule
|
|||
from BaseClasses import Region, Location, Entrance, Item
|
||||
from Utils import get_options, output_path
|
||||
import typing
|
||||
import lzma
|
||||
import os
|
||||
import os.path
|
||||
import threading
|
||||
|
@ -17,7 +16,7 @@ except ImportError:
|
|||
|
||||
from . import Logic # load logic mixin
|
||||
from .Options import soe_options
|
||||
from .Patch import generate_patch
|
||||
from .Patch import SoEDeltaPatch, get_base_rom_path
|
||||
|
||||
"""
|
||||
In evermizer:
|
||||
|
@ -181,7 +180,7 @@ class SoEWorld(World):
|
|||
try:
|
||||
money = self.world.money_modifier[self.player].value
|
||||
exp = self.world.exp_modifier[self.player].value
|
||||
rom_file = get_options()['soe_options']['rom_file']
|
||||
rom_file = get_base_rom_path()
|
||||
out_base = output_path(output_directory, f'AP_{self.world.seed_name}_P{self.player}_{player_name}')
|
||||
out_file = out_base + '.sfc'
|
||||
placement_file = out_base + '.txt'
|
||||
|
@ -210,13 +209,9 @@ class SoEWorld(World):
|
|||
if (pyevermizer.main(rom_file, out_file, placement_file, self.world.seed_name, self.connect_name,
|
||||
self.evermizer_seed, flags, money, exp)):
|
||||
raise RuntimeError()
|
||||
with lzma.LZMAFile(patch_file, 'wb') as f:
|
||||
f.write(generate_patch(rom_file, out_file,
|
||||
{
|
||||
# used by WebHost
|
||||
"player_name": self.world.player_name[self.player],
|
||||
"player_id": self.player
|
||||
}))
|
||||
patch = SoEDeltaPatch(patch_file, player=self.player,
|
||||
player_name=player_name, patched_path=out_file)
|
||||
patch.write()
|
||||
except:
|
||||
raise
|
||||
finally:
|
||||
|
|
Loading…
Reference in New Issue