Patch: update to version 4 (#312)
This commit is contained in:
		
							parent
							
								
									b02a710bc5
								
							
						
					
					
						commit
						7394598aff
					
				
							
								
								
									
										177
									
								
								Patch.py
								
								
								
								
							
							
						
						
									
										177
									
								
								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,6 +248,15 @@ def get_base_rom_data(game: str):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
def create_rom_file(patch_file: str) -> Tuple[dict, str]:
 | 
			
		||||
    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)
 | 
			
		||||
| 
						 | 
				
			
			@ -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}")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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,13 +15,31 @@ 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)
 | 
			
		||||
 | 
			
		||||
            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 = io.BytesIO(patch_data)
 | 
			
		||||
            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]}"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,22 +393,24 @@ class SMWorld(World):
 | 
			
		|||
        romPatcher.writeRandoSettings(self.variaRando.randoExec.randoSettings, itemLocs)
 | 
			
		||||
 | 
			
		||||
    def generate_output(self, output_directory: str):
 | 
			
		||||
        try:
 | 
			
		||||
        outfilebase = 'AP_' + self.world.seed_name
 | 
			
		||||
        outfilepname = f'_P{self.player}'
 | 
			
		||||
            outfilepname += f"_{self.world.player_name[self.player].replace(' ', '_')}" \
 | 
			
		||||
 | 
			
		||||
        outfilepname += f"_{self.world.player_name[self.player].replace(' ', '_')}"
 | 
			
		||||
        outputFilename = os.path.join(output_directory, f'{outfilebase}{outfilepname}.sfc')
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            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:
 | 
			
		||||
            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):
 | 
			
		||||
| 
						 | 
				
			
			@ -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