Merge branch 'bsdiff4_baserom'

# Conflicts:
#	Main.py
This commit is contained in:
Fabian Dill 2020-06-13 22:51:44 +02:00
commit fdaba7e28c
7 changed files with 108 additions and 149 deletions

View File

@ -13,9 +13,6 @@ def adjust(args):
if os.stat(args.rom).st_size in (0x200000, 0x400000) and os.path.splitext(args.rom)[-1].lower() == '.sfc':
rom = LocalRom(args.rom, patch=False)
if os.path.isfile(args.baserom):
baserom = LocalRom(args.baserom, patch=True)
rom.orig_buffer = baserom.orig_buffer
else:
raise RuntimeError(
'Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.')

View File

@ -280,10 +280,6 @@ def parse_arguments(argv, no_defaults=False):
''')
parser.add_argument('--suppress_rom', help='Do not create an output rom file.', action='store_true')
parser.add_argument('--gui', help='Launch the GUI', action='store_true')
parser.add_argument('--jsonout', action='store_true', help='''\
Output .json patch to stdout instead of a patched rom. Used
for VT site integration, do not use otherwise.
''')
parser.add_argument('--skip_progression_balancing', action='store_true', default=defval(False),
help="Skip Multiworld Progression balancing.")
parser.add_argument('--skip_playthrough', action='store_true', default=defval(False))
@ -302,8 +298,7 @@ def parse_arguments(argv, no_defaults=False):
parser.add_argument('--race', default=defval(False), action='store_true')
parser.add_argument('--outputname')
parser.add_argument('--create_diff', default=defval(False), action='store_true', help='''\
create a binary patch file from which the randomized rom can be recreated using MultiClient.
Does not work with jsonout.''')
create a binary patch file from which the randomized rom can be recreated using MultiClient.''')
parser.add_argument('--disable_glitch_boots', default=defval(False), action='store_true', help='''\
turns off starting with Pegasus Boots in glitched modes.''')
@ -357,15 +352,14 @@ def start():
sys.exit(0)
# ToDo: Validate files further than mere existance
if not args.jsonout and not os.path.isfile(args.rom):
input('Could not find valid base rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
if not os.path.isfile(args.rom):
input(
'Could not find valid base rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
sys.exit(1)
if any([sprite is not None and not os.path.isfile(sprite) and not get_sprite_from_name(sprite) for sprite in
args.sprite.values()]):
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
sys.exit(1)
if any([sprite is not None and not os.path.isfile(sprite) and not get_sprite_from_name(sprite) for sprite in args.sprite.values()]):
if not args.jsonout:
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
sys.exit(1)
else:
raise IOError('Cannot find sprite file at %s' % args.sprite)
# set up logger
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[args.loglevel]

102
Main.py
View File

@ -13,7 +13,7 @@ from Items import ItemFactory
from Regions import create_regions, create_shops, mark_light_world_regions
from InvertedRegions import create_inverted_regions, mark_dark_world_regions
from EntranceShuffle import link_entrances, link_inverted_entrances
from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom, get_hash_string
from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, get_hash_string
from Rules import set_rules
from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive
from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items, balance_multiworld_progression
@ -155,7 +155,6 @@ def main(args, seed=None):
outfilebase = 'BM_%s' % (args.outputname if args.outputname else world.seed)
rom_names = []
jsonout = {}
def _gen_rom(team: int, player: int):
sprite_random_on_hit = type(args.sprite[player]) is str and args.sprite[player].lower() == 'randomonhit'
@ -163,15 +162,13 @@ def main(args, seed=None):
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
or args.shufflepots[player] or sprite_random_on_hit)
rom = JsonRom() if args.jsonout or use_enemizer else LocalRom(args.rom)
rom = LocalRom(args.rom)
patch_rom(world, rom, player, team, use_enemizer)
if use_enemizer and (args.enemizercli or not args.jsonout):
if use_enemizer:
patch_enemizer(world, player, rom, args.rom, args.enemizercli, args.shufflepots[player],
sprite_random_on_hit)
if not args.jsonout:
rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000)
if args.race:
patch_race_rom(rom)
@ -182,49 +179,46 @@ def main(args, seed=None):
args.fastmenu[player], args.disablemusic[player], args.sprite[player],
args.ow_palettes[player], args.uw_palettes[player])
if args.jsonout:
jsonout[f'patch_t{team}_p{player}'] = rom.patches
else:
mcsb_name = ''
if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
world.bigkeyshuffle[player]]):
mcsb_name = '-keysanity'
elif [world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
world.bigkeyshuffle[player]].count(True) == 1:
mcsb_name = '-mapshuffle' if world.mapshuffle[player] else '-compassshuffle' if world.compassshuffle[
player] else '-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle'
elif any([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
world.bigkeyshuffle[player]]):
mcsb_name = '-%s%s%s%sshuffle' % (
'M' if world.mapshuffle[player] else '', 'C' if world.compassshuffle[player] else '',
'S' if world.keyshuffle[player] else '', 'B' if world.bigkeyshuffle[player] else '')
mcsb_name = ''
if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
world.bigkeyshuffle[player]]):
mcsb_name = '-keysanity'
elif [world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
world.bigkeyshuffle[player]].count(True) == 1:
mcsb_name = '-mapshuffle' if world.mapshuffle[player] else '-compassshuffle' if world.compassshuffle[
player] else '-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle'
elif any([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
world.bigkeyshuffle[player]]):
mcsb_name = '-%s%s%s%sshuffle' % (
'M' if world.mapshuffle[player] else '', 'C' if world.compassshuffle[player] else '',
'S' if world.keyshuffle[player] else '', 'B' if world.bigkeyshuffle[player] else '')
outfilepname = f'_T{team + 1}' if world.teams > 1 else ''
if world.players > 1:
outfilepname += f'_P{player}'
if world.players > 1 or world.teams > 1:
outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" if world.player_names[player][
team] != 'Player%d' % player else ''
outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % (world.logic[player], world.difficulty[player],
world.difficulty_adjustments[player],
world.mode[player], world.goal[player],
"" if world.timer[player] in [False,
'display'] else "-" +
world.timer[
player],
world.shuffle[player], world.algorithm,
mcsb_name,
"-retro" if world.retro[player] else "",
"-prog_" + world.progressive[player] if
world.progressive[player] in ['off',
'random'] else "",
"-nohints" if not world.hints[
player] else "")) if not args.outputname else ''
rompath = output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc')
rom.write_to_file(rompath)
if args.create_diff:
import Patch
Patch.create_patch_file(rompath)
outfilepname = f'_T{team + 1}' if world.teams > 1 else ''
if world.players > 1:
outfilepname += f'_P{player}'
if world.players > 1 or world.teams > 1:
outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" if world.player_names[player][
team] != 'Player%d' % player else ''
outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % (world.logic[player], world.difficulty[player],
world.difficulty_adjustments[player],
world.mode[player], world.goal[player],
"" if world.timer[player] in [False,
'display'] else "-" +
world.timer[
player],
world.shuffle[player], world.algorithm,
mcsb_name,
"-retro" if world.retro[player] else "",
"-prog_" + world.progressive[player] if
world.progressive[player] in ['off',
'random'] else "",
"-nohints" if not world.hints[
player] else "")) if not args.outputname else ''
rompath = output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc')
rom.write_to_file(rompath)
if args.create_diff:
import Patch
Patch.create_patch_file(rompath)
return (player, team, list(rom.name))
if not args.suppress_rom:
@ -266,19 +260,15 @@ def main(args, seed=None):
"server_options": get_options()["server_options"],
"er_hint_data": er_hint_data,
}).encode("utf-8"), 9)
if args.jsonout:
jsonout["multidata"] = list(multidata)
else:
with open(output_path('%s.multidata' % outfilebase), 'wb') as f:
f.write(multidata)
with open(output_path('%s.multidata' % outfilebase), 'wb') as f:
f.write(multidata)
if not args.skip_playthrough:
logger.info('Calculating playthrough.')
create_playthrough(world)
if args.jsonout:
print(json.dumps({**jsonout, 'spoiler': world.spoiler.to_json()}))
elif args.create_spoiler:
if args.create_spoiler:
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
logger.info('Done. Enjoy.')

View File

@ -57,16 +57,19 @@ def create_patch_file(rom_file_to_patch: str, server: str = "") -> str:
write_lzma(bytes, target)
return target
def create_rom_file(patch_file: str) -> Tuple[dict, str]:
def create_rom_bytes(patch_file: str) -> Tuple[dict, str, bytearray]:
data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig"))
patched_data = bsdiff4.patch(get_base_rom_bytes(), data["patch"])
rom_hash = patched_data[int(0x7FC0):int(0x7FD5)]
data["meta"]["hash"] = "".join(chr(x) for x in rom_hash)
target = os.path.splitext(patch_file)[0] + ".sfc"
return data["meta"], target, patched_data
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["meta"], target
return data, target
def update_patch_data(patch_data: bytes, server: str = "") -> bytes:

120
Rom.py
View File

@ -24,53 +24,6 @@ from EntranceShuffle import door_addresses
JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = 'aec17dd8b3c76c16d0b0311c36eb1c00'
class JsonRom(object):
def __init__(self, name=None, hash=None):
self.name = name
self.hash = hash
self.orig_buffer = None
self.patches = {}
self.addresses = []
def write_byte(self, address, value):
self.write_bytes(address, [value])
def write_bytes(self, startaddress, values):
if not values:
return
values = list(values)
pos = bisect.bisect_right(self.addresses, startaddress)
intervalstart = self.addresses[pos-1] if pos else None
intervalpatch = self.patches[str(intervalstart)] if pos else None
if pos and startaddress <= intervalstart + len(intervalpatch): # merge with previous segment
offset = startaddress - intervalstart
intervalpatch[offset:offset+len(values)] = values
startaddress = intervalstart
values = intervalpatch
else: # new segment
self.addresses.insert(pos, startaddress)
self.patches[str(startaddress)] = values
pos = pos + 1
while pos < len(self.addresses) and self.addresses[pos] <= startaddress + len(values): # merge the next segment into this one
intervalstart = self.addresses[pos]
values.extend(self.patches[str(intervalstart)][startaddress+len(values)-intervalstart:])
del self.patches[str(intervalstart)]
del self.addresses[pos]
def write_to_file(self, file):
with open(file, 'w') as stream:
json.dump([self.patches], stream)
def get_hash(self):
h = hashlib.md5()
h.update(json.dumps([self.patches]).encode('utf-8'))
return h.hexdigest()
class LocalRom(object):
def __init__(self, file, patch=True, name=None, hash=None):
@ -93,6 +46,10 @@ class LocalRom(object):
with open(file, 'wb') as outfile:
outfile.write(self.buffer)
def read_from_file(self, file):
with open(file, 'rb') as stream:
self.buffer = bytearray(stream.read())
@staticmethod
def fromJsonRom(rom, file, rom_size=0x200000):
ret = LocalRom(file, True, rom.name, rom.hash)
@ -101,12 +58,37 @@ class LocalRom(object):
ret.write_bytes(int(address), values)
return ret
@staticmethod
def verify(buffer, expected=RANDOMIZERBASEHASH):
buffermd5 = hashlib.md5()
buffermd5.update(buffer)
return expected == buffermd5.hexdigest()
def patch_base_rom(self):
from Patch import create_patch_file, create_rom_bytes
if os.path.isfile(local_path('data/baserom.sfc')):
with open(local_path('data/baserom.sfc'), 'rb') as stream:
buffer = bytearray(stream.read())
if self.verify(buffer):
self.buffer = buffer
if not os.path.exists(local_path('data/baserom.bmbp')):
create_patch_file(local_path('data/baserom.sfc'))
return
if os.path.isfile(local_path('data/baserom.bmbp')):
_, target, buffer = create_rom_bytes(local_path('data/baserom.bmbp'))
if self.verify(buffer):
self.buffer = bytearray(buffer)
with open(local_path('data/baserom.sfc'), 'wb') as stream:
stream.write(buffer)
return
# verify correct checksum of baserom
basemd5 = hashlib.md5()
basemd5.update(self.buffer)
if JAP10HASH != basemd5.hexdigest():
logging.getLogger('').warning('Supplied Base Rom does not match known MD5 for JAP(1.0) release. Will try to patch anyway.')
if not self.verify(self.buffer, JAP10HASH):
logging.getLogger('').warning(
'Supplied Base Rom does not match known MD5 for JAP(1.0) release. Will try to patch anyway.')
# extend to 2MB
self.buffer.extend(bytearray([0x00]) * (0x200000 - len(self.buffer)))
@ -120,10 +102,14 @@ class LocalRom(object):
self.write_bytes(int(baseaddress), values)
# verify md5
patchedmd5 = hashlib.md5()
patchedmd5.update(self.buffer)
if patchedmd5.hexdigest() not in [RANDOMIZERBASEHASH]:
raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.')
if self.verify(self.buffer):
with open(local_path('data/baserom.sfc'), 'wb') as stream:
stream.write(self.buffer)
create_patch_file(local_path('data/baserom.sfc'))
os.remove(local_path('data/base2current.json'))
else:
raise RuntimeError(
'Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.')
def write_crc(self):
crc = (sum(self.buffer[:0x7FDC] + self.buffer[0x7FE0:]) + 0x01FE) & 0xFFFF
@ -158,12 +144,9 @@ def read_rom(stream) -> bytearray:
return buffer
def patch_enemizer(world, player, rom, baserom_path, enemizercli, shufflepots, random_sprite_on_hit):
baserom_path = os.path.abspath(baserom_path)
basepatch_path = os.path.abspath(local_path('data/base2current.json'))
enemizer_basepatch_path = os.path.join(os.path.dirname(enemizercli), "enemizerBasePatch.json")
randopatch_path = os.path.abspath(output_path(f'enemizer_randopatch_{player}.json'))
randopatch_path = os.path.abspath(output_path(f'enemizer_randopatch_{player}.sfc'))
options_path = os.path.abspath(output_path(f'enemizer_options_{player}.json'))
enemizer_output_path = os.path.abspath(output_path(f'enemizer_output_{player}.json'))
enemizer_output_path = os.path.abspath(output_path(f'enemizer_output_{player}.sfc'))
# write options file for enemizer
options = {
@ -255,21 +238,14 @@ def patch_enemizer(world, player, rom, baserom_path, enemizercli, shufflepots, r
json.dump(options, f)
subprocess.check_call([os.path.abspath(enemizercli),
'--rom', baserom_path,
'--rom', randopatch_path,
'--seed', str(world.rom_seeds[player]),
'--base', basepatch_path,
'--randomizer', randopatch_path,
'--binary',
'--enemizer', options_path,
'--output', enemizer_output_path],
cwd=os.path.dirname(enemizercli), stdout=subprocess.DEVNULL)
with open(enemizer_basepatch_path, 'r') as f:
for patch in json.load(f):
rom.write_bytes(patch["address"], patch["patchData"])
with open(enemizer_output_path, 'r') as f:
for patch in json.load(f):
rom.write_bytes(patch["address"], patch["patchData"])
rom.read_from_file(enemizer_output_path)
os.remove(enemizer_output_path)
if random_sprite_on_hit:
_populate_sprite_table()
@ -285,7 +261,7 @@ def patch_enemizer(world, player, rom, baserom_path, enemizercli, shufflepots, r
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
for used in (randopatch_path, options_path, enemizer_output_path):
for used in (randopatch_path, options_path):
try:
os.remove(used)
except OSError:

File diff suppressed because one or more lines are too long

BIN
data/baserom.bmbp Normal file

Binary file not shown.