diff --git a/worlds/sm/Rom.py b/worlds/sm/Rom.py index ac516ae4..c5b6645e 100644 --- a/worlds/sm/Rom.py +++ b/worlds/sm/Rom.py @@ -4,18 +4,59 @@ import os import json import Utils from Utils import read_snes_rom -from worlds.Files import APDeltaPatch +from worlds.Files import APPatchExtension, APProcedurePatch, APTokenMixin, APTokenTypes from .variaRandomizer.utils.utils import openFile SMJUHASH = '21f3e98df4780ee1c667b84e57d88675' SM_ROM_MAX_PLAYERID = 65535 SM_ROM_PLAYERDATA_COUNT = 202 -class SMDeltaPatch(APDeltaPatch): +class SMPatchExtensions(APPatchExtension): + game = "Super Metroid" + + @staticmethod + def write_crc(caller: APProcedurePatch, rom: bytes) -> bytes: + def checksum_mirror_sum(start, length, mask = 0x800000): + while not(length & mask) and mask: + mask >>= 1 + + part1 = sum(start[:mask]) & 0xFFFF + part2 = 0 + + next_length = length - mask + if next_length: + part2 = checksum_mirror_sum(start[mask:], next_length, mask >> 1) + + while (next_length < mask): + next_length += next_length + part2 += part2 + + return (part1 + part2) & 0xFFFF + + def write_bytes(buffer, startaddress: int, values): + buffer[startaddress:startaddress + len(values)] = values + + buffer = bytearray(rom) + crc = checksum_mirror_sum(buffer, len(buffer)) + inv = crc ^ 0xFFFF + write_bytes(buffer, 0x7FDC, [inv & 0xFF, (inv >> 8) & 0xFF, crc & 0xFF, (crc >> 8) & 0xFF]) + return bytes(buffer) + +class SMProcedurePatch(APProcedurePatch, APTokenMixin): hash = SMJUHASH game = "Super Metroid" patch_file_ending = ".apsm" + procedure = [ + ("apply_tokens", ["token_data.bin"]), + ("write_crc", []) + ] + + def write_tokens(self, patches): + for addr, data in patches.items(): + self.write_token(APTokenTypes.WRITE, addr, bytes(data)) + self.write_file("token_data.bin", self.get_token_binary()) + @classmethod def get_source_data(cls) -> bytes: return get_base_rom_bytes() diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 160b7e4e..5d53270d 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -17,7 +17,7 @@ logger = logging.getLogger("Super Metroid") from .Options import SMOptions from .Client import SMSNIClient -from .Rom import get_base_rom_path, SM_ROM_MAX_PLAYERID, SM_ROM_PLAYERDATA_COUNT, SMDeltaPatch, get_sm_symbols +from .Rom import SM_ROM_MAX_PLAYERID, SM_ROM_PLAYERDATA_COUNT, SMProcedurePatch, get_sm_symbols import Utils from .variaRandomizer.logic.smboolmanager import SMBoolManager @@ -40,7 +40,7 @@ class SMSettings(settings.Group): """File name of the v1.0 J rom""" description = "Super Metroid (JU) ROM" copy_to = "Super Metroid (JU).sfc" - md5s = [SMDeltaPatch.hash] + md5s = [SMProcedurePatch.hash] rom_file: RomFile = RomFile(RomFile.copy_to) @@ -120,12 +120,6 @@ class SMWorld(World): self.locations = {} super().__init__(world, player) - @classmethod - def stage_assert_generate(cls, multiworld: MultiWorld): - rom_file = get_base_rom_path() - if not os.path.exists(rom_file): - raise FileNotFoundError(rom_file) - def generate_early(self): Logic.factory('vanilla') @@ -802,23 +796,19 @@ class SMWorld(World): romPatcher.end() def generate_output(self, output_directory: str): - self.variaRando.args.rom = get_base_rom_path() - outfilebase = self.multiworld.get_out_file_name_base(self.player) - outputFilename = os.path.join(output_directory, f"{outfilebase}.sfc") - try: - self.variaRando.PatchRom(outputFilename, self.APPrePatchRom, self.APPostPatchRom) - self.write_crc(outputFilename) + patcher = self.variaRando.PatchRom(self.APPrePatchRom, self.APPostPatchRom) self.rom_name = self.romName + + patch = SMProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player]) + patch.write_tokens(patcher.romFile.getPatchDict()) + rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}" + f"{patch.patch_file_ending}") + patch.write(rom_path) + except: raise - else: - patch = SMDeltaPatch(os.path.splitext(outputFilename)[0] + SMDeltaPatch.patch_file_ending, player=self.player, - player_name=self.multiworld.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): diff --git a/worlds/sm/variaRandomizer/randomizer.py b/worlds/sm/variaRandomizer/randomizer.py index 8a7a2ea0..22712aa4 100644 --- a/worlds/sm/variaRandomizer/randomizer.py +++ b/worlds/sm/variaRandomizer/randomizer.py @@ -680,7 +680,7 @@ class VariaRandomizer: #dumpErrorMsg(args.output, self.randoExec.errorMsg) raise Exception("Can't generate " + self.fileName + " with the given parameters: {}".format(self.randoExec.errorMsg)) - def PatchRom(self, outputFilename, customPrePatchApply = None, customPostPatchApply = None): + def PatchRom(self, customPrePatchApply = None, customPostPatchApply = None) -> RomPatcher: args = self.args optErrMsgs = self.optErrMsgs @@ -758,9 +758,9 @@ class VariaRandomizer: # args.output is not None: generate local json named args.output if args.rom is not None: # patch local rom - romFileName = args.rom - shutil.copyfile(romFileName, outputFilename) - romPatcher = RomPatcher(settings=patcherSettings, romFileName=outputFilename, magic=args.raceMagic, player=self.player) + # romFileName = args.rom + # shutil.copyfile(romFileName, outputFilename) + romPatcher = RomPatcher(settings=patcherSettings, magic=args.raceMagic, player=self.player) else: romPatcher = RomPatcher(settings=patcherSettings, magic=args.raceMagic) @@ -779,24 +779,12 @@ class VariaRandomizer: #msg = randoExec.errorMsg msg = '' - if args.rom is None: # web mode - data = romPatcher.romFile.data - self.fileName = '{}.sfc'.format(self.fileName) - data["fileName"] = self.fileName - # error msg in json to be displayed by the web site - data["errorMsg"] = msg - # replaced parameters to update stats in database - if len(self.forcedArgs) > 0: - data["forcedArgs"] = self.forcedArgs - with open(outputFilename, 'w') as jsonFile: - json.dump(data, jsonFile) - else: # CLI mode - if msg != "": - print(msg) + return romPatcher + except Exception as e: import traceback traceback.print_exc(file=sys.stdout) - raise Exception("Error patching {}: ({}: {})".format(outputFilename, type(e).__name__, e)) + raise Exception("Error patching: ({}: {})".format(type(e).__name__, e)) #dumpErrorMsg(args.output, msg) # if stuck == True: diff --git a/worlds/sm/variaRandomizer/rom/ips.py b/worlds/sm/variaRandomizer/rom/ips.py index dd3f30a3..add187a8 100644 --- a/worlds/sm/variaRandomizer/rom/ips.py +++ b/worlds/sm/variaRandomizer/rom/ips.py @@ -21,10 +21,23 @@ class IPS_Patch(object): def toDict(self): ret = {} for record in self.records: - if 'rle_count' in record: - ret[record['address']] = [int.from_bytes(record['data'],'little')]*record['rle_count'] + if record['address'] in ret.keys(): + if 'rle_count' in record: + if len(ret[record['address']]) > record['rle_count']: + ret[record['address']][:record['rle_count']] = [int.from_bytes(record['data'],'little')]*record['rle_count'] + else: + ret[record['address']] = [int.from_bytes(record['data'],'little')]*record['rle_count'] + else: + size = len(record['data']) + if len(ret[record['address']]) > size: + ret[record['address']][:size] = [int(b) for b in record['data']] + else: + ret[record['address']] = [int(b) for b in record['data']] else: - ret[record['address']] = [int(b) for b in record['data']] + if 'rle_count' in record: + ret[record['address']] = [int.from_bytes(record['data'],'little')]*record['rle_count'] + else: + ret[record['address']] = [int(b) for b in record['data']] return ret @staticmethod diff --git a/worlds/sm/variaRandomizer/rom/rom.py b/worlds/sm/variaRandomizer/rom/rom.py index 37c15698..f0f37b76 100644 --- a/worlds/sm/variaRandomizer/rom/rom.py +++ b/worlds/sm/variaRandomizer/rom/rom.py @@ -86,7 +86,67 @@ class ROM(object): self.seek(self.maxAddress + BANK_SIZE - off - 1) self.writeByte(0xff) assert (self.maxAddress % BANK_SIZE) == 0 - + +class FakeROM(ROM): + # to have the same code for real ROM and the webservice + def __init__(self, data={}): + super(FakeROM, self).__init__() + self.data = data + self.ipsPatches = [] + + def write(self, bytes): + for byte in bytes: + self.data[self.address] = byte + self.inc() + + def read(self, byteCount): + bytes = [] + for i in range(byteCount): + bytes.append(self.data[self.address]) + self.inc() + + return bytes + + def ipsPatch(self, ipsPatches): + self.ipsPatches += ipsPatches + + # generate ips from self data + def ips(self): + groupedData = {} + startAddress = -1 + prevAddress = -1 + curData = [] + for address in sorted(self.data): + if address == prevAddress + 1: + curData.append(self.data[address]) + prevAddress = address + else: + if len(curData) > 0: + groupedData[startAddress] = curData + startAddress = address + prevAddress = address + curData = [self.data[startAddress]] + if startAddress != -1: + groupedData[startAddress] = curData + + return IPS_Patch(groupedData) + + # generate final IPS for web patching with first the IPS patches, then written data + def close(self): + self.mergedIPS = IPS_Patch() + for ips in self.ipsPatches: + self.mergedIPS.append(ips) + self.mergedIPS.append(self.ips()) + #patchData = mergedIPS.encode() + #self.data = {} + #self.data["ips"] = base64.b64encode(patchData).decode() + #if mergedIPS.truncate_length is not None: + # self.data["truncate_length"] = mergedIPS.truncate_length + #self.data["max_size"] = mergedIPS.max_size + + def getPatchDict(self): + return self.mergedIPS.toDict() + class RealROM(ROM): def __init__(self, name): super(RealROM, self).__init__() diff --git a/worlds/sm/variaRandomizer/rom/rompatcher.py b/worlds/sm/variaRandomizer/rom/rompatcher.py index 2dcf554a..a350764a 100644 --- a/worlds/sm/variaRandomizer/rom/rompatcher.py +++ b/worlds/sm/variaRandomizer/rom/rompatcher.py @@ -7,7 +7,7 @@ from ..utils.doorsmanager import DoorsManager, IndicatorFlag from ..utils.objectives import Objectives from ..graph.graph_utils import GraphUtils, getAccessPoint, locIdsByAreaAddresses, graphAreas from ..logic.logic import Logic -from ..rom.rom import RealROM, snes_to_pc, pc_to_snes +from ..rom.rom import FakeROM, snes_to_pc, pc_to_snes from ..rom.addresses import Addresses from ..rom.rom_patches import RomPatches from ..patches.patchaccess import PatchAccess @@ -52,10 +52,10 @@ class RomPatcher: def __init__(self, settings=None, romFileName=None, magic=None, player=0): self.log = log.get('RomPatcher') self.settings = settings - self.romFileName = romFileName + #self.romFileName = romFileName self.patchAccess = PatchAccess() self.race = None - self.romFile = RealROM(romFileName) + self.romFile = FakeROM() #if magic is not None: # from rom.race_mode import RaceModePatcher # self.race = RaceModePatcher(self, magic) @@ -312,7 +312,7 @@ class RomPatcher: self.applyStartAP(self.settings["startLocation"], plms, doors) self.applyPLMs(plms) except Exception as e: - raise Exception("Error patching {}. ({})".format(self.romFileName, e)) + raise Exception("Error patching. ({})".format(e)) def applyIPSPatch(self, patchName, patchDict=None, ipsDir=None): if patchDict is None: @@ -493,6 +493,7 @@ class RomPatcher: def commitIPS(self): self.romFile.ipsPatch(self.ipsPatches) + self.ipsPatches = [] def writeSeed(self, seed): random.seed(seed)