From 6966b0a7980d2da53ed1047ef57224897d592608 Mon Sep 17 00:00:00 2001 From: Bonta-kun <40473493+Bonta0@users.noreply.github.com> Date: Wed, 1 Jan 2020 18:42:36 +0100 Subject: [PATCH 01/20] Add a --keysanity shortcut to enable all dungeon items shuffles for convenience --- EntranceRandomizer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/EntranceRandomizer.py b/EntranceRandomizer.py index 91fa5bfa..204fb40b 100755 --- a/EntranceRandomizer.py +++ b/EntranceRandomizer.py @@ -211,6 +211,7 @@ def parse_arguments(argv, no_defaults=False): parser.add_argument('--compassshuffle', default=defval(False), help='Compasses are no longer restricted to their dungeons, but can be anywhere', action='store_true') parser.add_argument('--keyshuffle', default=defval(False), help='Small Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true') parser.add_argument('--bigkeyshuffle', default=defval(False), help='Big Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true') + parser.add_argument('--keysanity', default=defval(False), help=argparse.SUPPRESS, action='store_true') parser.add_argument('--retro', default=defval(False), help='''\ Keys are universal, shooting arrows costs rupees, and a few other little things make this more like Zelda-1. @@ -273,6 +274,8 @@ def parse_arguments(argv, no_defaults=False): parser.add_argument(f'--p{player}', default=defval(''), help=argparse.SUPPRESS) ret = parser.parse_args(argv) + if ret.keysanity: + ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = [True] * 4 if multiargs.multi: defaults = copy.deepcopy(ret) From 28e7819fab83612cf2d5e654694cb2eff966a6ec Mon Sep 17 00:00:00 2001 From: Bonta-kun <40473493+Bonta0@users.noreply.github.com> Date: Sat, 4 Jan 2020 18:45:42 +0100 Subject: [PATCH 02/20] Items table: include the 2nd progressive bow --- Items.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Items.py b/Items.py index ff3d9e13..d134fdd4 100644 --- a/Items.py +++ b/Items.py @@ -25,6 +25,7 @@ def ItemFactory(items, player): # Format: Name: (Advancement, Priority, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text) item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'), 'Progressive Bow': (True, False, None, 0x64, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), + 'Progressive Bow (Alt)': (True, False, None, 0x65, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), 'Book of Mudora': (True, False, None, 0x1D, 'This is a\nparadox?!', 'and the story book', 'the scholarly kid', 'moon runes for sale', 'drugs for literacy', 'book-worm boy can read again', 'the Book'), 'Hammer': (True, False, None, 0x09, 'stop\nhammer time!', 'and m c hammer', 'hammer-smashing kid', 'm c hammer for sale', 'stop... hammer time', 'stop, hammer time', 'the hammer'), 'Hookshot': (True, False, None, 0x0A, 'BOING!!!\nBOING!!!\nBOING!!!', 'and the tickle beam', 'tickle-monster kid', 'tickle beam for sale', 'witch and tickle boy', 'beam boy tickles again', 'the Hookshot'), From 1be0d62d4fff895899018edec7eb42b69b1c00fa Mon Sep 17 00:00:00 2001 From: Bonta-kun <40473493+Bonta0@users.noreply.github.com> Date: Sun, 5 Jan 2020 20:22:19 +0100 Subject: [PATCH 03/20] MultiClient: allow different protocols if a prefix is present --- MultiClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MultiClient.py b/MultiClient.py index fd7cfa9a..7435cc2f 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -543,7 +543,7 @@ async def server_loop(ctx : Context): print('Enter multiworld server address') ctx.server_address = await console_input(ctx) - address = 'ws://' + ctx.server_address + address = f"ws://{ctx.server_address}" if "://" not in ctx.server_address else ctx.server_address print('Connecting to multiworld server at %s' % address) try: From a3657c02aa6f9545911984c64b37e866645d7491 Mon Sep 17 00:00:00 2001 From: Bonta-kun <40473493+Bonta0@users.noreply.github.com> Date: Sat, 4 Jan 2020 22:08:13 +0100 Subject: [PATCH 04/20] Multidata/save: moved away from pickle and store a compressed json instead --- Main.py | 18 +++++++----------- MultiServer.py | 45 +++++++++++++++++++++++++-------------------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/Main.py b/Main.py index f8af954c..31b28244 100644 --- a/Main.py +++ b/Main.py @@ -4,9 +4,9 @@ from itertools import zip_longest import json import logging import os -import pickle import random import time +import zlib from BaseClasses import World, CollectionState, Item, Region, Location, Shop from Regions import create_regions, mark_light_world_regions @@ -140,12 +140,9 @@ def main(args, seed=None): player_names = parse_names_string(args.names) outfilebase = 'ER_%s' % (args.outputname if args.outputname else world.seed) + rom_names = [] jsonout = {} if not args.suppress_rom: - from MultiServer import MultiWorld - multidata = MultiWorld() - multidata.players = world.players - for player in range(1, world.players + 1): use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] != 'none' or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default' @@ -161,16 +158,12 @@ def main(args, seed=None): else: rom = LocalRom(args.rom) patch_rom(world, player, rom, use_enemizer) + rom_names.append((player, list(rom.name))) enemizer_patch = [] if use_enemizer and (args.enemizercli or not args.jsonout): enemizer_patch = get_enemizer_patch(world, player, rom, args.rom, args.enemizercli, args.shufflepalette[player], args.shufflepots[player]) - multidata.rom_names[player] = list(rom.name) - for location in world.get_filled_locations(player): - if type(location.address) is int: - multidata.locations[(location.address, player)] = (location.item.code, location.item.player) - if args.jsonout: jsonout[f'patch{player}'] = rom.patches if use_enemizer: @@ -209,7 +202,10 @@ def main(args, seed=None): rom.write_to_file(output_path(f'{outfilebase}{playername}{outfilesuffix}.sfc')) with open(output_path('%s_multidata' % outfilebase), 'wb') as f: - pickle.dump(multidata, f, pickle.HIGHEST_PROTOCOL) + jsonstr = json.dumps((world.players, + rom_names, + [((location.address, location.player), (location.item.code, location.item.player)) for location in world.get_filled_locations() if type(location.address) is int])) + f.write(zlib.compress(jsonstr.encode("utf-8"))) if args.create_spoiler and not args.jsonout: world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) diff --git a/MultiServer.py b/MultiServer.py index cb753721..87d1fd41 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -4,10 +4,10 @@ import asyncio import functools import json import logging -import pickle import re import urllib.request import websockets +import zlib import Items import Regions @@ -22,18 +22,14 @@ class Client: self.slot = None self.send_index = 0 -class MultiWorld: - def __init__(self): - self.players = None - self.rom_names = {} - self.locations = {} - class Context: def __init__(self, host, port, password): self.data_filename = None self.save_filename = None self.disable_save = False - self.world = MultiWorld() + self.players = 0 + self.rom_names = {} + self.locations = {} self.host = host self.port = port self.password = password @@ -44,7 +40,7 @@ class Context: def get_room_info(ctx : Context): return { 'password': ctx.password is not None, - 'slots': ctx.world.players, + 'slots': ctx.players, 'players': [(client.name, client.team, client.slot) for client in ctx.clients if client.auth] } @@ -175,8 +171,8 @@ def forfeit_player(ctx : Context, team, slot, name): def register_location_checks(ctx : Context, name, team, slot, locations): found_items = False for location in locations: - if (location, slot) in ctx.world.locations: - target_item, target_player = ctx.world.locations[(location, slot)] + if (location, slot) in ctx.locations: + target_item, target_player = ctx.locations[(location, slot)] if target_player != slot: found = False recvd_items = get_received_items(ctx, team, target_player) @@ -196,7 +192,10 @@ def register_location_checks(ctx : Context, name, team, slot, locations): if found_items and not ctx.disable_save: try: with open(ctx.save_filename, "wb") as f: - pickle.dump((ctx.world.players, ctx.world.rom_names, ctx.received_items), f, pickle.HIGHEST_PROTOCOL) + jsonstr = json.dumps((ctx.players, + [(k, v) for k, v in ctx.rom_names.items()], + [(k, [i.__dict__ for i in v]) for k, v in ctx.received_items.items()])) + f.write(zlib.compress(jsonstr.encode("utf-8"))) except Exception as e: logging.exception(e) @@ -233,13 +232,13 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args): if 'slot' in args and any([c.slot == args['slot'] for c in ctx.clients if c.auth and same_team(c.team, client.team)]): errors.add('SlotAlreadyTaken') elif 'slot' not in args or not args['slot']: - for slot in range(1, ctx.world.players + 1): + for slot in range(1, ctx.players + 1): if slot not in [c.slot for c in ctx.clients if c.auth and same_team(c.team, client.team)]: client.slot = slot break - elif slot == ctx.world.players: + elif slot == ctx.players: errors.add('SlotAlreadyTaken') - elif args['slot'] not in range(1, ctx.world.players + 1): + elif args['slot'] not in range(1, ctx.players + 1): errors.add('InvalidSlot') else: client.slot = args['slot'] @@ -251,7 +250,7 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args): await send_msgs(client.socket, [['ConnectionRefused', list(errors)]]) else: client.auth = True - reply = [['Connected', ctx.world.rom_names[client.slot]]] + reply = [['Connected', ctx.rom_names[client.slot]]] items = get_received_items(ctx, client.team, client.slot) if items: reply.append(['ReceivedItems', (0, tuplize_received_items(items))]) @@ -358,13 +357,16 @@ async def main(): ctx.data_filename = tkinter.filedialog.askopenfilename(filetypes=(("Multiworld data","*multidata"),)) with open(ctx.data_filename, 'rb') as f: - ctx.world = pickle.load(f) + jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8")) + ctx.players = jsonobj[0] + ctx.rom_names = {k: v for k, v in jsonobj[1]} + ctx.locations = {tuple(k): tuple(v) for k, v in jsonobj[2]} except Exception as e: print('Failed to read multiworld data (%s)' % e) return ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8') if not ctx.host else ctx.host - print('Hosting game of %d players (%s) at %s:%d' % (ctx.world.players, 'No password' if not ctx.password else 'Password: %s' % ctx.password, ip, ctx.port)) + print('Hosting game of %d players (%s) at %s:%d' % (ctx.players, 'No password' if not ctx.password else 'Password: %s' % ctx.password, ip, ctx.port)) ctx.disable_save = args.disable_save if not ctx.disable_save: @@ -372,8 +374,11 @@ async def main(): ctx.save_filename = (ctx.data_filename[:-9] if ctx.data_filename[-9:] == 'multidata' else (ctx.data_filename + '_')) + 'multisave' try: with open(ctx.save_filename, 'rb') as f: - players, rom_names, received_items = pickle.load(f) - if players != ctx.world.players or rom_names != ctx.world.rom_names: + jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8")) + players = jsonobj[0] + rom_names = {k: v for k, v in jsonobj[1]} + received_items = {tuple(k): [ReceivedItem(**i) for i in v] for k, v in jsonobj[2]} + if players != ctx.players or rom_names != ctx.rom_names: raise Exception('Save file mismatch, will start a new game') ctx.received_items = received_items print('Loaded save file with %d received items for %d players' % (sum([len(p) for p in received_items.values()]), len(received_items))) From eb7ca4fdf975e95e9c23788c916735edd18abf5f Mon Sep 17 00:00:00 2001 From: Bonta-kun <40473493+Bonta0@users.noreply.github.com> Date: Mon, 6 Jan 2020 19:13:42 +0100 Subject: [PATCH 05/20] Implement --startinventory --- EntranceRandomizer.py | 3 +- ItemList.py | 58 ++++++--------- Items.py | 2 +- Main.py | 6 ++ Rom.py | 164 ++++++++++++++++++++++++++++++++++++++---- 5 files changed, 180 insertions(+), 53 deletions(-) diff --git a/EntranceRandomizer.py b/EntranceRandomizer.py index 204fb40b..c6f03721 100755 --- a/EntranceRandomizer.py +++ b/EntranceRandomizer.py @@ -216,6 +216,7 @@ def parse_arguments(argv, no_defaults=False): Keys are universal, shooting arrows costs rupees, and a few other little things make this more like Zelda-1. ''', action='store_true') + parser.add_argument('--startinventory', default=defval(''), help='Specifies a list of items that will be in your starting inventory (separated by commas)') parser.add_argument('--custom', default=defval(False), help='Not supported.') parser.add_argument('--customitemarray', default=defval(False), help='Not supported.') parser.add_argument('--accessibility', default=defval('items'), const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\ @@ -284,7 +285,7 @@ def parse_arguments(argv, no_defaults=False): for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'shuffle', 'crystals_ganon', 'crystals_gt', 'openpyramid', - 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', + 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', 'retro', 'accessibility', 'hints', 'shufflepalette', 'shufflepots', 'beemizer', 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) diff --git a/ItemList.py b/ItemList.py index 19eb83e3..73e40043 100644 --- a/ItemList.py +++ b/ItemList.py @@ -51,8 +51,8 @@ difficulties = { progressivearmor = ['Progressive Armor'] * 2, basicarmor = ['Blue Mail', 'Red Mail'], swordless = ['Rupees (20)'] * 4, - progressivesword = ['Progressive Sword'] * 3, - basicsword = ['Master Sword', 'Tempered Sword', 'Golden Sword'], + progressivesword = ['Progressive Sword'] * 4, + basicsword = ['Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'], basicbow = ['Bow', 'Silver Arrows'], timedohko = ['Green Clock'] * 25, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, @@ -78,8 +78,8 @@ difficulties = { progressivearmor = ['Progressive Armor'] * 2, basicarmor = ['Progressive Armor'] * 2, # neither will count swordless = ['Rupees (20)'] * 4, - progressivesword = ['Progressive Sword'] * 3, - basicsword = ['Master Sword', 'Master Sword', 'Tempered Sword'], + progressivesword = ['Progressive Sword'] * 4, + basicsword = ['Fighter Sword', 'Master Sword', 'Master Sword', 'Tempered Sword'], basicbow = ['Bow'] * 2, timedohko = ['Green Clock'] * 25, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, @@ -105,8 +105,8 @@ difficulties = { progressivearmor = ['Progressive Armor'] * 2, # neither will count basicarmor = ['Progressive Armor'] * 2, # neither will count swordless = ['Rupees (20)'] * 4, - progressivesword = ['Progressive Sword'] * 3, - basicsword = ['Fighter Sword', 'Master Sword', 'Master Sword'], + progressivesword = ['Progressive Sword'] * 4, + basicsword = ['Fighter Sword', 'Fighter Sword', 'Master Sword', 'Master Sword'], basicbow = ['Bow'] * 2, timedohko = ['Green Clock'] * 20 + ['Red Clock'] * 5, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, @@ -444,34 +444,17 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r else: pool.extend(diff.basicarmor) - if swords != 'swordless': - if want_progressives(): - pool.extend(['Progressive Bow'] * 2) - else: - pool.extend(diff.basicbow) + if want_progressives(): + pool.extend(['Progressive Bow'] * 2) + elif swords != 'swordless': + pool.extend(diff.basicbow) + else: + pool.extend(['Bow', 'Silver Arrows']) if swords == 'swordless': pool.extend(diff.swordless) - if want_progressives(): - pool.extend(['Progressive Bow'] * 2) - else: - pool.extend(['Bow', 'Silver Arrows']) - elif swords == 'assured': - precollected_items.append('Fighter Sword') - if want_progressives(): - pool.extend(diff.progressivesword) - pool.extend(['Rupees (100)']) - else: - pool.extend(diff.basicsword) - pool.extend(['Rupees (100)']) elif swords == 'vanilla': - swords_to_use = [] - if want_progressives(): - swords_to_use.extend(diff.progressivesword) - swords_to_use.extend(['Progressive Sword']) - else: - swords_to_use.extend(diff.basicsword) - swords_to_use.extend(['Fighter Sword']) + swords_to_use = diff.progressivesword.copy() if want_progressives() else diff.basicsword.copy() random.shuffle(swords_to_use) place_item('Link\'s Uncle', swords_to_use.pop()) @@ -482,12 +465,15 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r else: place_item('Master Sword Pedestal', 'Triforce') else: - if want_progressives(): - pool.extend(diff.progressivesword) - pool.extend(['Progressive Sword']) - else: - pool.extend(diff.basicsword) - pool.extend(['Fighter Sword']) + pool.extend(diff.progressivesword if want_progressives() else diff.basicsword) + if swords == 'assured': + if want_progressives(): + precollected_items.append('Progressive Sword') + pool.remove('Progressive Sword') + else: + precollected_items.append('Fighter Sword') + pool.remove('Fighter Sword') + pool.extend(['Rupees (50)']) extraitems = total_items_to_place - len(pool) - len(placed_items) diff --git a/Items.py b/Items.py index d134fdd4..f2d4e910 100644 --- a/Items.py +++ b/Items.py @@ -44,8 +44,8 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla 'Flippers': (True, False, None, 0x1E, 'fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the flippers'), 'Ice Rod': (True, False, None, 0x08, 'I\'m the cold\nrod. I make\nthings freeze!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again', 'the ice rod'), 'Titans Mitts': (True, False, None, 0x1C, 'Now you can\nlift heavy\nstuff!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again', 'the mitts'), - 'Ether': (True, False, None, 0x10, 'This magic\ncoin freezes\neverything!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again', 'Ether'), 'Bombos': (True, False, None, 0x0F, 'Burn, baby,\nburn! Fear my\nring of fire!', 'and the swirly coin', 'coin-collecting kid', 'swirly coin for sale', 'shrooms for swirly-coin', 'medallion boy melts room again', 'Bombos'), + 'Ether': (True, False, None, 0x10, 'This magic\ncoin freezes\neverything!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again', 'Ether'), 'Quake': (True, False, None, 0x11, 'Maxing out the\nRichter scale\nis what I do!', 'and the wavy coin', 'coin-collecting kid', 'wavy coin for sale', 'shrooms for wavy-coin', 'medallion boy shakes dirt again', 'Quake'), 'Bottle': (True, False, None, 0x16, 'Now you can\nstore potions\nand stuff!', 'and the terrarium', 'the terrarium kid', 'terrarium for sale', 'special promotion', 'bottle boy has terrarium again', 'a Bottle'), 'Bottle (Red Potion)': (True, False, None, 0x2B, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a Bottle'), diff --git a/Main.py b/Main.py index 31b28244..8e7ccfb4 100644 --- a/Main.py +++ b/Main.py @@ -9,6 +9,7 @@ import time import zlib from BaseClasses import World, CollectionState, Item, Region, Location, Shop +from Items import ItemFactory from Regions import create_regions, mark_light_world_regions from InvertedRegions import create_inverted_regions, mark_dark_world_regions from EntranceShuffle import link_entrances, link_inverted_entrances @@ -64,6 +65,11 @@ def main(args, seed=None): if world.mode[player] == 'standard' and world.enemy_shuffle[player] != 'none': world.escape_assist[player].append('bombs') # enemized escape assumes infinite bombs available and will likely be unbeatable without it + for tok in filter(None, args.startinventory[player].split(',')): + item = ItemFactory(tok.strip(), player) + if item: + world.push_precollected(item) + if world.mode[player] != 'inverted': create_regions(world, player) else: diff --git a/Rom.py b/Rom.py index aa4c150a..adb462b8 100644 --- a/Rom.py +++ b/Rom.py @@ -9,13 +9,13 @@ import struct import sys import subprocess -from BaseClasses import ShopType, Region, Location, Item +from BaseClasses import CollectionState, ShopType, Region, Location from Dungeons import dungeon_music_addresses from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable from Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts, junk_texts from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc -from Items import ItemFactory, item_table +from Items import ItemFactory from EntranceShuffle import door_addresses @@ -895,24 +895,158 @@ def patch_rom(world, player, rom, enemized): rom.write_byte(0x1800A1, 0x01) # enable overworld screen transition draining for water level inside swamp rom.write_byte(0x180174, 0x01 if world.fix_fake_world[player] else 0x00) rom.write_byte(0x18017E, 0x01) # Fairy fountains only trade in bottles - rom.write_byte(0x180034, 0x0A) # starting max bombs - rom.write_byte(0x180035, 30) # starting max arrows - for x in range(0x183000, 0x18304F): - rom.write_byte(x, 0) # Zero the initial equipment array - rom.write_byte(0x18302C, 0x18) # starting max health - rom.write_byte(0x18302D, 0x18) # starting current health - rom.write_byte(0x183039, 0x68) # starting abilities, bit array - + + # Starting equipment + equip = [0] * (0x340 + 0x4F) + equip[0x36C] = 0x18 + equip[0x36D] = 0x18 + equip[0x379] = 0x68 + starting_max_bombs = 10 + starting_max_arrows = 30 + + startingstate = CollectionState(world) + + if startingstate.has('Bow', player): + equip[0x340] = 1 + equip[0x38E] |= 0x20 # progressive flag to get the correct hint in all cases + if not world.retro[player]: + equip[0x38E] |= 0x80 + if startingstate.has('Silver Arrows', player): + equip[0x38E] |= 0x40 + + if startingstate.has('Titans Mitts', player): + equip[0x354] = 2 + elif startingstate.has('Power Glove', player): + equip[0x354] = 1 + + if startingstate.has('Golden Sword', player): + equip[0x359] = 4 + elif startingstate.has('Tempered Sword', player): + equip[0x359] = 3 + elif startingstate.has('Master Sword', player): + equip[0x359] = 2 + elif startingstate.has('Fighter Sword', player): + equip[0x359] = 1 + + if startingstate.has('Mirror Shield', player): + equip[0x35A] = 3 + elif startingstate.has('Red Shield', player): + equip[0x35A] = 2 + elif startingstate.has('Blue Shield', player): + equip[0x35A] = 1 + + if startingstate.has('Red Mail', player): + equip[0x35B] = 2 + elif startingstate.has('Blue Mail', player): + equip[0x35B] = 1 + + if startingstate.has('Magic Upgrade (1/4)', player): + equip[0x37B] = 2 + equip[0x36E] = 0x80 + elif startingstate.has('Magic Upgrade (1/2)', player): + equip[0x37B] = 1 + equip[0x36E] = 0x80 + for item in world.precollected_items: if item.player != player: continue - if item.name == 'Fighter Sword': - rom.write_byte(0x183000+0x19, 0x01) - rom.write_byte(0x0271A6+0x19, 0x01) - rom.write_byte(0x180043, 0x01) # special starting sword byte + if item.name in ['Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)', + 'Titans Mitts', 'Power Glove', 'Progressive Glove', + 'Golden Sword', 'Tempered Sword', 'Master Sword', 'Fighter Sword', 'Progressive Sword', + 'Mirror Shield', 'Red Shield', 'Blue Shield', 'Progressive Shield', + 'Red Mail', 'Blue Mail', 'Progressive Armor', + 'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)']: + continue + + set_table = {'Book of Mudora': (0x34E, 1), 'Hammer': (0x34B, 1), 'Bug Catching Net': (0x34D, 1), 'Hookshot': (0x342, 1), 'Magic Mirror': (0x353, 2), + 'Cape': (0x352, 1), 'Lamp': (0x34A, 1), 'Moon Pearl': (0x357, 1), 'Cane of Somaria': (0x350, 1), 'Cane of Byrna': (0x351, 1), + 'Fire Rod': (0x345, 1), 'Ice Rod': (0x346, 1), 'Bombos': (0x347, 1), 'Ether': (0x348, 1), 'Quake': (0x349, 1)} + or_table = {'Green Pendant': (0x374, 0x04), 'Red Pendant': (0x374, 0x01), 'Blue Pendant': (0x374, 0x02), + 'Crystal 1': (0x37A, 0x02), 'Crystal 2': (0x37A, 0x10), 'Crystal 3': (0x37A, 0x40), 'Crystal 4': (0x37A, 0x20), + 'Crystal 5': (0x37A, 0x04), 'Crystal 6': (0x37A, 0x01), 'Crystal 7': (0x37A, 0x08), + 'Big Key (Eastern Palace)': (0x367, 0x20), 'Compass (Eastern Palace)': (0x365, 0x20), 'Map (Eastern Palace)': (0x369, 0x20), + 'Big Key (Desert Palace)': (0x367, 0x10), 'Compass (Desert Palace)': (0x365, 0x10), 'Map (Desert Palace)': (0x369, 0x10), + 'Big Key (Tower of Hera)': (0x366, 0x20), 'Compass (Tower of Hera)': (0x364, 0x20), 'Map (Tower of Hera)': (0x368, 0x20), + 'Big Key (Escape)': (0x367, 0xC0), 'Compass (Escape)': (0x365, 0xC0), 'Map (Escape)': (0x369, 0xC0), + 'Big Key (Palace of Darkness)': (0x367, 0x02), 'Compass (Palace of Darkness)': (0x365, 0x02), 'Map (Palace of Darkness)': (0x369, 0x02), + 'Big Key (Thieves Town)': (0x366, 0x10), 'Compass (Thieves Town)': (0x364, 0x10), 'Map (Thieves Town)': (0x368, 0x10), + 'Big Key (Skull Woods)': (0x366, 0x80), 'Compass (Skull Woods)': (0x364, 0x80), 'Map (Skull Woods)': (0x368, 0x80), + 'Big Key (Swamp Palace)': (0x367, 0x04), 'Compass (Swamp Palace)': (0x365, 0x04), 'Map (Swamp Palace)': (0x369, 0x04), + 'Big Key (Ice Palace)': (0x366, 0x40), 'Compass (Ice Palace)': (0x364, 0x40), 'Map (Ice Palace)': (0x368, 0x40), + 'Big Key (Misery Mire)': (0x367, 0x01), 'Compass (Misery Mire)': (0x365, 0x01), 'Map (Misery Mire)': (0x369, 0x01), + 'Big Key (Turtle Rock)': (0x366, 0x08), 'Compass (Turtle Rock)': (0x364, 0x08), 'Map (Turtle Rock)': (0x368, 0x08), + 'Big Key (Ganons Tower)': (0x366, 0x04), 'Compass (Ganons Tower)': (0x364, 0x04), 'Map (Ganons Tower)': (0x368, 0x04)} + set_or_table = {'Flippers': (0x356, 1, 0x379, 0x02),'Pegasus Boots': (0x355, 1, 0x379, 0x04), + 'Shovel': (0x34C, 1, 0x38C, 0x04), 'Ocarina': (0x34C, 3, 0x38C, 0x01), + 'Mushroom': (0x344, 1, 0x38C, 0x20 | 0x08), 'Magic Powder': (0x344, 2, 0x38C, 0x10), + 'Blue Boomerang': (0x341, 1, 0x38C, 0x80), 'Red Boomerang': (0x341, 2, 0x38C, 0x40)} + keys = {'Small Key (Eastern Palace)': [0x37E], 'Small Key (Desert Palace)': [0x37F], + 'Small Key (Tower of Hera)': [0x386], + 'Small Key (Agahnims Tower)': [0x380], 'Small Key (Palace of Darkness)': [0x382], + 'Small Key (Thieves Town)': [0x387], + 'Small Key (Skull Woods)': [0x384], 'Small Key (Swamp Palace)': [0x381], + 'Small Key (Ice Palace)': [0x385], + 'Small Key (Misery Mire)': [0x383], 'Small Key (Turtle Rock)': [0x388], + 'Small Key (Ganons Tower)': [0x389], + 'Small Key (Universal)': [0x38B], 'Small Key (Escape)': [0x37C, 0x37D]} + bottles = {'Bottle': 2, 'Bottle (Red Potion)': 3, 'Bottle (Green Potion)': 4, 'Bottle (Blue Potion)': 5, + 'Bottle (Fairy)': 6, 'Bottle (Bee)': 7, 'Bottle (Good Bee)': 8} + rupees = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)': 50, 'Rupees (100)': 100, 'Rupees (300)': 300} + bomb_caps = {'Bomb Upgrade (+5)': 5, 'Bomb Upgrade (+10)': 10} + arrow_caps = {'Arrow Upgrade (+5)': 5, 'Arrow Upgrade (+10)': 10} + bombs = {'Single Bomb': 1, 'Bombs (3)': 3, 'Bombs (10)': 10} + arrows = {'Single Arrow': 1, 'Arrows (10)': 10} + + if item.name in set_table: + equip[set_table[item.name][0]] = set_table[item.name][1] + elif item.name in or_table: + equip[or_table[item.name][0]] |= or_table[item.name][1] + elif item.name in set_or_table: + equip[set_or_table[item.name][0]] = set_or_table[item.name][1] + equip[set_or_table[item.name][2]] |= set_or_table[item.name][3] + elif item.name in keys: + for address in keys[item.name]: + equip[address] = min(equip[address] + 1, 99) + elif item.name in bottles: + if equip[0x34F] < world.difficulty_requirements[player].progressive_bottle_limit: + equip[0x35C + equip[0x34F]] = bottles[item.name] + equip[0x34F] += 1 + elif item.name in rupees: + equip[0x360:0x362] = list(min(equip[0x360] + (equip[0x361] << 8) + rupees[item.name], 9999).to_bytes(2, byteorder='little', signed=False)) + equip[0x362:0x364] = list(min(equip[0x362] + (equip[0x363] << 8) + rupees[item.name], 9999).to_bytes(2, byteorder='little', signed=False)) + elif item.name in bomb_caps: + starting_max_bombs = min(starting_max_bombs + bomb_caps[item.name], 50) + elif item.name in arrow_caps: + starting_max_arrows = min(starting_max_arrows + arrow_caps[item.name], 70) + elif item.name in bombs: + equip[0x343] += bombs[item.name] + elif item.name in arrows: + if world.retro[player]: + equip[0x38E] |= 0x80 + equip[0x377] = 1 + else: + equip[0x377] += arrows[item.name] + elif item.name in ['Piece of Heart', 'Boss Heart Container', 'Sanctuary Heart Container']: + if item.name == 'Piece of Heart': + equip[0x36B] = (equip[0x36B] + 1) % 4 + if item.name != 'Piece of Heart' or equip[0x36B] == 0: + equip[0x36C] = min(equip[0x36C] + 0x08, 0xA0) + equip[0x36D] = min(equip[0x36D] + 0x08, 0xA0) else: - raise RuntimeError("Unsupported pre-collected item: {}".format(item)) + raise RuntimeError(f'Unsupported item in starting equipment: {item.name}') + + equip[0x343] = min(equip[0x343], starting_max_bombs) + rom.write_byte(0x180034, starting_max_bombs) + equip[0x377] = min(equip[0x377], starting_max_arrows) + rom.write_byte(0x180035, starting_max_arrows) + rom.write_bytes(0x180046, equip[0x360:0x362]) + if equip[0x359]: + rom.write_byte(0x180043, equip[0x359]) + + assert equip[:0x340] == [0] * 0x340 + rom.write_bytes(0x183000, equip[0x340:]) + rom.write_bytes(0x271A6, equip[0x340:0x340+60]) rom.write_byte(0x18004A, 0x00 if world.mode[player] != 'inverted' else 0x01) # Inverted mode rom.write_byte(0x18005D, 0x00) # Hammer always breaks barrier From 71b4f6e94b567bd917b52acc0ef14c014652074e Mon Sep 17 00:00:00 2001 From: Bonta-kun <40473493+Bonta0@users.noreply.github.com> Date: Mon, 6 Jan 2020 18:39:18 +0100 Subject: [PATCH 06/20] Set default value for --enemizercli --- .gitignore | 3 ++- EntranceRandomizer.py | 2 +- Gui.py | 2 +- Mystery.py | 3 ++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 2c13bec1..83d5e97f 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ README.html *multisave EnemizerCLI/ .mypy_cache/ -RaceRom.py \ No newline at end of file +RaceRom.py +weights/ diff --git a/EntranceRandomizer.py b/EntranceRandomizer.py index c6f03721..064d58ff 100755 --- a/EntranceRandomizer.py +++ b/EntranceRandomizer.py @@ -256,7 +256,7 @@ def parse_arguments(argv, no_defaults=False): for VT site integration, do not use otherwise. ''') parser.add_argument('--skip_playthrough', action='store_true', default=defval(False)) - parser.add_argument('--enemizercli', default=defval('')) + parser.add_argument('--enemizercli', default=defval('EnemizerCLI/EnemizerCLI.Core')) parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos']) parser.add_argument('--shuffleenemies', default=defval('none'), choices=['none', 'shuffled', 'chaos']) parser.add_argument('--enemy_health', default=defval('default'), choices=['default', 'easy', 'normal', 'hard', 'expert']) diff --git a/Gui.py b/Gui.py index b897866b..29b15442 100755 --- a/Gui.py +++ b/Gui.py @@ -309,7 +309,7 @@ def guiMain(args=None): enemizerPathFrame.grid(row=0, column=0, columnspan=3, sticky=W) enemizerCLIlabel = Label(enemizerPathFrame, text="EnemizerCLI path: ") enemizerCLIlabel.pack(side=LEFT) - enemizerCLIpathVar = StringVar() + enemizerCLIpathVar = StringVar(value="EnemizerCLI/EnemizerCLI.Core") enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=enemizerCLIpathVar, width=80) enemizerCLIpathEntry.pack(side=LEFT) def EnemizerSelectPath(): diff --git a/Mystery.py b/Mystery.py index 06f57aca..d43ef6c5 100644 --- a/Mystery.py +++ b/Mystery.py @@ -75,7 +75,8 @@ def main(): if args.rom: erargs.rom = args.rom - erargs.enemizercli = args.enemizercli + if args.enemizercli: + erargs.enemizercli = args.enemizercli settings_cache = {k: (roll_settings(v) if args.samesettings else None) for k, v in weights_cache.items()} From 7d05d697dd6c2d916758a93db9859b85256c029b Mon Sep 17 00:00:00 2001 From: Bonta-kun <40473493+Bonta0@users.noreply.github.com> Date: Mon, 6 Jan 2020 19:09:46 +0100 Subject: [PATCH 07/20] Mystery: can now roll for starting inventory items, eg: startinventory: Pegasus Boots: 'on': 1 'off': 0 Bombs (3): 'on': 1 'off': 1 --- Mystery.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Mystery.py b/Mystery.py index d43ef6c5..e19a5203 100644 --- a/Mystery.py +++ b/Mystery.py @@ -109,8 +109,8 @@ def get_weights(path): return parse_yaml(yaml) def roll_settings(weights): - def get_choice(option): - return random.choices(list(weights[option].keys()), weights=list(map(int,weights[option].values())))[0].replace('"','').replace("'",'') + def get_choice(option, root=weights): + return random.choices(list(root[option].keys()), weights=list(map(int,root[option].values())))[0].replace('"','').replace("'",'') ret = argparse.Namespace() @@ -202,6 +202,13 @@ def roll_settings(weights): ret.beemizer = int(get_choice('beemizer')) if 'beemizer' in weights.keys() else 1 # suck it :) + inventoryweights = weights.get('startinventory', {}) + startitems = [] + for item in inventoryweights.keys(): + if get_choice(item, inventoryweights) == 'on': + startitems.append(item) + ret.startinventory = ','.join(startitems) + return ret if __name__ == '__main__': From 48305adaa08f808316230e230e37af41791769a4 Mon Sep 17 00:00:00 2001 From: Bonta-kun <40473493+Bonta0@users.noreply.github.com> Date: Mon, 6 Jan 2020 20:01:03 +0100 Subject: [PATCH 08/20] Mystery: weights can now specify a default value for convenience, eg: dungeon_items: full startinventory: Pegasus Boots: on --- Mystery.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Mystery.py b/Mystery.py index e19a5203..298b9217 100644 --- a/Mystery.py +++ b/Mystery.py @@ -110,6 +110,10 @@ def get_weights(path): def roll_settings(weights): def get_choice(option, root=weights): + if type(root[option]) is not dict: + return root[option] + if not root[option]: + return None return random.choices(list(root[option].keys()), weights=list(map(int,root[option].values())))[0].replace('"','').replace("'",'') ret = argparse.Namespace() From 99577d9e4c2398d84ee9facb13521d6aa1d80e6b Mon Sep 17 00:00:00 2001 From: Bonta-kun <40473493+Bonta0@users.noreply.github.com> Date: Mon, 6 Jan 2020 20:14:16 +0100 Subject: [PATCH 09/20] Mystery: safer strip --- Mystery.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Mystery.py b/Mystery.py index 298b9217..9d7755ea 100644 --- a/Mystery.py +++ b/Mystery.py @@ -8,15 +8,18 @@ from EntranceRandomizer import parse_arguments from Main import main as ERmain def parse_yaml(txt): + def strip(s): + s = s.strip() + return '' if not s else s.strip('"') if s[0] == '"' else s.strip("'") if s[0] == "'" else s ret = {} indents = {len(txt) - len(txt.lstrip(' ')): ret} for line in txt.splitlines(): if not line: continue name, val = line.split(':', 1) - val = val.strip() + val = strip(val) spaces = len(name) - len(name.lstrip(' ')) - name = name.strip() + name = strip(name) if val: indents[spaces][name] = val else: @@ -114,7 +117,7 @@ def roll_settings(weights): return root[option] if not root[option]: return None - return random.choices(list(root[option].keys()), weights=list(map(int,root[option].values())))[0].replace('"','').replace("'",'') + return random.choices(list(root[option].keys()), weights=list(map(int,root[option].values())))[0] ret = argparse.Namespace() From 28011cf675ccef247e752f57db6d43fcf2a96cf3 Mon Sep 17 00:00:00 2001 From: Bonta-kun <40473493+Bonta0@users.noreply.github.com> Date: Wed, 8 Jan 2020 03:43:48 +0100 Subject: [PATCH 10/20] Built-in palette shuffle (including blackout) --- Adjuster.py | 7 +- AdjusterMain.py | 5 +- EntranceRandomizer.py | 8 +- Gui.py | 187 ++++++++++++++++++++++++++---------------- Main.py | 6 +- Plando.py | 6 +- Rom.py | 149 +++++++++++++++++++++++++++++++-- 7 files changed, 281 insertions(+), 87 deletions(-) diff --git a/Adjuster.py b/Adjuster.py index 393b7b75..d3a8239c 100755 --- a/Adjuster.py +++ b/Adjuster.py @@ -15,7 +15,8 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter): def main(): parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) - parser.add_argument('--rom', default='ER_base.sfc', help='Path to an ALttP JAP(1.0) rom to use as a base.') + parser.add_argument('--rom', default='ER_base.sfc', help='Path to an ALttPR rom to adjust.') + parser.add_argument('--baserom', default='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', help='Path to an ALttP JAP(1.0) rom to use as a base.') parser.add_argument('--loglevel', default='info', const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.') parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'], help='''\ @@ -31,6 +32,8 @@ def main(): ''') parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'], help='Select the color of Link\'s heart meter. (default: %(default)s)') + parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout']) + parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout']) parser.add_argument('--sprite', help='''\ Path to a sprite sheet to use for Link. Needs to be in binary format and have a length of 0x7000 (28672) bytes, @@ -43,7 +46,7 @@ def main(): # ToDo: Validate files further than mere existance 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) + input('Could not find valid 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 args.sprite is not None and not os.path.isfile(args.sprite): input('Could not find link sprite sheet at given location. \nPress Enter to exit.' % args.sprite) diff --git a/AdjusterMain.py b/AdjusterMain.py index f4dbb663..5bf6eeb4 100644 --- a/AdjusterMain.py +++ b/AdjusterMain.py @@ -24,10 +24,13 @@ 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, False) + if os.path.isfile(args.baserom): + baserom = LocalRom(args.baserom, 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.') - apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, sprite, parse_names_string(args.names)) + apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, sprite, args.ow_palettes, args.uw_palettes, parse_names_string(args.names)) rom.write_to_file(output_path('%s.sfc' % outfilebase)) diff --git a/EntranceRandomizer.py b/EntranceRandomizer.py index 064d58ff..c315c962 100755 --- a/EntranceRandomizer.py +++ b/EntranceRandomizer.py @@ -242,6 +242,8 @@ def parse_arguments(argv, no_defaults=False): ''') parser.add_argument('--heartcolor', default=defval('red'), const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'], help='Select the color of Link\'s heart meter. (default: %(default)s)') + parser.add_argument('--ow_palettes', default=defval('default'), choices=['default', 'random', 'blackout']) + parser.add_argument('--uw_palettes', default=defval('default'), choices=['default', 'random', 'blackout']) parser.add_argument('--sprite', help='''\ Path to a sprite sheet to use for Link. Needs to be in binary format and have a length of 0x7000 (28672) bytes, @@ -261,7 +263,6 @@ def parse_arguments(argv, no_defaults=False): parser.add_argument('--shuffleenemies', default=defval('none'), choices=['none', 'shuffled', 'chaos']) parser.add_argument('--enemy_health', default=defval('default'), choices=['default', 'easy', 'normal', 'hard', 'expert']) parser.add_argument('--enemy_damage', default=defval('default'), choices=['default', 'shuffled', 'chaos']) - parser.add_argument('--shufflepalette', default=defval(False), action='store_true') parser.add_argument('--shufflepots', default=defval(False), action='store_true') parser.add_argument('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4)) parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255)) @@ -286,8 +287,9 @@ def parse_arguments(argv, no_defaults=False): for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'shuffle', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', - 'retro', 'accessibility', 'hints', 'shufflepalette', 'shufflepots', 'beemizer', - 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage']: + 'retro', 'accessibility', 'hints', 'shufflepots', 'beemizer', + 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', + 'ow_palettes', 'uw_palettes']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) diff --git a/Gui.py b/Gui.py index 29b15442..cac05534 100755 --- a/Gui.py +++ b/Gui.py @@ -60,8 +60,6 @@ def guiMain(args=None): createSpoilerCheckbutton = Checkbutton(checkBoxFrame, text="Create Spoiler Log", variable=createSpoilerVar) suppressRomVar = IntVar() suppressRomCheckbutton = Checkbutton(checkBoxFrame, text="Do not create patched Rom", variable=suppressRomVar) - quickSwapVar = IntVar() - quickSwapCheckbutton = Checkbutton(checkBoxFrame, text="Enabled L/R Item quickswapping", variable=quickSwapVar) openpyramidVar = IntVar() openpyramidCheckbutton = Checkbutton(checkBoxFrame, text="Pre-open Pyramid Hole", variable=openpyramidVar) mcsbshuffleFrame = Frame(checkBoxFrame) @@ -76,8 +74,6 @@ def guiMain(args=None): bigkeyshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="BigKeys", variable=bigkeyshuffleVar) retroVar = IntVar() retroCheckbutton = Checkbutton(checkBoxFrame, text="Retro mode (universal keys)", variable=retroVar) - disableMusicVar = IntVar() - disableMusicCheckbutton = Checkbutton(checkBoxFrame, text="Disable game music", variable=disableMusicVar) shuffleGanonVar = IntVar() shuffleGanonVar.set(1) #set default shuffleGanonCheckbutton = Checkbutton(checkBoxFrame, text="Include Ganon's Tower and Pyramid Hole in shuffle pool", variable=shuffleGanonVar) @@ -89,7 +85,6 @@ def guiMain(args=None): createSpoilerCheckbutton.pack(expand=True, anchor=W) suppressRomCheckbutton.pack(expand=True, anchor=W) - quickSwapCheckbutton.pack(expand=True, anchor=W) openpyramidCheckbutton.pack(expand=True, anchor=W) mcsbshuffleFrame.pack(expand=True, anchor=W) mcsbLabel.grid(row=0, column=0) @@ -98,57 +93,23 @@ def guiMain(args=None): keyshuffleCheckbutton.grid(row=0, column=3) bigkeyshuffleCheckbutton.grid(row=0, column=4) retroCheckbutton.pack(expand=True, anchor=W) - disableMusicCheckbutton.pack(expand=True, anchor=W) shuffleGanonCheckbutton.pack(expand=True, anchor=W) hintsCheckbutton.pack(expand=True, anchor=W) customCheckbutton.pack(expand=True, anchor=W) - fileDialogFrame = Frame(rightHalfFrame) + romOptionsFrame = LabelFrame(rightHalfFrame, text="Rom options") + romOptionsFrame.columnconfigure(0, weight=1) + romOptionsFrame.columnconfigure(1, weight=1) + for i in range(5): + romOptionsFrame.rowconfigure(i, weight=1) - heartbeepFrame = Frame(fileDialogFrame) - heartbeepVar = StringVar() - heartbeepVar.set('normal') - heartbeepOptionMenu = OptionMenu(heartbeepFrame, heartbeepVar, 'double', 'normal', 'half', 'quarter', 'off') - heartbeepOptionMenu.pack(side=RIGHT) - heartbeepLabel = Label(heartbeepFrame, text='Heartbeep sound rate') - heartbeepLabel.pack(side=LEFT, padx=(0,52)) + disableMusicVar = IntVar() + disableMusicCheckbutton = Checkbutton(romOptionsFrame, text="Disable music", variable=disableMusicVar) + disableMusicCheckbutton.grid(row=0, column=0, sticky=E) - heartcolorFrame = Frame(fileDialogFrame) - heartcolorVar = StringVar() - heartcolorVar.set('red') - heartcolorOptionMenu = OptionMenu(heartcolorFrame, heartcolorVar, 'red', 'blue', 'green', 'yellow', 'random') - heartcolorOptionMenu.pack(side=RIGHT) - heartcolorLabel = Label(heartcolorFrame, text='Heart color') - heartcolorLabel.pack(side=LEFT, padx=(0,127)) - - fastMenuFrame = Frame(fileDialogFrame) - fastMenuVar = StringVar() - fastMenuVar.set('normal') - fastMenuOptionMenu = OptionMenu(fastMenuFrame, fastMenuVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half') - fastMenuOptionMenu.pack(side=RIGHT) - fastMenuLabel = Label(fastMenuFrame, text='Menu speed') - fastMenuLabel.pack(side=LEFT, padx=(0,100)) - - heartbeepFrame.pack(expand=True, anchor=E) - heartcolorFrame.pack(expand=True, anchor=E) - fastMenuFrame.pack(expand=True, anchor=E) - - romDialogFrame = Frame(fileDialogFrame) - baseRomLabel = Label(romDialogFrame, text='Base Rom') - romVar = StringVar(value="Zelda no Densetsu - Kamigami no Triforce (Japan).sfc") - romEntry = Entry(romDialogFrame, textvariable=romVar) - - def RomSelect(): - rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")]) - romVar.set(rom) - romSelectButton = Button(romDialogFrame, text='Select Rom', command=RomSelect) - - baseRomLabel.pack(side=LEFT) - romEntry.pack(side=LEFT) - romSelectButton.pack(side=LEFT) - - spriteDialogFrame = Frame(fileDialogFrame) - baseSpriteLabel = Label(spriteDialogFrame, text='Link Sprite:') + spriteDialogFrame = Frame(romOptionsFrame) + spriteDialogFrame.grid(row=0, column=1) + baseSpriteLabel = Label(spriteDialogFrame, text='Sprite:') spriteNameVar = StringVar() sprite = None @@ -168,17 +129,79 @@ def guiMain(args=None): def SpriteSelect(): SpriteSelector(mainWindow, set_sprite) - spriteSelectButton = Button(spriteDialogFrame, text='Open Sprite Picker', command=SpriteSelect) + spriteSelectButton = Button(spriteDialogFrame, text='...', command=SpriteSelect) baseSpriteLabel.pack(side=LEFT) spriteEntry.pack(side=LEFT) spriteSelectButton.pack(side=LEFT) - romDialogFrame.pack() - spriteDialogFrame.pack() + quickSwapVar = IntVar() + quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=quickSwapVar) + quickSwapCheckbutton.grid(row=1, column=0, sticky=E) - checkBoxFrame.pack() - fileDialogFrame.pack() + fastMenuFrame = Frame(romOptionsFrame) + fastMenuFrame.grid(row=1, column=1, sticky=E) + fastMenuLabel = Label(fastMenuFrame, text='Menu speed') + fastMenuLabel.pack(side=LEFT) + fastMenuVar = StringVar() + fastMenuVar.set('normal') + fastMenuOptionMenu = OptionMenu(fastMenuFrame, fastMenuVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half') + fastMenuOptionMenu.pack(side=LEFT) + + heartcolorFrame = Frame(romOptionsFrame) + heartcolorFrame.grid(row=2, column=0, sticky=E) + heartcolorLabel = Label(heartcolorFrame, text='Heart color') + heartcolorLabel.pack(side=LEFT) + heartcolorVar = StringVar() + heartcolorVar.set('red') + heartcolorOptionMenu = OptionMenu(heartcolorFrame, heartcolorVar, 'red', 'blue', 'green', 'yellow', 'random') + heartcolorOptionMenu.pack(side=LEFT) + + heartbeepFrame = Frame(romOptionsFrame) + heartbeepFrame.grid(row=2, column=1, sticky=E) + heartbeepLabel = Label(heartbeepFrame, text='Heartbeep') + heartbeepLabel.pack(side=LEFT) + heartbeepVar = StringVar() + heartbeepVar.set('normal') + heartbeepOptionMenu = OptionMenu(heartbeepFrame, heartbeepVar, 'double', 'normal', 'half', 'quarter', 'off') + heartbeepOptionMenu.pack(side=LEFT) + + owPalettesFrame = Frame(romOptionsFrame) + owPalettesFrame.grid(row=3, column=0, sticky=E) + owPalettesLabel = Label(owPalettesFrame, text='Overworld palettes') + owPalettesLabel.pack(side=LEFT) + owPalettesVar = StringVar() + owPalettesVar.set('default') + owPalettesOptionMenu = OptionMenu(owPalettesFrame, owPalettesVar, 'default', 'random', 'blackout') + owPalettesOptionMenu.pack(side=LEFT) + + uwPalettesFrame = Frame(romOptionsFrame) + uwPalettesFrame.grid(row=3, column=1, sticky=E) + uwPalettesLabel = Label(uwPalettesFrame, text='Dungeon palettes') + uwPalettesLabel.pack(side=LEFT) + uwPalettesVar = StringVar() + uwPalettesVar.set('default') + uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, uwPalettesVar, 'default', 'random', 'blackout') + uwPalettesOptionMenu.pack(side=LEFT) + + romDialogFrame = Frame(romOptionsFrame) + romDialogFrame.grid(row=4, column=0, columnspan=2, sticky=W+E) + + baseRomLabel = Label(romDialogFrame, text='Base Rom: ') + romVar = StringVar(value="Zelda no Densetsu - Kamigami no Triforce (Japan).sfc") + romEntry = Entry(romDialogFrame, textvariable=romVar) + + def RomSelect(): + rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")]) + romVar.set(rom) + romSelectButton = Button(romDialogFrame, text='Select Rom', command=RomSelect) + + baseRomLabel.pack(side=LEFT) + romEntry.pack(side=LEFT, expand=True, fill=X) + romSelectButton.pack(side=LEFT) + + checkBoxFrame.pack(side=TOP, anchor=W, padx=5, pady=10) + romOptionsFrame.pack(expand=True, fill=BOTH, padx=3) drowDownFrame = Frame(topFrame) @@ -300,18 +323,19 @@ def guiMain(args=None): algorithmFrame.pack(expand=True, anchor=E) shuffleFrame.pack(expand=True, anchor=E) - enemizerFrame = LabelFrame(randomizerWindow, text="Enemizer", padx=5, pady=5) + enemizerFrame = LabelFrame(randomizerWindow, text="Enemizer", padx=5, pady=2) enemizerFrame.columnconfigure(0, weight=1) enemizerFrame.columnconfigure(1, weight=1) enemizerFrame.columnconfigure(2, weight=1) + enemizerFrame.columnconfigure(3, weight=1) enemizerPathFrame = Frame(enemizerFrame) - enemizerPathFrame.grid(row=0, column=0, columnspan=3, sticky=W) + enemizerPathFrame.grid(row=0, column=0, columnspan=3, sticky=W+E, padx=3) enemizerCLIlabel = Label(enemizerPathFrame, text="EnemizerCLI path: ") enemizerCLIlabel.pack(side=LEFT) enemizerCLIpathVar = StringVar(value="EnemizerCLI/EnemizerCLI.Core") - enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=enemizerCLIpathVar, width=80) - enemizerCLIpathEntry.pack(side=LEFT) + enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=enemizerCLIpathVar) + enemizerCLIpathEntry.pack(side=LEFT, expand=True, fill=X) def EnemizerSelectPath(): path = filedialog.askopenfilename(filetypes=[("EnemizerCLI executable", "*EnemizerCLI*")]) if path: @@ -319,18 +343,21 @@ def guiMain(args=None): enemizerCLIbrowseButton = Button(enemizerPathFrame, text='...', command=EnemizerSelectPath) enemizerCLIbrowseButton.pack(side=LEFT) - enemyShuffleVar = IntVar() - enemyShuffleButton = Checkbutton(enemizerFrame, text="Enemy shuffle", variable=enemyShuffleVar) - enemyShuffleButton.grid(row=1, column=0) - paletteShuffleVar = IntVar() - paletteShuffleButton = Checkbutton(enemizerFrame, text="Palette shuffle", variable=paletteShuffleVar) - paletteShuffleButton.grid(row=1, column=1) potShuffleVar = IntVar() potShuffleButton = Checkbutton(enemizerFrame, text="Pot shuffle", variable=potShuffleVar) - potShuffleButton.grid(row=1, column=2) + potShuffleButton.grid(row=0, column=3) + + enemizerEnemyFrame = Frame(enemizerFrame) + enemizerEnemyFrame.grid(row=1, column=0, pady=5) + enemizerEnemyLabel = Label(enemizerEnemyFrame, text='Enemy shuffle') + enemizerEnemyLabel.pack(side=LEFT) + enemyShuffleVar = StringVar() + enemyShuffleVar.set('none') + enemizerEnemyOption = OptionMenu(enemizerEnemyFrame, enemyShuffleVar, 'none', 'shuffled', 'chaos') + enemizerEnemyOption.pack(side=LEFT) enemizerBossFrame = Frame(enemizerFrame) - enemizerBossFrame.grid(row=2, column=0) + enemizerBossFrame.grid(row=1, column=1) enemizerBossLabel = Label(enemizerBossFrame, text='Boss shuffle') enemizerBossLabel.pack(side=LEFT) enemizerBossVar = StringVar() @@ -339,7 +366,7 @@ def guiMain(args=None): enemizerBossOption.pack(side=LEFT) enemizerDamageFrame = Frame(enemizerFrame) - enemizerDamageFrame.grid(row=2, column=1) + enemizerDamageFrame.grid(row=1, column=2) enemizerDamageLabel = Label(enemizerDamageFrame, text='Enemy damage') enemizerDamageLabel.pack(side=LEFT) enemizerDamageVar = StringVar() @@ -348,7 +375,7 @@ def guiMain(args=None): enemizerDamageOption.pack(side=LEFT) enemizerHealthFrame = Frame(enemizerFrame) - enemizerHealthFrame.grid(row=2, column=2) + enemizerHealthFrame.grid(row=1, column=3) enemizerHealthLabel = Label(enemizerHealthFrame, text='Enemy health') enemizerHealthLabel.pack(side=LEFT) enemizerHealthVar = StringVar() @@ -403,14 +430,15 @@ def guiMain(args=None): guiargs.retro = bool(retroVar.get()) guiargs.quickswap = bool(quickSwapVar.get()) guiargs.disablemusic = bool(disableMusicVar.get()) + guiargs.ow_palettes = owPalettesVar.get() + guiargs.uw_palettes = uwPalettesVar.get() guiargs.shuffleganon = bool(shuffleGanonVar.get()) guiargs.hints = bool(hintsVar.get()) guiargs.enemizercli = enemizerCLIpathVar.get() guiargs.shufflebosses = enemizerBossVar.get() - guiargs.shuffleenemies = 'chaos' if bool(enemyShuffleVar.get()) else 'none' + guiargs.shuffleenemies = enemyShuffleVar.get() guiargs.enemy_health = enemizerHealthVar.get() guiargs.enemy_damage = enemizerDamageVar.get() - guiargs.shufflepalette = bool(paletteShuffleVar.get()) guiargs.shufflepots = bool(potShuffleVar.get()) guiargs.custom = bool(customVar.get()) guiargs.customitemarray = [int(bowVar.get()), int(silverarrowVar.get()), int(boomerangVar.get()), int(magicboomerangVar.get()), int(hookshotVar.get()), int(mushroomVar.get()), int(magicpowderVar.get()), int(firerodVar.get()), @@ -534,6 +562,18 @@ def guiMain(args=None): fastMenuLabel2 = Label(fastMenuFrame2, text='Menu speed') fastMenuLabel2.pack(side=LEFT) + owPalettesFrame2 = Frame(drowDownFrame2) + owPalettesOptionMenu2 = OptionMenu(owPalettesFrame2, owPalettesVar, 'default', 'random', 'blackout') + owPalettesOptionMenu2.pack(side=RIGHT) + owPalettesLabel2 = Label(owPalettesFrame2, text='Overworld palettes') + owPalettesLabel2.pack(side=LEFT) + + uwPalettesFrame2 = Frame(drowDownFrame2) + uwPalettesOptionMenu2 = OptionMenu(uwPalettesFrame2, uwPalettesVar, 'default', 'random', 'blackout') + uwPalettesOptionMenu2.pack(side=RIGHT) + uwPalettesLabel2 = Label(uwPalettesFrame2, text='Dungeon palettes') + uwPalettesLabel2.pack(side=LEFT) + namesFrame2 = Frame(drowDownFrame2) namesLabel2 = Label(namesFrame2, text='Player names') namesVar2 = StringVar() @@ -545,6 +585,8 @@ def guiMain(args=None): heartbeepFrame2.pack(expand=True, anchor=E) heartcolorFrame2.pack(expand=True, anchor=E) fastMenuFrame2.pack(expand=True, anchor=E) + owPalettesFrame2.pack(expand=True, anchor=E) + uwPalettesFrame2.pack(expand=True, anchor=E) namesFrame2.pack(expand=True, anchor=E) bottomFrame2 = Frame(topFrame2) @@ -554,9 +596,12 @@ def guiMain(args=None): guiargs.heartbeep = heartbeepVar.get() guiargs.heartcolor = heartcolorVar.get() guiargs.fastmenu = fastMenuVar.get() + guiargs.ow_palettes = owPalettesVar.get() + guiargs.uw_palettes = uwPalettesVar.get() guiargs.quickswap = bool(quickSwapVar.get()) guiargs.disablemusic = bool(disableMusicVar.get()) guiargs.rom = romVar2.get() + guiargs.baserom = romVar.get() guiargs.sprite = sprite guiargs.names = namesEntry2.get() try: diff --git a/Main.py b/Main.py index 8e7ccfb4..f20940fb 100644 --- a/Main.py +++ b/Main.py @@ -152,7 +152,7 @@ def main(args, seed=None): for player in range(1, world.players + 1): use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] != 'none' or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default' - or args.shufflepalette[player] or args.shufflepots[player]) + or args.shufflepots[player]) local_rom = None if args.jsonout: @@ -168,7 +168,7 @@ def main(args, seed=None): enemizer_patch = [] if use_enemizer and (args.enemizercli or not args.jsonout): - enemizer_patch = get_enemizer_patch(world, player, rom, args.rom, args.enemizercli, args.shufflepalette[player], args.shufflepots[player]) + enemizer_patch = get_enemizer_patch(world, player, rom, args.rom, args.enemizercli, args.shufflepots[player]) if args.jsonout: jsonout[f'patch{player}'] = rom.patches @@ -185,7 +185,7 @@ def main(args, seed=None): for addr, values in get_race_rom_patches(rom).items(): rom.write_bytes(int(addr), values) - apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite, player_names) + apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite, args.ow_palettes[player], args.uw_palettes[player], player_names) mcsb_name = '' if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]): diff --git a/Plando.py b/Plando.py index 3011a023..cf1fbf51 100755 --- a/Plando.py +++ b/Plando.py @@ -74,9 +74,9 @@ def main(args): sprite = None rom = LocalRom(args.rom) - patch_rom(world, 1, rom) + patch_rom(world, 1, rom, False) - apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite) + apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite, args.ow_palettes, args.uw_palettes) for textname, texttype, text in text_patches: if texttype == 'text': @@ -213,6 +213,8 @@ def start(): help='Select the rate at which the heart beep sound is played at low health.') parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow'], help='Select the color of Link\'s heart meter. (default: %(default)s)') + parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout']) + parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout']) parser.add_argument('--sprite', help='Path to a sprite sheet to use for Link. Needs to be in binary format and have a length of 0x7000 (28672) bytes.') parser.add_argument('--plando', help='Filled out template to use for setting up the rom.') args = parser.parse_args() diff --git a/Rom.py b/Rom.py index adb462b8..8af1a23e 100644 --- a/Rom.py +++ b/Rom.py @@ -27,6 +27,7 @@ class JsonRom(object): def __init__(self): self.name = None + self.orig_buffer = None self.patches = {} self.addresses = [] @@ -72,10 +73,12 @@ class LocalRom(object): def __init__(self, file, patch=True): self.name = None + self.orig_buffer = None with open(file, 'rb') as stream: self.buffer = read_rom(stream) if patch: self.patch_base_rom() + self.orig_buffer = self.buffer.copy() def write_byte(self, address, value): self.buffer[address] = value @@ -161,7 +164,7 @@ def read_rom(stream): buffer = buffer[0x200:] return buffer -def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepalette, shufflepots): +def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepots): baserom_path = os.path.abspath(baserom_path) basepatch_path = os.path.abspath(local_path('data/base2current.json')) randopatch_path = os.path.abspath(output_path('enemizer_randopatch.json')) @@ -197,10 +200,10 @@ def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepal 'RandomizeBossDamageMinAmount': 0, 'RandomizeBossDamageMaxAmount': 200, 'RandomizeBossBehavior': False, - 'RandomizeDungeonPalettes': shufflepalette, + 'RandomizeDungeonPalettes': False, 'SetBlackoutMode': False, - 'RandomizeOverworldPalettes': shufflepalette, - 'RandomizeSpritePalettes': shufflepalette, + 'RandomizeOverworldPalettes': False, + 'RandomizeSpritePalettes': False, 'SetAdvancedSpritePalettes': False, 'PukeMode': False, 'NegativeMode': False, @@ -1278,7 +1281,7 @@ def hud_format_text(text): return output[:32] -def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite, names = None): +def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite, ow_palettes, uw_palettes, names = None): # enable instant item menu if fastmenu == 'instant': @@ -1439,6 +1442,18 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr if sprite is not None: write_sprite(rom, sprite) + default_ow_palettes(rom) + if ow_palettes == 'random': + randomize_ow_palettes(rom) + elif ow_palettes == 'blackout': + blackout_ow_palettes(rom) + + default_uw_palettes(rom) + if uw_palettes == 'random': + randomize_uw_palettes(rom) + elif uw_palettes == 'blackout': + blackout_uw_palettes(rom) + # set player names for player, name in names.items(): if 0 < player <= 64: @@ -1455,6 +1470,130 @@ def write_sprite(rom, sprite): rom.write_bytes(0xDD308, sprite.palette) rom.write_bytes(0xDEDF5, sprite.glove_palette) +def set_color(rom, address, color, shade): + r = round(min(color[0], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF) + g = round(min(color[1], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF) + b = round(min(color[2], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF) + + rom.write_bytes(address, ((b << 10) | (g << 5) | (r << 0)).to_bytes(2, byteorder='little', signed=False)) + +def default_ow_palettes(rom): + if not rom.orig_buffer: + return + rom.write_bytes(0xDE604, rom.orig_buffer[0xDE604:0xDEBB4]) + + for address in [0x067FB4, 0x067F94, 0x067FC6, 0x067FE6, 0x067FE1, 0x05FEA9, 0x05FEB3]: + rom.write_bytes(address, rom.orig_buffer[address:address+2]) + +def randomize_ow_palettes(rom): + grass, grass2, grass3, dirt, dirt2, water, clouds, dwdirt,\ + dwgrass, dwwater, dwdmdirt, dwdmgrass, dwdmclouds1, dwdmclouds2 = [[random.randint(60, 215) for _ in range(3)] for _ in range(14)] + dwtree = [c + random.randint(-20, 10) for c in dwgrass] + treeleaf = [c + random.randint(-20, 10) for c in grass] + + patches = {0x067FB4: (grass, 0), 0x067F94: (grass, 0), 0x067FC6: (grass, 0), 0x067FE6: (grass, 0), 0x067FE1: (grass, 3), 0x05FEA9: (grass, 0), 0x05FEB3: (dwgrass, 1), + 0x0DD4AC: (grass, 2), 0x0DE6DE: (grass2, 2), 0x0DE6E0: (grass2, 1), 0x0DD4AE: (grass2, 1), 0x0DE9FA: (grass2, 1), 0x0DEA0E: (grass2, 1), 0x0DE9FE: (grass2, 0), + 0x0DD3D2: (grass2, 2), 0x0DE88C: (grass2, 2), 0x0DE8A8: (grass2, 2), 0x0DE9F8: (grass2, 2), 0x0DEA4E: (grass2, 2), 0x0DEAF6: (grass2, 2), 0x0DEB2E: (grass2, 2), 0x0DEB4A: (grass2, 2), + 0x0DE892: (grass, 1), 0x0DE886: (grass, 0), 0x0DE6D2: (grass, 0), 0x0DE6FA: (grass, 3), 0x0DE6FC: (grass, 0), 0x0DE6FE: (grass, 0), 0x0DE70A: (grass, 0), 0x0DE708: (grass, 2), 0x0DE70C: (grass, 1), + 0x0DE6D4: (dirt, 2), 0x0DE6CA: (dirt, 5), 0x0DE6CC: (dirt, 4), 0x0DE6CE: (dirt, 3), 0x0DE6E2: (dirt, 2), 0x0DE6D8: (dirt, 5), 0x0DE6DA: (dirt, 4), 0x0DE6DC: (dirt, 2), + 0x0DE6F0: (dirt, 2), 0x0DE6E6: (dirt, 5), 0x0DE6E8: (dirt, 4), 0x0DE6EA: (dirt, 2), 0x0DE6EC: (dirt, 4), 0x0DE6EE: (dirt, 2), + 0x0DE91E: (grass, 0), + 0x0DE920: (dirt, 2), 0x0DE916: (dirt, 3), 0x0DE934: (dirt, 3), + 0x0DE92C: (grass, 0), 0x0DE93A: (grass, 0), 0x0DE91C: (grass, 1), 0x0DE92A: (grass, 1), 0x0DEA1C: (grass, 0), 0x0DEA2A: (grass, 0), 0x0DEA30: (grass, 0), + 0x0DEA2E: (dirt, 5), + 0x0DE884: (grass, 3), 0x0DE8AE: (grass, 3), 0x0DE8BE: (grass, 3), 0x0DE8E4: (grass, 3), 0x0DE938: (grass, 3), 0x0DE9C4: (grass, 3), 0x0DE6D0: (grass, 4), + 0x0DE890: (treeleaf, 1), 0x0DE894: (treeleaf, 0), + 0x0DE924: (water, 3), 0x0DE668: (water, 3), 0x0DE66A: (water, 2), 0x0DE670: (water, 1), 0x0DE918: (water, 1), 0x0DE66C: (water, 0), 0x0DE91A: (water, 0), 0x0DE92E: (water, 1), 0x0DEA1A: (water, 1), 0x0DEA16: (water, 3), 0x0DEA10: (water, 4), + 0x0DE66E: (dirt, 3), 0x0DE672: (dirt, 2), 0x0DE932: (dirt, 4), 0x0DE936: (dirt, 2), 0x0DE93C: (dirt, 1), + 0x0DE756: (dirt2, 4), 0x0DE764: (dirt2, 4), 0x0DE772: (dirt2, 4), 0x0DE994: (dirt2, 4), 0x0DE9A2: (dirt2, 4), 0x0DE758: (dirt2, 3), 0x0DE766: (dirt2, 3), 0x0DE774: (dirt2, 3), + 0x0DE996: (dirt2, 3), 0x0DE9A4: (dirt2, 3), 0x0DE75A: (dirt2, 2), 0x0DE768: (dirt2, 2), 0x0DE776: (dirt2, 2), 0x0DE778: (dirt2, 2), 0x0DE998: (dirt2, 2), 0x0DE9A6: (dirt2, 2), + 0x0DE9AC: (dirt2, 1), 0x0DE99E: (dirt2, 1), 0x0DE760: (dirt2, 1), 0x0DE77A: (dirt2, 1), 0x0DE77C: (dirt2, 1), 0x0DE798: (dirt2, 1), 0x0DE980: (dirt2, 1), + 0x0DE75C: (grass3, 2), 0x0DE786: (grass3, 2), 0x0DE794: (grass3, 2), 0x0DE99A: (grass3, 2), 0x0DE75E: (grass3, 1), 0x0DE788: (grass3, 1), 0x0DE796: (grass3, 1), 0x0DE99C: (grass3, 1), + 0x0DE76A: (clouds, 2), 0x0DE9A8: (clouds, 2), 0x0DE76E: (clouds, 0), 0x0DE9AA: (clouds, 0), 0x0DE8DA: (clouds, 0), 0x0DE8D8: (clouds, 0), 0x0DE8D0: (clouds, 0), 0x0DE98C: (clouds, 2), 0x0DE990: (clouds, 0), + 0x0DEB34: (dwtree, 4), 0x0DEB30: (dwtree, 3), 0x0DEB32: (dwtree, 1), + 0x0DE710: (dwdirt, 5), 0x0DE71E: (dwdirt, 5), 0x0DE72C: (dwdirt, 5), 0x0DEAD6: (dwdirt, 5), 0x0DE712: (dwdirt, 4), 0x0DE720: (dwdirt, 4), 0x0DE72E: (dwdirt, 4), 0x0DE660: (dwdirt, 4), + 0x0DEAD8: (dwdirt, 4), 0x0DEADA: (dwdirt, 3), 0x0DE714: (dwdirt, 3), 0x0DE722: (dwdirt, 3), 0x0DE730: (dwdirt, 3), 0x0DE732: (dwdirt, 3), 0x0DE734: (dwdirt, 2), 0x0DE736: (dwdirt, 2), + 0x0DE728: (dwdirt, 2), 0x0DE71A: (dwdirt, 2), 0x0DE664: (dwdirt, 2), 0x0DEAE0: (dwdirt, 2), + 0x0DE716: (dwgrass, 3), 0x0DE740: (dwgrass, 3), 0x0DE74E: (dwgrass, 3), 0x0DEAC0: (dwgrass, 3), 0x0DEACE: (dwgrass, 3), 0x0DEADC: (dwgrass, 3), 0x0DEB24: (dwgrass, 3), 0x0DE752: (dwgrass, 2), + 0x0DE718: (dwgrass, 1), 0x0DE742: (dwgrass, 1), 0x0DE750: (dwgrass, 1), 0x0DEB26: (dwgrass, 1), 0x0DEAC2: (dwgrass, 1), 0x0DEAD0: (dwgrass, 1), 0x0DEADE: (dwgrass, 1), + 0x0DE65A: (dwwater, 5), 0x0DE65C: (dwwater, 3), 0x0DEAC8: (dwwater, 3), 0x0DEAD2: (dwwater, 2), 0x0DEABC: (dwwater, 2), 0x0DE662: (dwwater, 2), 0x0DE65E: (dwwater, 1), 0x0DEABE: (dwwater, 1), 0x0DEA98: (dwwater, 2), + 0x0DE79A: (dwdmdirt, 6), 0x0DE7A8: (dwdmdirt, 6), 0x0DE7B6: (dwdmdirt, 6), 0x0DEB60: (dwdmdirt, 6), 0x0DEB6E: (dwdmdirt, 6), 0x0DE93E: (dwdmdirt, 6), 0x0DE94C: (dwdmdirt, 6), 0x0DEBA6: (dwdmdirt, 6), + 0x0DE79C: (dwdmdirt, 4), 0x0DE7AA: (dwdmdirt, 4), 0x0DE7B8: (dwdmdirt, 4), 0x0DEB70: (dwdmdirt, 4), 0x0DEBA8: (dwdmdirt, 4), 0x0DEB72: (dwdmdirt, 3), 0x0DEB74: (dwdmdirt, 3), 0x0DE79E: (dwdmdirt, 3), 0x0DE7AC: (dwdmdirt, 3), 0x0DEBAA: (dwdmdirt, 3), 0x0DE7A0: (dwdmdirt, 3), + 0x0DE7BC: (dwdmgrass, 3), + 0x0DEBAC: (dwdmdirt, 2), 0x0DE7AE: (dwdmdirt, 2), 0x0DE7C2: (dwdmdirt, 2), 0x0DE7A6: (dwdmdirt, 2), 0x0DEB7A: (dwdmdirt, 2), 0x0DEB6C: (dwdmdirt, 2), 0x0DE7C0: (dwdmdirt, 2), + 0x0DE7A2: (dwdmgrass, 3), 0x0DE7BE: (dwdmgrass, 3), 0x0DE7CC: (dwdmgrass, 3), 0x0DE7DA: (dwdmgrass, 3), 0x0DEB6A: (dwdmgrass, 3), 0x0DE948: (dwdmgrass, 3), 0x0DE956: (dwdmgrass, 3), 0x0DE964: (dwdmgrass, 3), 0x0DE7CE: (dwdmgrass, 1), 0x0DE7A4: (dwdmgrass, 1), 0x0DEBA2: (dwdmgrass, 1), 0x0DEBB0: (dwdmgrass, 1), + 0x0DE644: (dwdmclouds1, 2), 0x0DEB84: (dwdmclouds1, 2), 0x0DE648: (dwdmclouds1, 1), 0x0DEB88: (dwdmclouds1, 1), + 0x0DEBAE: (dwdmclouds2, 2), 0x0DE7B0: (dwdmclouds2, 2), 0x0DE7B4: (dwdmclouds2, 0), 0x0DEB78: (dwdmclouds2, 0), 0x0DEBB2: (dwdmclouds2, 0) + } + for address, (color, shade) in patches.items(): + set_color(rom, address, color, shade) + +def blackout_ow_palettes(rom): + rom.write_bytes(0xDE604, [0] * 0xC4) + for i in range(0xDE6C8, 0xDE86C, 70): + rom.write_bytes(i, [0] * 64) + rom.write_bytes(i+66, [0] * 4) + rom.write_bytes(0xDE86C, [0] * 0x348) + + for address in [0x067FB4, 0x067F94, 0x067FC6, 0x067FE6, 0x067FE1, 0x05FEA9, 0x05FEB3]: + rom.write_bytes(address, [0,0]) + +def default_uw_palettes(rom): + if not rom.orig_buffer: + return + rom.write_bytes(0xDD734, rom.orig_buffer[0xDD734:0xDE544]) + +def randomize_uw_palettes(rom): + for dungeon in range(20): + wall, pot, chest, floor1, floor2, floor3 = [[random.randint(60, 240) for _ in range(3)] for _ in range(6)] + + for i in range(5): + shade = 10 - (i * 2) + set_color(rom, 0x0DD734 + (0xB4 * dungeon) + (i * 2), wall, shade) + set_color(rom, 0x0DD770 + (0xB4 * dungeon) + (i * 2), wall, shade) + set_color(rom, 0x0DD744 + (0xB4 * dungeon) + (i * 2), wall, shade) + if dungeon == 0: + set_color(rom, 0x0DD7CA + (0xB4 * dungeon) + (i * 2), wall, shade) + + if dungeon == 2: + set_color(rom, 0x0DD74E + (0xB4 * dungeon), wall, 3) + set_color(rom, 0x0DD750 + (0xB4 * dungeon), wall, 5) + set_color(rom, 0x0DD73E + (0xB4 * dungeon), wall, 3) + set_color(rom, 0x0DD740 + (0xB4 * dungeon), wall, 5) + + set_color(rom, 0x0DD7E4 + (0xB4 * dungeon), wall, 4) + set_color(rom, 0x0DD7E6 + (0xB4 * dungeon), wall, 2) + + set_color(rom, 0xDD7DA + (0xB4 * dungeon), wall, 10) + set_color(rom, 0xDD7DC + (0xB4 * dungeon), wall, 8) + + set_color(rom, 0x0DD75A + (0xB4 * dungeon), pot, 7) + set_color(rom, 0x0DD75C + (0xB4 * dungeon), pot, 1) + set_color(rom, 0x0DD75E + (0xB4 * dungeon), pot, 3) + + set_color(rom, 0x0DD76A + (0xB4 * dungeon), wall, 7) + set_color(rom, 0x0DD76C + (0xB4 * dungeon), wall, 2) + set_color(rom, 0x0DD76E + (0xB4 * dungeon), wall, 4) + + set_color(rom, 0x0DD7AE + (0xB4 * dungeon), chest, 2) + set_color(rom, 0x0DD7B0 + (0xB4 * dungeon), chest, 0) + + for i in range(3): + shade = 6 - (i * 2) + set_color(rom, 0x0DD764 + (0xB4 * dungeon) + (i * 2), floor1, shade) + set_color(rom, 0x0DD782 + (0xB4 * dungeon) + (i * 2), floor1, shade + 3) + + set_color(rom, 0x0DD7A0 + (0xB4 * dungeon) + (i * 2), floor2, shade) + set_color(rom, 0x0DD7BE + (0xB4 * dungeon) + (i * 2), floor2, shade + 3) + + set_color(rom, 0x0DD7E2 + (0xB4 * dungeon), floor3, 3) + set_color(rom, 0x0DD796 + (0xB4 * dungeon), floor3, 4) + +def blackout_uw_palettes(rom): + for i in range(0xDD734, 0xDE544, 180): + rom.write_bytes(i, [0] * 38) + rom.write_bytes(i+44, [0] * 76) + rom.write_bytes(i+136, [0] * 44) def write_string_to_rom(rom, target, string): address, maxbytes = text_addresses[target] From feb925d2b1f953e74c435d6fb08acd186002503d Mon Sep 17 00:00:00 2001 From: Bonta-kun <40473493+Bonta0@users.noreply.github.com> Date: Wed, 8 Jan 2020 08:17:25 +0100 Subject: [PATCH 11/20] Rom: cleaner disable_music patches to match what the website does --- Rom.py | 114 ++------------------------------------------------------- 1 file changed, 4 insertions(+), 110 deletions(-) diff --git a/Rom.py b/Rom.py index 8af1a23e..cdb1a641 100644 --- a/Rom.py +++ b/Rom.py @@ -1307,119 +1307,13 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr rom.write_byte(0x18004B, 0x01 if quickswap else 0x00) - music_volumes = [ - (0x00, [0xD373B, 0xD375B, 0xD90F8]), - (0x14, [0xDA710, 0xDA7A4, 0xDA7BB, 0xDA7D2]), - (0x3C, [0xD5954, 0xD653B, 0xDA736, 0xDA752, 0xDA772, 0xDA792]), - (0x50, [0xD5B47, 0xD5B5E]), - (0x54, [0xD4306]), - (0x64, - [0xD6878, 0xD6883, 0xD6E48, 0xD6E76, 0xD6EFB, 0xD6F2D, 0xDA211, 0xDA35B, 0xDA37B, 0xDA38E, 0xDA39F, 0xDA5C3, - 0xDA691, 0xDA6A8, 0xDA6DF]), - (0x78, - [0xD2349, 0xD3F45, 0xD42EB, 0xD48B9, 0xD48FF, 0xD543F, 0xD5817, 0xD5957, 0xD5ACB, 0xD5AE8, 0xD5B4A, 0xDA5DE, - 0xDA608, 0xDA635, - 0xDA662, 0xDA71F, 0xDA7AF, 0xDA7C6, 0xDA7DD]), - (0x82, [0xD2F00, 0xDA3D5]), - (0xA0, - [0xD249C, 0xD24CD, 0xD2C09, 0xD2C53, 0xD2CAF, 0xD2CEB, 0xD2D91, 0xD2EE6, 0xD38ED, 0xD3C91, 0xD3CD3, 0xD3CE8, - 0xD3F0C, - 0xD3F82, 0xD405F, 0xD4139, 0xD4198, 0xD41D5, 0xD41F6, 0xD422B, 0xD4270, 0xD42B1, 0xD4334, 0xD4371, 0xD43A6, - 0xD43DB, - 0xD441E, 0xD4597, 0xD4B3C, 0xD4BAB, 0xD4C03, 0xD4C53, 0xD4C7F, 0xD4D9C, 0xD5424, 0xD65D2, 0xD664F, 0xD6698, - 0xD66FF, - 0xD6985, 0xD6C5C, 0xD6C6F, 0xD6C8E, 0xD6CB4, 0xD6D7D, 0xD827D, 0xD960C, 0xD9828, 0xDA233, 0xDA3A2, 0xDA49E, - 0xDA72B, - 0xDA745, 0xDA765, 0xDA785, 0xDABF6, 0xDAC0D, 0xDAEBE, 0xDAFAC]), - (0xAA, [0xD9A02, 0xD9BD6]), - (0xB4, - [0xD21CD, 0xD2279, 0xD2E66, 0xD2E70, 0xD2EAB, 0xD3B97, 0xD3BAC, 0xD3BE8, 0xD3C0D, 0xD3C39, 0xD3C68, 0xD3C9F, - 0xD3CBC, - 0xD401E, 0xD4290, 0xD443E, 0xD456F, 0xD47D3, 0xD4D43, 0xD4DCC, 0xD4EBA, 0xD4F0B, 0xD4FE5, 0xD5012, 0xD54BC, - 0xD54D5, - 0xD54F0, 0xD5509, 0xD57D8, 0xD59B9, 0xD5A2F, 0xD5AEB, 0xD5E5E, 0xD5FE9, 0xD658F, 0xD674A, 0xD6827, 0xD69D6, - 0xD69F5, - 0xD6A05, 0xD6AE9, 0xD6DCF, 0xD6E20, 0xD6ECB, 0xD71D4, 0xD71E6, 0xD7203, 0xD721E, 0xD8724, 0xD8732, 0xD9652, - 0xD9698, - 0xD9CBC, 0xD9DC0, 0xD9E49, 0xDAA68, 0xDAA77, 0xDAA88, 0xDAA99, 0xDAF04]), - (0x8c, - [0xD1D28, 0xD1D41, 0xD1D5C, 0xD1D77, 0xD1EEE, 0xD311D, 0xD31D1, 0xD4148, 0xD5543, 0xD5B6F, 0xD65B3, 0xD6760, - 0xD6B6B, - 0xD6DF6, 0xD6E0D, 0xD73A1, 0xD814C, 0xD825D, 0xD82BE, 0xD8340, 0xD8394, 0xD842C, 0xD8796, 0xD8903, 0xD892A, - 0xD91E8, - 0xD922B, 0xD92E0, 0xD937E, 0xD93C1, 0xDA958, 0xDA971, 0xDA98C, 0xDA9A7]), - (0xC8, - [0xD1D92, 0xD1DBD, 0xD1DEB, 0xD1F5D, 0xD1F9F, 0xD1FBD, 0xD1FDC, 0xD1FEA, 0xD20CA, 0xD21BB, 0xD22C9, 0xD2754, - 0xD284C, - 0xD2866, 0xD2887, 0xD28A0, 0xD28BA, 0xD28DB, 0xD28F4, 0xD293E, 0xD2BF3, 0xD2C1F, 0xD2C69, 0xD2CA1, 0xD2CC5, - 0xD2D05, - 0xD2D73, 0xD2DAF, 0xD2E3D, 0xD2F36, 0xD2F46, 0xD2F6F, 0xD2FCF, 0xD2FDF, 0xD302B, 0xD3086, 0xD3099, 0xD30A5, - 0xD30CD, - 0xD30F6, 0xD3154, 0xD3184, 0xD333A, 0xD33D9, 0xD349F, 0xD354A, 0xD35E5, 0xD3624, 0xD363C, 0xD3672, 0xD3691, - 0xD36B4, - 0xD36C6, 0xD3724, 0xD3767, 0xD38CB, 0xD3B1D, 0xD3B2F, 0xD3B55, 0xD3B70, 0xD3B81, 0xD3BBF, 0xD3F65, 0xD3FA6, - 0xD404F, - 0xD4087, 0xD417A, 0xD41A0, 0xD425C, 0xD4319, 0xD433C, 0xD43EF, 0xD440C, 0xD4452, 0xD4494, 0xD44B5, 0xD4512, - 0xD45D1, - 0xD45EF, 0xD4682, 0xD46C3, 0xD483C, 0xD4848, 0xD4855, 0xD4862, 0xD486F, 0xD487C, 0xD4A1C, 0xD4A3B, 0xD4A60, - 0xD4B27, - 0xD4C7A, 0xD4D12, 0xD4D81, 0xD4E90, 0xD4ED6, 0xD4EE2, 0xD5005, 0xD502E, 0xD503C, 0xD5081, 0xD51B1, 0xD51C7, - 0xD51CF, - 0xD51EF, 0xD520C, 0xD5214, 0xD5231, 0xD5257, 0xD526D, 0xD5275, 0xD52AF, 0xD52BD, 0xD52CD, 0xD52DB, 0xD549C, - 0xD5801, - 0xD58A4, 0xD5A68, 0xD5A7F, 0xD5C12, 0xD5D71, 0xD5E10, 0xD5E9A, 0xD5F8B, 0xD5FA4, 0xD651A, 0xD6542, 0xD65ED, - 0xD661D, - 0xD66D7, 0xD6776, 0xD68BD, 0xD68E5, 0xD6956, 0xD6973, 0xD69A8, 0xD6A51, 0xD6A86, 0xD6B96, 0xD6C3E, 0xD6D4A, - 0xD6E9C, - 0xD6F80, 0xD717E, 0xD7190, 0xD71B9, 0xD811D, 0xD8139, 0xD816B, 0xD818A, 0xD819E, 0xD81BE, 0xD829C, 0xD82E1, - 0xD8306, - 0xD830E, 0xD835E, 0xD83AB, 0xD83CA, 0xD83F0, 0xD83F8, 0xD844B, 0xD8479, 0xD849E, 0xD84CB, 0xD84EB, 0xD84F3, - 0xD854A, - 0xD8573, 0xD859D, 0xD85B4, 0xD85CE, 0xD862A, 0xD8681, 0xD87E3, 0xD87FF, 0xD887B, 0xD88C6, 0xD88E3, 0xD8944, - 0xD897B, - 0xD8C97, 0xD8CA4, 0xD8CB3, 0xD8CC2, 0xD8CD1, 0xD8D01, 0xD917B, 0xD918C, 0xD919A, 0xD91B5, 0xD91D0, 0xD91DD, - 0xD9220, - 0xD9273, 0xD9284, 0xD9292, 0xD92AD, 0xD92C8, 0xD92D5, 0xD9311, 0xD9322, 0xD9330, 0xD934B, 0xD9366, 0xD9373, - 0xD93B6, - 0xD97A6, 0xD97C2, 0xD97DC, 0xD97FB, 0xD9811, 0xD98FF, 0xD996F, 0xD99A8, 0xD99D5, 0xD9A30, 0xD9A4E, 0xD9A6B, - 0xD9A88, - 0xD9AF7, 0xD9B1D, 0xD9B43, 0xD9B7C, 0xD9BA9, 0xD9C84, 0xD9C8D, 0xD9CAC, 0xD9CE8, 0xD9CF3, 0xD9CFD, 0xD9D46, - 0xDA35E, - 0xDA37E, 0xDA391, 0xDA478, 0xDA4C3, 0xDA4D7, 0xDA4F6, 0xDA515, 0xDA6E2, 0xDA9C2, 0xDA9ED, 0xDAA1B, 0xDAA57, - 0xDABAF, - 0xDABC9, 0xDABE2, 0xDAC28, 0xDAC46, 0xDAC63, 0xDACB8, 0xDACEC, 0xDAD08, 0xDAD25, 0xDAD42, 0xDAD5F, 0xDAE17, - 0xDAE34, - 0xDAE51, 0xDAF2E, 0xDAF55, 0xDAF6B, 0xDAF81, 0xDB14F, 0xDB16B, 0xDB180, 0xDB195, 0xDB1AA]), - (0xD2, [0xD2B88, 0xD364A, 0xD369F, 0xD3747]), - (0xDC, - [0xD213F, 0xD2174, 0xD229E, 0xD2426, 0xD4731, 0xD4753, 0xD4774, 0xD4795, 0xD47B6, 0xD4AA5, 0xD4AE4, 0xD4B96, - 0xD4CA5, - 0xD5477, 0xD5A3D, 0xD6566, 0xD672C, 0xD67C0, 0xD69B8, 0xD6AB1, 0xD6C05, 0xD6DB3, 0xD71AB, 0xD8E2D, 0xD8F0D, - 0xD94E0, - 0xD9544, 0xD95A8, 0xD9982, 0xD9B56, 0xDA694, 0xDA6AB, 0xDAE88, 0xDAEC8, 0xDAEE6, 0xDB1BF]), - (0xE6, [0xD210A, 0xD22DC, 0xD2447, 0xD5A4D, 0xD5DDC, 0xDA251, 0xDA26C]), - (0xF0, [0xD945E, 0xD967D, 0xD96C2, 0xD9C95, 0xD9EE6, 0xDA5C6]), - (0xFA, - [0xD2047, 0xD24C2, 0xD24EC, 0xD25A4, 0xD51A8, 0xD51E6, 0xD524E, 0xD529E, 0xD6045, 0xD81DE, 0xD821E, 0xD94AA, - 0xD9A9E, - 0xD9AE4, 0xDA289]), - (0xFF, [0xD2085, 0xD21C5, 0xD5F28]) - ] - for volume, addresses in music_volumes: - for address in addresses: - rom.write_byte(address, volume if not disable_music else 0x00) + rom.write_byte(0x0CFE18, 0x00 if disable_music else rom.orig_buffer[0x0CFE18] if rom.orig_buffer else 0x70) + rom.write_byte(0x0CFEC1, 0x00 if disable_music else rom.orig_buffer[0x0CFEC1] if rom.orig_buffer else 0xC0) + rom.write_bytes(0x0D0000, [0x00, 0x00] if disable_music else rom.orig_buffer[0x0D0000:0x0D0002] if rom.orig_buffer else [0xDA, 0x58]) + rom.write_bytes(0x0D00E7, [0xC4, 0x58] if disable_music else rom.orig_buffer[0x0D00E7:0x0D00E9] if rom.orig_buffer else [0xDA, 0x58]) rom.write_byte(0x18021A, 1 if disable_music else 0x00) - # restore Mirror sound effect volumes (for existing seeds that lack it) - rom.write_byte(0xD3E04, 0xC8) - rom.write_byte(0xD3DC6, 0xC8) - rom.write_byte(0xD3D6E, 0xC8) - rom.write_byte(0xD3D34, 0xC8) - rom.write_byte(0xD3D55, 0xC8) - rom.write_byte(0xD3E38, 0xC8) - rom.write_byte(0xD3DAA, 0xFA) - # set heart beep rate rom.write_byte(0x180033, {'off': 0x00, 'half': 0x40, 'quarter': 0x80, 'normal': 0x20, 'double': 0x10}[beep]) From 5db7e066da271a89ddde52b8391947b49001a637 Mon Sep 17 00:00:00 2001 From: Bonta-kun <40473493+Bonta0@users.noreply.github.com> Date: Thu, 9 Jan 2020 02:30:00 +0100 Subject: [PATCH 12/20] Sprites are now player specific, can be chosen from their sprite name rather than file path, support "random" choice and support "randomonhit" enemizer-only option --- Adjuster.py | 5 +++-- AdjusterMain.py | 13 ++----------- EntranceRandomizer.py | 7 ++++--- Main.py | 29 ++++++++--------------------- Plando.py | 13 ++++--------- Rom.py | 43 ++++++++++++++++++++++++++++++++++++++----- 6 files changed, 59 insertions(+), 51 deletions(-) diff --git a/Adjuster.py b/Adjuster.py index d3a8239c..570bcef9 100755 --- a/Adjuster.py +++ b/Adjuster.py @@ -6,6 +6,7 @@ import textwrap import sys from AdjusterMain import adjust +from Rom import get_sprite_from_name class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter): @@ -48,8 +49,8 @@ def main(): if not os.path.isfile(args.rom): input('Could not find valid 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 args.sprite is not None and not os.path.isfile(args.sprite): - input('Could not find link sprite sheet at given location. \nPress Enter to exit.' % args.sprite) + if args.sprite is not None and not os.path.isfile(args.sprite) and not get_sprite_from_name(args.sprite): + input('Could not find link sprite sheet at given location. \nPress Enter to exit.') sys.exit(1) # set up logger diff --git a/AdjusterMain.py b/AdjusterMain.py index 5bf6eeb4..4bdfa50b 100644 --- a/AdjusterMain.py +++ b/AdjusterMain.py @@ -1,10 +1,9 @@ import os -import re import time import logging from Utils import output_path, parse_names_string -from Rom import LocalRom, Sprite, apply_rom_settings +from Rom import LocalRom, apply_rom_settings def adjust(args): @@ -12,14 +11,6 @@ def adjust(args): logger = logging.getLogger('') logger.info('Patching ROM.') - if args.sprite is not None: - if isinstance(args.sprite, Sprite): - sprite = args.sprite - else: - sprite = Sprite(args.sprite) - else: - sprite = None - outfilebase = os.path.basename(args.rom)[:-4] + '_adjusted' if os.stat(args.rom).st_size in (0x200000, 0x400000) and os.path.splitext(args.rom)[-1].lower() == '.sfc': @@ -30,7 +21,7 @@ def adjust(args): else: raise RuntimeError('Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.') - apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, sprite, args.ow_palettes, args.uw_palettes, parse_names_string(args.names)) + apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes, parse_names_string(args.names)) rom.write_to_file(output_path('%s.sfc' % outfilebase)) diff --git a/EntranceRandomizer.py b/EntranceRandomizer.py index c315c962..58d6d097 100755 --- a/EntranceRandomizer.py +++ b/EntranceRandomizer.py @@ -9,6 +9,7 @@ import shlex import sys from Main import main +from Rom import get_sprite_from_name from Utils import is_bundled, close_console @@ -289,7 +290,7 @@ def parse_arguments(argv, no_defaults=False): 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', 'retro', 'accessibility', 'hints', 'shufflepots', 'beemizer', 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', - 'ow_palettes', 'uw_palettes']: + 'ow_palettes', 'uw_palettes', 'sprite']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -315,9 +316,9 @@ def start(): 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) sys.exit(1) - if args.sprite is not None and not os.path.isfile(args.sprite): + 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.' % args.sprite) + 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) diff --git a/Main.py b/Main.py index f20940fb..836a4d65 100644 --- a/Main.py +++ b/Main.py @@ -13,7 +13,7 @@ from Items import ItemFactory from Regions import create_regions, 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, get_race_rom_patches, get_enemizer_patch, apply_rom_settings, Sprite, LocalRom, JsonRom +from Rom import patch_rom, get_race_rom_patches, get_enemizer_patch, apply_rom_settings, LocalRom, JsonRom 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 @@ -135,14 +135,6 @@ def main(args, seed=None): logger.info('Patching ROM.') - if args.sprite is not None: - if isinstance(args.sprite, Sprite): - sprite = args.sprite - else: - sprite = Sprite(args.sprite) - else: - sprite = None - player_names = parse_names_string(args.names) outfilebase = 'ER_%s' % (args.outputname if args.outputname else world.seed) @@ -150,25 +142,20 @@ def main(args, seed=None): jsonout = {} if not args.suppress_rom: for player in range(1, world.players + 1): + sprite_random_on_hit = type(args.sprite[player]) is str and args.sprite[player].lower() == 'randomonhit' use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] != 'none' or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default' - or args.shufflepots[player]) + or args.shufflepots[player] or sprite_random_on_hit) + + rom = JsonRom() if args.jsonout or use_enemizer else LocalRom(args.rom) + local_rom = LocalRom(args.rom) if not args.jsonout and use_enemizer else None - local_rom = None - if args.jsonout: - rom = JsonRom() - else: - if use_enemizer: - local_rom = LocalRom(args.rom) - rom = JsonRom() - else: - rom = LocalRom(args.rom) patch_rom(world, player, rom, use_enemizer) rom_names.append((player, list(rom.name))) enemizer_patch = [] if use_enemizer and (args.enemizercli or not args.jsonout): - enemizer_patch = get_enemizer_patch(world, player, rom, args.rom, args.enemizercli, args.shufflepots[player]) + enemizer_patch = get_enemizer_patch(world, player, rom, args.rom, args.enemizercli, args.shufflepots[player], sprite_random_on_hit) if args.jsonout: jsonout[f'patch{player}'] = rom.patches @@ -185,7 +172,7 @@ def main(args, seed=None): for addr, values in get_race_rom_patches(rom).items(): rom.write_bytes(int(addr), values) - apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite, args.ow_palettes[player], args.uw_palettes[player], player_names) + apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, args.sprite[player], args.ow_palettes[player], args.uw_palettes[player], player_names) mcsb_name = '' if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]): diff --git a/Plando.py b/Plando.py index cf1fbf51..917d4649 100755 --- a/Plando.py +++ b/Plando.py @@ -10,7 +10,7 @@ import sys from BaseClasses import World from Regions import create_regions from EntranceShuffle import link_entrances, connect_entrance, connect_two_way, connect_exit -from Rom import patch_rom, LocalRom, Sprite, write_string_to_rom, apply_rom_settings +from Rom import patch_rom, LocalRom, write_string_to_rom, apply_rom_settings, get_sprite_from_name from Rules import set_rules from Dungeons import create_dungeons from Items import ItemFactory @@ -68,15 +68,10 @@ def main(args): logger.info('Patching ROM.') - if args.sprite is not None: - sprite = Sprite(args.sprite) - else: - sprite = None - rom = LocalRom(args.rom) patch_rom(world, 1, rom, False) - apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite, args.ow_palettes, args.uw_palettes) + apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, args.sprite, args.ow_palettes, args.uw_palettes) for textname, texttype, text in text_patches: if texttype == 'text': @@ -226,8 +221,8 @@ def start(): if not os.path.isfile(args.plando): input('Could not find Plandomizer distribution at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.plando) sys.exit(1) - if args.sprite is not None and not os.path.isfile(args.rom): - input('Could not find link sprite sheet at given location. \nPress Enter to exit.' % args.sprite) + if args.sprite is not None and not os.path.isfile(args.sprite) and not get_sprite_from_name(args.sprite): + input('Could not find link sprite sheet at given location. \nPress Enter to exit.') sys.exit(1) # set up logger diff --git a/Rom.py b/Rom.py index cdb1a641..9d942511 100644 --- a/Rom.py +++ b/Rom.py @@ -37,8 +37,7 @@ class JsonRom(object): def write_bytes(self, startaddress, values): if not values: return - if type(values) is not list: - values = list(values) + values = list(values) pos = bisect.bisect_right(self.addresses, startaddress) intervalstart = self.addresses[pos-1] if pos else None @@ -164,7 +163,7 @@ def read_rom(stream): buffer = buffer[0x200:] return buffer -def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepots): +def get_enemizer_patch(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')) randopatch_path = os.path.abspath(output_path('enemizer_randopatch.json')) @@ -224,7 +223,7 @@ def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepot 'RandomizeTileTrapPattern': world.enemy_shuffle[player] == 'chaos', 'RandomizeTileTrapFloorTile': False, 'AllowKillableThief': bool(random.randint(0,1)) if world.enemy_shuffle[player] == 'chaos' else world.enemy_shuffle[player] != 'none', - 'RandomizeSpriteOnHit': False, + 'RandomizeSpriteOnHit': random_sprite_on_hit, 'DebugMode': False, 'DebugForceEnemy': False, 'DebugForceEnemyId': 0, @@ -260,7 +259,6 @@ def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepot options['ManualBosses']['GanonsTower2'] = world.get_dungeon('Inverted Ganons Tower', player).bosses['middle'].enemizer_name options['ManualBosses']['GanonsTower3'] = world.get_dungeon('Inverted Ganons Tower', player).bosses['top'].enemizer_name - rom.write_to_file(randopatch_path) with open(options_path, 'w') as f: @@ -287,8 +285,41 @@ def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepot if os.path.exists(enemizer_output_path): os.remove(enemizer_output_path) + if random_sprite_on_hit: + _populate_sprite_table() + sprites = list(_sprite_table.values()) + if sprites: + while len(sprites) < 32: + sprites.extend(sprites) + random.shuffle(sprites) + + for i, path in enumerate(sprites[:32]): + sprite = Sprite(path) + ret.append({"address": 0x300000 + (i * 0x8000), "patchData": list(sprite.sprite)}) + ret.append({"address": 0x307000 + (i * 0x8000), "patchData": list(sprite.palette)}) + ret.append({"address": 0x307078 + (i * 0x8000), "patchData": list(sprite.glove_palette)}) + return ret +_sprite_table = {} +def _populate_sprite_table(): + if not _sprite_table: + for dir in [local_path('data/sprites/official'), local_path('data/sprites/unofficial')]: + for file in os.listdir(dir): + filepath = os.path.join(dir, file) + if not os.path.isfile(filepath): + continue + sprite = Sprite(filepath) + if sprite.valid: + _sprite_table[sprite.name.lower()] = filepath + +def get_sprite_from_name(name): + _populate_sprite_table() + name = name.lower() + if name in ['random', 'randomonhit']: + return Sprite(random.choice(list(_sprite_table.values()))) + return Sprite(_sprite_table[name]) if name in _sprite_table else None + class Sprite(object): default_palette = [255, 127, 126, 35, 183, 17, 158, 54, 165, 20, 255, 1, 120, 16, 157, 89, 71, 54, 104, 59, 74, 10, 239, 18, 92, 42, 113, 21, 24, 122, @@ -1282,6 +1313,8 @@ def hud_format_text(text): def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite, ow_palettes, uw_palettes, names = None): + if sprite and not isinstance(sprite, Sprite): + sprite = Sprite(sprite) if os.path.isfile(sprite) else get_sprite_from_name(sprite) # enable instant item menu if fastmenu == 'instant': From 42b85d7a3cc30624691f61f76a9f95e5398cab92 Mon Sep 17 00:00:00 2001 From: Bonta-kun <40473493+Bonta0@users.noreply.github.com> Date: Thu, 9 Jan 2020 08:31:49 +0100 Subject: [PATCH 13/20] Include sphere0 items in the spoiler log and in the playthrough --- BaseClasses.py | 10 +++++++++- Main.py | 13 +++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 5fdf3b27..16a9935a 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -214,6 +214,8 @@ class World(object): return [location for location in self.get_locations() if location.item is not None and location.item.name == item and location.item.player == player] def push_precollected(self, item): + if (item.smallkey and self.keyshuffle[item.player]) or (item.bigkey and self.bigkeyshuffle[item.player]): + item.advancement = True self.precollected_items.append(item) self.state.collect(item, True) @@ -962,6 +964,7 @@ class Spoiler(object): self.medallions = {} self.playthrough = {} self.unreachables = [] + self.startinventory = [] self.locations = {} self.paths = {} self.metadata = {} @@ -984,6 +987,8 @@ class Spoiler(object): self.medallions['Misery Mire (Player %d)' % player] = self.world.required_medallions[player][0] self.medallions['Turtle Rock (Player %d)' % player] = self.world.required_medallions[player][1] + self.startinventory = self.world.precollected_items.copy() + self.locations = OrderedDict() listed_locations = set() @@ -1081,6 +1086,7 @@ class Spoiler(object): out = OrderedDict() out['Entrances'] = list(self.entrances.values()) out.update(self.locations) + out['Starting Inventory'] = self.startinventory out['Special'] = self.medallions if self.shops: out['Shops'] = self.shops @@ -1131,12 +1137,14 @@ class Spoiler(object): for player in range(1, self.world.players + 1): outfile.write('\nMisery Mire Medallion (Player %d): %s' % (player, self.medallions['Misery Mire (Player %d)' % player])) outfile.write('\nTurtle Rock Medallion (Player %d): %s' % (player, self.medallions['Turtle Rock (Player %d)' % player])) + outfile.write('\n\nStarting Inventory:\n\n') + outfile.write('\n'.join(map(str, self.startinventory))) outfile.write('\n\nLocations:\n\n') outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()])) outfile.write('\n\nShops:\n\n') outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops)) outfile.write('\n\nPlaythrough:\n\n') - outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()])) for (sphere_nr, sphere) in self.playthrough.items()])) + outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()])) if self.unreachables: outfile.write('\n\nUnreachable Items:\n\n') outfile.write('\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables])) diff --git a/Main.py b/Main.py index 836a4d65..6274ede1 100644 --- a/Main.py +++ b/Main.py @@ -380,7 +380,6 @@ def create_playthrough(world): logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player) old_item = location.item location.item = None - state.remove(old_item) if world.can_beat_game(state_cache[num]): to_delete.append(location) else: @@ -391,6 +390,14 @@ def create_playthrough(world): for location in to_delete: sphere.remove(location) + # second phase, sphere 0 + for item in [i for i in world.precollected_items if i.advancement]: + logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player) + world.precollected_items.remove(item) + world.state.remove(item) + if not world.can_beat_game(): + world.push_precollected(item) + # we are now down to just the required progress items in collection_spheres. Unfortunately # the previous pruning stage could potentially have made certain items dependant on others # in the same or later sphere (because the location had 2 ways to access but the item originally @@ -442,4 +449,6 @@ def create_playthrough(world): old_world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, world.get_region('Inverted Big Bomb Shop', player)) # we can finally output our playthrough - old_world.spoiler.playthrough = OrderedDict([(str(i + 1), {str(location): str(location.item) for location in sphere}) for i, sphere in enumerate(collection_spheres)]) + old_world.spoiler.playthrough = OrderedDict([("0", [item for item in world.precollected_items if item.advancement])]) + for i, sphere in enumerate(collection_spheres): + old_world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sphere} From 6bb71802ae886365c28e21f60feac0ff9281c906 Mon Sep 17 00:00:00 2001 From: Bonta-kun <40473493+Bonta0@users.noreply.github.com> Date: Thu, 9 Jan 2020 08:40:03 +0100 Subject: [PATCH 14/20] Dont tag capacity upgrade shop as replaceable --- InvertedRegions.py | 2 +- Regions.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/InvertedRegions.py b/InvertedRegions.py index e1915d39..81422609 100644 --- a/InvertedRegions.py +++ b/InvertedRegions.py @@ -311,7 +311,7 @@ def create_inverted_regions(world, player): shop.add_inventory(index, item, price) region = world.get_region('Capacity Upgrade', player) - shop = Shop(region, 0x0115, ShopType.UpgradeShop, 0x04, True) + shop = Shop(region, 0x0115, ShopType.UpgradeShop, 0x04, False) region.shop = shop world.shops.append(shop) shop.add_inventory(0, 'Bomb Upgrade (+5)', 100, 7) diff --git a/Regions.py b/Regions.py index 021cf217..93959edf 100644 --- a/Regions.py +++ b/Regions.py @@ -302,7 +302,7 @@ def create_regions(world, player): shop.add_inventory(index, item, price) region = world.get_region('Capacity Upgrade', player) - shop = Shop(region, 0x0115, ShopType.UpgradeShop, 0x04, True) + shop = Shop(region, 0x0115, ShopType.UpgradeShop, 0x04, False) region.shop = shop world.shops.append(shop) shop.add_inventory(0, 'Bomb Upgrade (+5)', 100, 7) From 240cf2d84459b76d0534694a4cb9fe2e8d4ebda5 Mon Sep 17 00:00:00 2001 From: Bonta-kun <40473493+Bonta0@users.noreply.github.com> Date: Thu, 9 Jan 2020 09:13:50 +0100 Subject: [PATCH 15/20] Mystery: pot_shuffle (on/off) --- Mystery.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/Mystery.py b/Mystery.py index 9d7755ea..0ebd1c06 100644 --- a/Mystery.py +++ b/Mystery.py @@ -113,6 +113,8 @@ def get_weights(path): def roll_settings(weights): def get_choice(option, root=weights): + if option not in weights: + return None if type(root[option]) is not dict: return root[option] if not root[option]: @@ -130,17 +132,11 @@ def roll_settings(weights): item_placement = get_choice('item_placement') # not supported in ER - if {'map_shuffle', 'compass_shuffle', 'smallkey_shuffle', 'bigkey_shuffle'}.issubset(weights.keys()): - ret.mapshuffle = get_choice('map_shuffle') == 'on' - ret.compassshuffle = get_choice('compass_shuffle') == 'on' - ret.keyshuffle = get_choice('smallkey_shuffle') == 'on' - ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on' - else: - dungeon_items = get_choice('dungeon_items') - ret.mapshuffle = dungeon_items in ['mc', 'mcs', 'full'] - ret.compassshuffle = dungeon_items in ['mc', 'mcs', 'full'] - ret.keyshuffle = dungeon_items in ['mcs', 'full'] - ret.bigkeyshuffle = dungeon_items in ['full'] + dungeon_items = get_choice('dungeon_items') + ret.mapshuffle = get_choice('map_shuffle') == 'on' if 'map_shuffle' in weights else dungeon_items in ['mc', 'mcs', 'full'] + ret.compassshuffle = get_choice('compass_shuffle') == 'on' if 'compass_shuffle' in weights else dungeon_items in ['mc', 'mcs', 'full'] + ret.keyshuffle = get_choice('smallkey_shuffle') == 'on' if 'smallkey_shuffle' in weights else dungeon_items in ['mcs', 'full'] + ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on' if 'bigkey_shuffle' in weights else dungeon_items in ['full'] accessibility = get_choice('accessibility') ret.accessibility = accessibility @@ -207,7 +203,10 @@ def roll_settings(weights): enemy_health = get_choice('enemy_health') ret.enemy_health = enemy_health - ret.beemizer = int(get_choice('beemizer')) if 'beemizer' in weights.keys() else 1 # suck it :) + pot_shuffle = get_choice('pot_shuffle') + ret.shufflepots = pot_shuffle == 'on' + + ret.beemizer = int(get_choice('beemizer')) if 'beemizer' in weights else 0 inventoryweights = weights.get('startinventory', {}) startitems = [] From 6bafdfafe6dc2833a4422eaf5896f8cfa51c002a Mon Sep 17 00:00:00 2001 From: Bonta-kun <40473493+Bonta0@users.noreply.github.com> Date: Thu, 9 Jan 2020 17:46:07 +0100 Subject: [PATCH 16/20] Mystery: rom options can be set in weights file, eg rom: sprite: random: 1 randomonhit: 1 mog: 1 disablemusic: off quickswap: on: 1 off: 0 menuspeed: normal: 1 instant: 1 double: 1 triple: 1 quadruple: 1 half: 1 heartcolor: red: 1 blue: 1 green: 1 yellow: 1 random: 1 heartbeep: double: 1 normal: 1 half: 1 quarter: 1 off: 1 ow_palettes: default: 1 random: 1 blackout: 1 uw_palettes: default: 1 random: 1 blackout: 1 --- BaseClasses.py | 7 +---- EntranceRandomizer.py | 6 ++--- Main.py | 6 ++--- Mystery.py | 62 +++++++++++++++++++++---------------------- Plando.py | 4 +-- 5 files changed, 39 insertions(+), 46 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 16a9935a..3438fa20 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -9,7 +9,7 @@ from Utils import int16_as_bytes class World(object): - def __init__(self, players, shuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, quickswap, fastmenu, disable_music, retro, custom, customitemarray, hints): + def __init__(self, players, shuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints): self.players = players self.shuffle = shuffle.copy() self.logic = logic.copy() @@ -44,9 +44,6 @@ class World(object): self.accessibility = accessibility.copy() self.shuffle_ganon = shuffle_ganon self.fix_gtower_exit = self.shuffle_ganon - self.quickswap = quickswap - self.fastmenu = fastmenu - self.disable_music = disable_music self.retro = retro.copy() self.custom = custom self.customitemarray = customitemarray @@ -1124,8 +1121,6 @@ class Spoiler(object): outfile.write('Enemy health: %s\n' % self.metadata['enemy_health']) outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage']) outfile.write('Hints: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['hints'].items()}) - outfile.write('L\\R Quickswap enabled: %s\n' % ('Yes' if self.world.quickswap else 'No')) - outfile.write('Menu speed: %s' % self.world.fastmenu) if self.entrances: outfile.write('\n\nEntrances:\n\n') outfile.write('\n'.join(['%s%s %s %s' % ('Player {0}: '.format(entry['player']) if self.world.players >1 else '', entry['entrance'], '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', entry['exit']) for entry in self.entrances.values()])) diff --git a/EntranceRandomizer.py b/EntranceRandomizer.py index 58d6d097..eb643baa 100755 --- a/EntranceRandomizer.py +++ b/EntranceRandomizer.py @@ -288,9 +288,9 @@ def parse_arguments(argv, no_defaults=False): for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'shuffle', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', - 'retro', 'accessibility', 'hints', 'shufflepots', 'beemizer', - 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', - 'ow_palettes', 'uw_palettes', 'sprite']: + 'retro', 'accessibility', 'hints', 'beemizer', + 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', + 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) diff --git a/Main.py b/Main.py index 6274ede1..8315569c 100644 --- a/Main.py +++ b/Main.py @@ -33,7 +33,7 @@ def main(args, seed=None): start = time.process_time() # initialize the world - world = World(args.multi, args.shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, args.accessibility, args.shuffleganon, args.quickswap, args.fastmenu, args.disablemusic, args.retro, args.custom, args.customitemarray, args.hints) + world = World(args.multi, args.shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, args.accessibility, args.shuffleganon, args.retro, args.custom, args.customitemarray, args.hints) logger = logging.getLogger('') if seed is None: random.seed(None) @@ -172,7 +172,7 @@ def main(args, seed=None): for addr, values in get_race_rom_patches(rom).items(): rom.write_bytes(int(addr), values) - apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, args.sprite[player], args.ow_palettes[player], args.uw_palettes[player], player_names) + apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player], args.fastmenu[player], args.disablemusic[player], args.sprite[player], args.ow_palettes[player], args.uw_palettes[player], player_names) mcsb_name = '' if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]): @@ -219,7 +219,7 @@ def main(args, seed=None): def copy_world(world): # ToDo: Not good yet - ret = World(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.quickswap, world.fastmenu, world.disable_music, world.retro, world.custom, world.customitemarray, world.hints) + ret = World(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints) ret.required_medallions = world.required_medallions.copy() ret.swamp_patch_required = world.swamp_patch_required.copy() ret.ganon_at_pyramid = world.ganon_at_pyramid.copy() diff --git a/Mystery.py b/Mystery.py index 0ebd1c06..0c5c848c 100644 --- a/Mystery.py +++ b/Mystery.py @@ -88,7 +88,8 @@ def main(): if path: settings = settings_cache[path] if settings_cache[path] else roll_settings(weights_cache[path]) for k, v in vars(settings).items(): - getattr(erargs, k)[player] = v + if v is not None: + getattr(erargs, k)[player] = v else: raise RuntimeError(f'No weights specified for player {player}') @@ -113,7 +114,7 @@ def get_weights(path): def roll_settings(weights): def get_choice(option, root=weights): - if option not in weights: + if option not in root: return None if type(root[option]) is not dict: return root[option] @@ -138,73 +139,59 @@ def roll_settings(weights): ret.keyshuffle = get_choice('smallkey_shuffle') == 'on' if 'smallkey_shuffle' in weights else dungeon_items in ['mcs', 'full'] ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on' if 'bigkey_shuffle' in weights else dungeon_items in ['full'] - accessibility = get_choice('accessibility') - ret.accessibility = accessibility + ret.accessibility = get_choice('accessibility') entrance_shuffle = get_choice('entrance_shuffle') ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla' - goals = get_choice('goals') ret.goal = {'ganon': 'ganon', 'fast_ganon': 'crystals', 'dungeons': 'dungeons', 'pedestal': 'pedestal', 'triforce-hunt': 'triforcehunt' - }[goals] - ret.openpyramid = goals == 'fast_ganon' + }[get_choice('goals')] + ret.openpyramid = ret.goal == 'fast_ganon' - tower_open = get_choice('tower_open') - ret.crystals_gt = tower_open + ret.crystals_gt = get_choice('tower_open') - ganon_open = get_choice('ganon_open') - ret.crystals_ganon = ganon_open + ret.crystals_ganon = get_choice('ganon_open') - world_state = get_choice('world_state') - ret.mode = world_state - if world_state == 'retro': + ret.mode = get_choice('world_state') + if ret.mode == 'retro': ret.mode = 'open' ret.retro = True - hints = get_choice('hints') - ret.hints = hints == 'on' + ret.hints = get_choice('hints') == 'on' - weapons = get_choice('weapons') ret.swords = {'randomized': 'random', 'assured': 'assured', 'vanilla': 'vanilla', 'swordless': 'swordless' - }[weapons] + }[get_choice('weapons')] - item_pool = get_choice('item_pool') - ret.difficulty = item_pool + ret.difficulty = get_choice('item_pool') - item_functionality = get_choice('item_functionality') - ret.item_functionality = item_functionality + ret.item_functionality = get_choice('item_functionality') - boss_shuffle = get_choice('boss_shuffle') ret.shufflebosses = {'none': 'none', 'simple': 'basic', 'full': 'normal', 'random': 'chaos' - }[boss_shuffle] + }[get_choice('boss_shuffle')] - enemy_shuffle = get_choice('enemy_shuffle') ret.shuffleenemies = {'none': 'none', 'shuffled': 'shuffled', 'random': 'chaos' - }[enemy_shuffle] + }[get_choice('enemy_shuffle')] - enemy_damage = get_choice('enemy_damage') ret.enemy_damage = {'default': 'default', 'shuffled': 'shuffled', 'random': 'chaos' - }[enemy_damage] + }[get_choice('enemy_damage')] - enemy_health = get_choice('enemy_health') - ret.enemy_health = enemy_health + ret.enemy_health = get_choice('enemy_health') - pot_shuffle = get_choice('pot_shuffle') - ret.shufflepots = pot_shuffle == 'on' + ret.shufflepots = get_choice('pot_shuffle') == 'on' ret.beemizer = int(get_choice('beemizer')) if 'beemizer' in weights else 0 @@ -215,6 +202,17 @@ def roll_settings(weights): startitems.append(item) ret.startinventory = ','.join(startitems) + if 'rom' in weights: + romweights = weights['rom'] + ret.sprite = get_choice('sprite', romweights) + ret.disablemusic = get_choice('disablemusic', romweights) == 'on' + ret.quickswap = get_choice('quickswap', romweights) == 'on' + ret.fastmenu = get_choice('menuspeed', romweights) + ret.heartcolor = get_choice('heartcolor', romweights) + ret.heartbeep = get_choice('heartbeep', romweights) + ret.ow_palettes = get_choice('ow_palettes', romweights) + ret.uw_palettes = get_choice('uw_palettes', romweights) + return ret if __name__ == '__main__': diff --git a/Plando.py b/Plando.py index 917d4649..46af99c3 100755 --- a/Plando.py +++ b/Plando.py @@ -23,7 +23,7 @@ def main(args): start_time = time.process_time() # initialize the world - world = World(1, 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, args.quickswap, args.fastmenu, args.disablemusic, False, False, False, None, False) + world = World(1, 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False) logger = logging.getLogger('') hasher = hashlib.md5() @@ -71,7 +71,7 @@ def main(args): rom = LocalRom(args.rom) patch_rom(world, 1, rom, False) - apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, args.sprite, args.ow_palettes, args.uw_palettes) + apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes) for textname, texttype, text in text_patches: if texttype == 'text': From 77ae96cf1b7a84a0e6d2b37a7f0e1b8bf042fb7c Mon Sep 17 00:00:00 2001 From: Bonta-kun <40473493+Bonta0@users.noreply.github.com> Date: Fri, 10 Jan 2020 07:02:44 +0100 Subject: [PATCH 17/20] Refactor rom patching now that jsonrom patches can safely be merged --- BaseClasses.py | 4 +-- Main.py | 45 ++++++++++++++--------------- Rom.py | 77 ++++++++++++++++++++++---------------------------- 3 files changed, 56 insertions(+), 70 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 3438fa20..ffdf060e 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -984,7 +984,7 @@ class Spoiler(object): self.medallions['Misery Mire (Player %d)' % player] = self.world.required_medallions[player][0] self.medallions['Turtle Rock (Player %d)' % player] = self.world.required_medallions[player][1] - self.startinventory = self.world.precollected_items.copy() + self.startinventory = list(map(str, self.world.precollected_items)) self.locations = OrderedDict() listed_locations = set() @@ -1133,7 +1133,7 @@ class Spoiler(object): outfile.write('\nMisery Mire Medallion (Player %d): %s' % (player, self.medallions['Misery Mire (Player %d)' % player])) outfile.write('\nTurtle Rock Medallion (Player %d): %s' % (player, self.medallions['Turtle Rock (Player %d)' % player])) outfile.write('\n\nStarting Inventory:\n\n') - outfile.write('\n'.join(map(str, self.startinventory))) + outfile.write('\n'.join(self.startinventory)) outfile.write('\n\nLocations:\n\n') outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()])) outfile.write('\n\nShops:\n\n') diff --git a/Main.py b/Main.py index 8315569c..3b21c512 100644 --- a/Main.py +++ b/Main.py @@ -13,7 +13,7 @@ from Items import ItemFactory from Regions import create_regions, 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, get_race_rom_patches, get_enemizer_patch, apply_rom_settings, LocalRom, JsonRom +from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom 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 @@ -148,32 +148,25 @@ def main(args, seed=None): or args.shufflepots[player] or sprite_random_on_hit) rom = JsonRom() if args.jsonout or use_enemizer else LocalRom(args.rom) - local_rom = LocalRom(args.rom) if not args.jsonout and use_enemizer else None patch_rom(world, player, rom, use_enemizer) rom_names.append((player, list(rom.name))) - enemizer_patch = [] if use_enemizer and (args.enemizercli or not args.jsonout): - enemizer_patch = get_enemizer_patch(world, player, rom, args.rom, args.enemizercli, args.shufflepots[player], sprite_random_on_hit) + patch_enemizer(world, player, rom, args.rom, args.enemizercli, args.shufflepots[player], sprite_random_on_hit) + if not args.jsonout: + patches = rom.patches + rom = LocalRom(args.rom) + rom.merge_enemizer_patches(patches) + + if args.race: + patch_race_rom(rom) + + apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player], args.fastmenu[player], args.disablemusic[player], args.sprite[player], args.ow_palettes[player], args.uw_palettes[player], player_names) if args.jsonout: jsonout[f'patch{player}'] = rom.patches - if use_enemizer: - jsonout[f'enemizer{player}'] = enemizer_patch - if args.race: - jsonout[f'race{player}'] = get_race_rom_patches(rom) else: - if use_enemizer: - local_rom.patch_enemizer(rom.patches, os.path.join(os.path.dirname(args.enemizercli), "enemizerBasePatch.json"), enemizer_patch) - rom = local_rom - - if args.race: - for addr, values in get_race_rom_patches(rom).items(): - rom.write_bytes(int(addr), values) - - apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player], args.fastmenu[player], args.disablemusic[player], args.sprite[player], args.ow_palettes[player], args.uw_palettes[player], player_names) - mcsb_name = '' if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]): mcsb_name = '-keysanity' @@ -194,11 +187,15 @@ def main(args, seed=None): "-nohints" if not world.hints[player] else "")) if not args.outputname else '' rom.write_to_file(output_path(f'{outfilebase}{playername}{outfilesuffix}.sfc')) - with open(output_path('%s_multidata' % outfilebase), 'wb') as f: - jsonstr = json.dumps((world.players, - rom_names, - [((location.address, location.player), (location.item.code, location.item.player)) for location in world.get_filled_locations() if type(location.address) is int])) - f.write(zlib.compress(jsonstr.encode("utf-8"))) + multidata = zlib.compress(json.dumps((world.players, + rom_names, + [((location.address, location.player), (location.item.code, location.item.player)) for location in world.get_filled_locations() if type(location.address) is int]) + ).encode("utf-8")) + if args.jsonout: + jsonout["multidata"] = list(multidata) + else: + with open(output_path('%s_multidata' % outfilebase), 'wb') as f: + f.write(multidata) if args.create_spoiler and not args.jsonout: world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) @@ -449,6 +446,6 @@ def create_playthrough(world): old_world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, world.get_region('Inverted Big Bomb Shop', player)) # we can finally output our playthrough - old_world.spoiler.playthrough = OrderedDict([("0", [item for item in world.precollected_items if item.advancement])]) + old_world.spoiler.playthrough = OrderedDict([("0", [str(item) for item in world.precollected_items if item.advancement])]) for i, sphere in enumerate(collection_spheres): old_world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sphere} diff --git a/Rom.py b/Rom.py index 9d942511..ec71854c 100644 --- a/Rom.py +++ b/Rom.py @@ -114,24 +114,11 @@ class LocalRom(object): # if RANDOMIZERBASEHASH != patchedmd5.hexdigest(): # 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 patch_enemizer(self, rando_patch, base_enemizer_patch_path, enemizer_patch): - # extend to 4MB + def merge_enemizer_patches(self, patches): self.buffer.extend(bytearray([0x00] * (0x400000 - len(self.buffer)))) - - # apply randomizer patches - for address, values in rando_patch.items(): + for address, values in patches.items(): self.write_bytes(int(address), values) - # load base enemizer patches - with open(base_enemizer_patch_path, 'r') as f: - base_enemizer_patch = json.load(f) - for patch in base_enemizer_patch: - self.write_bytes(patch["address"], patch["patchData"]) - - # apply enemizer patches - for patch in enemizer_patch: - self.write_bytes(patch["address"], patch["patchData"]) - def write_crc(self): crc = (sum(self.buffer[:0x7FDC] + self.buffer[0x7FE0:]) + 0x01FE) & 0xFFFF inv = crc ^ 0xFFFF @@ -163,9 +150,10 @@ def read_rom(stream): buffer = buffer[0x200:] return buffer -def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepots, random_sprite_on_hit): +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('enemizer_randopatch.json')) options_path = os.path.abspath(output_path('enemizer_options.json')) enemizer_output_path = os.path.abspath(output_path('enemizer_output.json')) @@ -245,20 +233,14 @@ def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepot 'IcePalace': world.get_dungeon("Ice Palace", player).boss.enemizer_name, 'MiseryMire': world.get_dungeon("Misery Mire", player).boss.enemizer_name, 'TurtleRock': world.get_dungeon("Turtle Rock", player).boss.enemizer_name, + 'GanonsTower1': world.get_dungeon('Ganons Tower' if world.mode[player] != 'inverted' else 'Inverted Ganons Tower', player).bosses['bottom'].enemizer_name, + 'GanonsTower2': world.get_dungeon('Ganons Tower' if world.mode[player] != 'inverted' else 'Inverted Ganons Tower', player).bosses['middle'].enemizer_name, + 'GanonsTower3': world.get_dungeon('Ganons Tower' if world.mode[player] != 'inverted' else 'Inverted Ganons Tower', player).bosses['top'].enemizer_name, 'GanonsTower4': 'Agahnim2', 'Ganon': 'Ganon', } } - if world.mode[player] != 'inverted': - options['ManualBosses']['GanonsTower1'] = world.get_dungeon('Ganons Tower', player).bosses['bottom'].enemizer_name - options['ManualBosses']['GanonsTower2'] = world.get_dungeon('Ganons Tower', player).bosses['middle'].enemizer_name - options['ManualBosses']['GanonsTower3'] = world.get_dungeon('Ganons Tower', player).bosses['top'].enemizer_name - else: - options['ManualBosses']['GanonsTower1'] = world.get_dungeon('Inverted Ganons Tower', player).bosses['bottom'].enemizer_name - options['ManualBosses']['GanonsTower2'] = world.get_dungeon('Inverted Ganons Tower', player).bosses['middle'].enemizer_name - options['ManualBosses']['GanonsTower3'] = world.get_dungeon('Inverted Ganons Tower', player).bosses['top'].enemizer_name - rom.write_to_file(randopatch_path) with open(options_path, 'w') as f: @@ -273,17 +255,13 @@ def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepot '--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: - ret = json.load(f) - - if os.path.exists(randopatch_path): - os.remove(randopatch_path) - - if os.path.exists(options_path): - os.remove(options_path) - - if os.path.exists(enemizer_output_path): - os.remove(enemizer_output_path) + for patch in json.load(f): + rom.write_bytes(patch["address"], patch["patchData"]) if random_sprite_on_hit: _populate_sprite_table() @@ -295,11 +273,24 @@ def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepot for i, path in enumerate(sprites[:32]): sprite = Sprite(path) - ret.append({"address": 0x300000 + (i * 0x8000), "patchData": list(sprite.sprite)}) - ret.append({"address": 0x307000 + (i * 0x8000), "patchData": list(sprite.palette)}) - ret.append({"address": 0x307078 + (i * 0x8000), "patchData": list(sprite.glove_palette)}) + rom.write_bytes(0x300000 + (i * 0x8000), sprite.sprite) + rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette) + rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette) - return ret + try: + os.remove(randopatch_path) + except OSError: + pass + + try: + os.remove(options_path) + except OSError: + pass + + try: + os.remove(enemizer_output_path) + except OSError: + pass _sprite_table = {} def _populate_sprite_table(): @@ -1255,13 +1246,11 @@ try: except ImportError: RaceRom = None -def get_race_rom_patches(rom): - patches = {str(0x180213): [0x01, 0x00]} # Tournament Seed +def patch_race_rom(rom): + rom.write_bytes(0x180213, [0x01, 0x00]) # Tournament Seed if 'RaceRom' in sys.modules: - RaceRom.encrypt(rom, patches) - - return patches + RaceRom.encrypt(rom) def write_custom_shops(rom, world, player): shops = [shop for shop in world.shops if shop.replaceable and shop.active and shop.region.player == player] From 39a07a062475303a414e09291c3469834c7c7273 Mon Sep 17 00:00:00 2001 From: Bonta-kun <40473493+Bonta0@users.noreply.github.com> Date: Fri, 10 Jan 2020 07:15:11 +0100 Subject: [PATCH 18/20] Rom: dont block HC exit in standard with vanilla entrances to match website mystery behavior --- Rom.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index ec71854c..5db57484 100644 --- a/Rom.py +++ b/Rom.py @@ -1097,7 +1097,9 @@ def patch_rom(world, player, rom, enemized): rom.write_byte(0x18005E, world.crystals_needed_for_gt[player]) rom.write_byte(0x18005F, world.crystals_needed_for_ganon[player]) - rom.write_byte(0x18008A, 0x01 if world.mode[player] == "standard" else 0x00) # block HC upstairs doors in rain state in standard mode + + # block HC upstairs doors in rain state in standard mode + rom.write_byte(0x18008A, 0x01 if world.mode[player] == "standard" and world.shuffle[player] != 'vanilla' else 0x00) rom.write_byte(0x18016A, 0x10 | ((0x01 if world.keyshuffle[player] else 0x00) | (0x02 if world.compassshuffle[player] else 0x00) From 239ea0f67c4b7a3bcbc77ec826bce41038892792 Mon Sep 17 00:00:00 2001 From: Bonta-kun <40473493+Bonta0@users.noreply.github.com> Date: Fri, 10 Jan 2020 07:25:16 +0100 Subject: [PATCH 19/20] outputpath: use makedirs instead of mkdir --- Main.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Main.py b/Main.py index 3b21c512..5d5b974a 100644 --- a/Main.py +++ b/Main.py @@ -24,10 +24,7 @@ __version__ = '0.6.3-pre' def main(args, seed=None): if args.outputpath: - try: - os.mkdir(args.outputpath) - except OSError: - pass + os.makedirs(args.outputpath, exist_ok=True) output_path.cached_path = args.outputpath start = time.process_time() From 2f5a3e24dda9bbee50019d3a30f5e8d7b2d096e6 Mon Sep 17 00:00:00 2001 From: Bonta-kun <40473493+Bonta0@users.noreply.github.com> Date: Fri, 10 Jan 2020 11:41:22 +0100 Subject: [PATCH 20/20] Small shops refactor, cleanup some inverted mess --- BaseClasses.py | 8 +- InvertedRegions.py | 309 +-------------------------------------------- ItemList.py | 25 ++-- Main.py | 7 +- Regions.py | 69 ++++------ Rom.py | 2 +- 6 files changed, 46 insertions(+), 374 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index ffdf060e..7e3740cc 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -898,14 +898,14 @@ class ShopType(Enum): UpgradeShop = 2 class Shop(object): - def __init__(self, region, room_id, type, shopkeeper_config, replaceable): + def __init__(self, region, room_id, type, shopkeeper_config, custom, locked): self.region = region self.room_id = room_id self.type = type self.inventory = [None, None, None] self.shopkeeper_config = shopkeeper_config - self.replaceable = replaceable - self.active = False + self.custom = custom + self.locked = locked @property def item_count(self): @@ -1013,7 +1013,7 @@ class Spoiler(object): self.shops = [] for shop in self.world.shops: - if not shop.active: + if not shop.custom: continue shopdata = {'location': str(shop.region), 'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop' diff --git a/InvertedRegions.py b/InvertedRegions.py index 81422609..cfbf5ed0 100644 --- a/InvertedRegions.py +++ b/InvertedRegions.py @@ -1,5 +1,6 @@ import collections -from BaseClasses import Region, Location, Entrance, RegionType, Shop, ShopType +from BaseClasses import RegionType +from Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region def create_inverted_regions(world, player): @@ -302,47 +303,8 @@ def create_inverted_regions(world, player): create_cave_region(player, 'The Sky', 'A Dark Sky', None, ['DDM Landing','NEDW Landing', 'WDW Landing', 'SDW Landing', 'EDW Landing', 'DD Landing', 'DLHL Landing']) ] - for region_name, (room_id, shopkeeper, replaceable) in shop_table.items(): - region = world.get_region(region_name, player) - shop = Shop(region, room_id, ShopType.Shop, shopkeeper, replaceable) - region.shop = shop - world.shops.append(shop) - for index, (item, price) in enumerate(default_shop_contents[region_name]): - shop.add_inventory(index, item, price) - - region = world.get_region('Capacity Upgrade', player) - shop = Shop(region, 0x0115, ShopType.UpgradeShop, 0x04, False) - region.shop = shop - world.shops.append(shop) - shop.add_inventory(0, 'Bomb Upgrade (+5)', 100, 7) - shop.add_inventory(1, 'Arrow Upgrade (+5)', 100, 7) world.initialize_regions() -def create_lw_region(player, name, locations=None, exits=None): - return _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits) - -def create_dw_region(player, name, locations=None, exits=None): - return _create_region(player, name, RegionType.DarkWorld, 'Dark World', locations, exits) - -def create_cave_region(player, name, hint='Hyrule', locations=None, exits=None): - return _create_region(player, name, RegionType.Cave, hint, locations, exits) - -def create_dungeon_region(player, name, hint='Hyrule', locations=None, exits=None): - return _create_region(player, name, RegionType.Dungeon, hint, locations, exits) - -def _create_region(player, name, type, hint='Hyrule', locations=None, exits=None): - ret = Region(name, type, hint, player) - if locations is None: - locations = [] - if exits is None: - exits = [] - - for exit in exits: - ret.exits.append(Entrance(player, exit, ret)) - for location in locations: - address, player_address, crystal, hint_text = location_table[location] - ret.locations.append(Location(player, location, address, crystal, hint_text, ret, player_address)) - return ret def mark_dark_world_regions(world, player): # cross world caves may have some sections marked as both in_light_world, and in_dark_work. @@ -372,270 +334,3 @@ def mark_dark_world_regions(world, player): if exit.connected_region not in seen: seen.add(exit.connected_region) queue.append(exit.connected_region) - -# (room_id, shopkeeper, replaceable) -shop_table = { - 'Cave Shop (Dark Death Mountain)': (0x0112, 0xC1, True), - 'Red Shield Shop': (0x0110, 0xC1, True), - 'Dark Lake Hylia Shop': (0x010F, 0xC1, False), - 'Dark World Lumberjack Shop': (0x010F, 0xC1, True), - 'Village of Outcasts Shop': (0x010F, 0xC1, True), - 'Dark World Potion Shop': (0x010F, 0xC1, True), - 'Light World Death Mountain Shop': (0x00FF, 0xA0, True), - 'Kakariko Shop': (0x011F, 0xA0, True), - 'Cave Shop (Lake Hylia)': (0x0112, 0xA0, True), - 'Potion Shop': (0x0109, 0xFF, False), - # Bomb Shop not currently modeled as a shop, due to special nature of items -} -# region, [item] -# slot, item, price, max=0, replacement=None, replacement_price=0 -# item = (item, price) - -_basic_shop_defaults = [('Red Potion', 150), ('Small Heart', 10), ('Bombs (10)', 50)] -_dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)] -default_shop_contents = { - 'Cave Shop (Dark Death Mountain)': _basic_shop_defaults, - 'Red Shield Shop': [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)], - 'Dark Lake Hylia Shop': [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)], - 'Dark World Lumberjack Shop': _dark_world_shop_defaults, - 'Village of Outcasts Shop': _dark_world_shop_defaults, - 'Dark World Potion Shop': _dark_world_shop_defaults, - 'Light World Death Mountain Shop': _basic_shop_defaults, - 'Kakariko Shop': _basic_shop_defaults, - 'Cave Shop (Lake Hylia)': _basic_shop_defaults, - 'Potion Shop': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)], -} - -location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'), - 'Bottle Merchant': (0x2eb18, 0x186339, False, 'with a merchant'), - 'Flute Spot': (0x18014a, 0x18633d, False, 'underground'), - 'Sunken Treasure': (0x180145, 0x186354, False, 'underwater'), - 'Purple Chest': (0x33d68, 0x186359, False, 'from a box'), - "Blind's Hideout - Top": (0xeb0f, 0x1862e3, False, 'in a basement'), - "Blind's Hideout - Left": (0xeb12, 0x1862e6, False, 'in a basement'), - "Blind's Hideout - Right": (0xeb15, 0x1862e9, False, 'in a basement'), - "Blind's Hideout - Far Left": (0xeb18, 0x1862ec, False, 'in a basement'), - "Blind's Hideout - Far Right": (0xeb1b, 0x1862ef, False, 'in a basement'), - "Link's Uncle": (0x2df45, 0x18635f, False, 'with your uncle'), - 'Secret Passage': (0xe971, 0x186145, False, 'near your uncle'), - 'King Zora': (0xee1c3, 0x186360, False, 'at a high price'), - "Zora's Ledge": (0x180149, 0x186358, False, 'near Zora'), - 'Waterfall Fairy - Left': (0xe9b0, 0x186184, False, 'near a fairy'), - 'Waterfall Fairy - Right': (0xe9d1, 0x1861a5, False, 'near a fairy'), - "King's Tomb": (0xe97a, 0x18614e, False, 'alone in a cave'), - 'Floodgate Chest': (0xe98c, 0x186160, False, 'in the dam'), - "Link's House": (0xe9bc, 0x186190, False, 'in your home'), - 'Kakariko Tavern': (0xe9ce, 0x1861a2, False, 'in the bar'), - 'Chicken House': (0xe9e9, 0x1861bd, False, 'near poultry'), - "Aginah's Cave": (0xe9f2, 0x1861c6, False, 'with Aginah'), - "Sahasrahla's Hut - Left": (0xea82, 0x186256, False, 'near the elder'), - "Sahasrahla's Hut - Middle": (0xea85, 0x186259, False, 'near the elder'), - "Sahasrahla's Hut - Right": (0xea88, 0x18625c, False, 'near the elder'), - 'Sahasrahla': (0x2f1fc, 0x186365, False, 'with the elder'), - 'Kakariko Well - Top': (0xea8e, 0x186262, False, 'in a well'), - 'Kakariko Well - Left': (0xea91, 0x186265, False, 'in a well'), - 'Kakariko Well - Middle': (0xea94, 0x186268, False, 'in a well'), - 'Kakariko Well - Right': (0xea97, 0x18626b, False, 'in a well'), - 'Kakariko Well - Bottom': (0xea9a, 0x18626e, False, 'in a well'), - 'Blacksmith': (0x18002a, 0x186366, False, 'with the smith'), - 'Magic Bat': (0x180015, 0x18635e, False, 'with the bat'), - 'Sick Kid': (0x339cf, 0x186367, False, 'with the sick'), - 'Hobo': (0x33e7d, 0x186368, False, 'with the hobo'), - 'Lost Woods Hideout': (0x180000, 0x186348, False, 'near a thief'), - 'Lumberjack Tree': (0x180001, 0x186349, False, 'in a hole'), - 'Cave 45': (0x180003, 0x18634b, False, 'alone in a cave'), - 'Graveyard Cave': (0x180004, 0x18634c, False, 'alone in a cave'), - 'Checkerboard Cave': (0x180005, 0x18634d, False, 'alone in a cave'), - 'Mini Moldorm Cave - Far Left': (0xeb42, 0x186316, False, 'near Moldorms'), - 'Mini Moldorm Cave - Left': (0xeb45, 0x186319, False, 'near Moldorms'), - 'Mini Moldorm Cave - Right': (0xeb48, 0x18631c, False, 'near Moldorms'), - 'Mini Moldorm Cave - Far Right': (0xeb4b, 0x18631f, False, 'near Moldorms'), - 'Mini Moldorm Cave - Generous Guy': (0x180010, 0x18635a, False, 'near Moldorms'), - 'Ice Rod Cave': (0xeb4e, 0x186322, False, 'in a frozen cave'), - 'Bonk Rock Cave': (0xeb3f, 0x186313, False, 'alone in a cave'), - 'Library': (0x180012, 0x18635c, False, 'near books'), - 'Potion Shop': (0x180014, 0x18635d, False, 'near potions'), - 'Lake Hylia Island': (0x180144, 0x186353, False, 'on an island'), - 'Maze Race': (0x180142, 0x186351, False, 'at the race'), - 'Desert Ledge': (0x180143, 0x186352, False, 'in the desert'), - 'Desert Palace - Big Chest': (0xe98f, 0x186163, False, 'in Desert Palace'), - 'Desert Palace - Torch': (0x180160, 0x186362, False, 'in Desert Palace'), - 'Desert Palace - Map Chest': (0xe9b6, 0x18618a, False, 'in Desert Palace'), - 'Desert Palace - Compass Chest': (0xe9cb, 0x18619f, False, 'in Desert Palace'), - 'Desert Palace - Big Key Chest': (0xe9c2, 0x186196, False, 'in Desert Palace'), - 'Desert Palace - Boss': (0x180151, 0x18633f, False, 'with Lanmolas'), - 'Eastern Palace - Compass Chest': (0xe977, 0x18614b, False, 'in Eastern Palace'), - 'Eastern Palace - Big Chest': (0xe97d, 0x186151, False, 'in Eastern Palace'), - 'Eastern Palace - Cannonball Chest': (0xe9b3, 0x186187, False, 'in Eastern Palace'), - 'Eastern Palace - Big Key Chest': (0xe9b9, 0x18618d, False, 'in Eastern Palace'), - 'Eastern Palace - Map Chest': (0xe9f5, 0x1861c9, False, 'in Eastern Palace'), - 'Eastern Palace - Boss': (0x180150, 0x18633e, False, 'with the Armos'), - 'Master Sword Pedestal': (0x289b0, 0x186369, False, 'at the pedestal'), - 'Hyrule Castle - Boomerang Chest': (0xe974, 0x186148, False, 'in Hyrule Castle'), - 'Hyrule Castle - Map Chest': (0xeb0c, 0x1862e0, False, 'in Hyrule Castle'), - "Hyrule Castle - Zelda's Chest": (0xeb09, 0x1862dd, False, 'in Hyrule Castle'), - 'Sewers - Dark Cross': (0xe96e, 0x186142, False, 'in the sewers'), - 'Sewers - Secret Room - Left': (0xeb5d, 0x186331, False, 'in the sewers'), - 'Sewers - Secret Room - Middle': (0xeb60, 0x186334, False, 'in the sewers'), - 'Sewers - Secret Room - Right': (0xeb63, 0x186337, False, 'in the sewers'), - 'Sanctuary': (0xea79, 0x18624d, False, 'in Sanctuary'), - 'Castle Tower - Room 03': (0xeab5, 0x186289, False, 'in Castle Tower'), - 'Castle Tower - Dark Maze': (0xeab2, 0x186286, False, 'in Castle Tower'), - 'Old Man': (0xf69fa, 0x186364, False, 'with the old man'), - 'Spectacle Rock Cave': (0x180002, 0x18634a, False, 'alone in a cave'), - 'Paradox Cave Lower - Far Left': (0xeb2a, 0x1862fe, False, 'in a cave with seven chests'), - 'Paradox Cave Lower - Left': (0xeb2d, 0x186301, False, 'in a cave with seven chests'), - 'Paradox Cave Lower - Right': (0xeb30, 0x186304, False, 'in a cave with seven chests'), - 'Paradox Cave Lower - Far Right': (0xeb33, 0x186307, False, 'in a cave with seven chests'), - 'Paradox Cave Lower - Middle': (0xeb36, 0x18630a, False, 'in a cave with seven chests'), - 'Paradox Cave Upper - Left': (0xeb39, 0x18630d, False, 'in a cave with seven chests'), - 'Paradox Cave Upper - Right': (0xeb3c, 0x186310, False, 'in a cave with seven chests'), - 'Spiral Cave': (0xe9bf, 0x186193, False, 'in spiral cave'), - 'Ether Tablet': (0x180016, 0x18633b, False, 'at a monolith'), - 'Spectacle Rock': (0x180140, 0x18634f, False, 'atop a rock'), - 'Tower of Hera - Basement Cage': (0x180162, 0x18633a, False, 'in Tower of Hera'), - 'Tower of Hera - Map Chest': (0xe9ad, 0x186181, False, 'in Tower of Hera'), - 'Tower of Hera - Big Key Chest': (0xe9e6, 0x1861ba, False, 'in Tower of Hera'), - 'Tower of Hera - Compass Chest': (0xe9fb, 0x1861cf, False, 'in Tower of Hera'), - 'Tower of Hera - Big Chest': (0xe9f8, 0x1861cc, False, 'in Tower of Hera'), - 'Tower of Hera - Boss': (0x180152, 0x186340, False, 'with Moldorm'), - 'Pyramid': (0x180147, 0x186356, False, 'on the pyramid'), - 'Catfish': (0xee185, 0x186361, False, 'with a catfish'), - 'Stumpy': (0x330c7, 0x18636a, False, 'with tree boy'), - 'Digging Game': (0x180148, 0x186357, False, 'underground'), - 'Bombos Tablet': (0x180017, 0x18633c, False, 'at a monolith'), - 'Hype Cave - Top': (0xeb1e, 0x1862f2, False, 'near a bat-like man'), - 'Hype Cave - Middle Right': (0xeb21, 0x1862f5, False, 'near a bat-like man'), - 'Hype Cave - Middle Left': (0xeb24, 0x1862f8, False, 'near a bat-like man'), - 'Hype Cave - Bottom': (0xeb27, 0x1862fb, False, 'near a bat-like man'), - 'Hype Cave - Generous Guy': (0x180011, 0x18635b, False, 'with a bat-like man'), - 'Peg Cave': (0x180006, 0x18634e, False, 'alone in a cave'), - 'Pyramid Fairy - Left': (0xe980, 0x186154, False, 'near a fairy'), - 'Pyramid Fairy - Right': (0xe983, 0x186157, False, 'near a fairy'), - 'Brewery': (0xe9ec, 0x1861c0, False, 'alone in a home'), - 'C-Shaped House': (0xe9ef, 0x1861c3, False, 'alone in a home'), - 'Chest Game': (0xeda8, 0x18636b, False, 'as a prize'), - 'Bumper Cave Ledge': (0x180146, 0x186355, False, 'on a ledge'), - 'Mire Shed - Left': (0xea73, 0x186247, False, 'near sparks'), - 'Mire Shed - Right': (0xea76, 0x18624a, False, 'near sparks'), - 'Superbunny Cave - Top': (0xea7c, 0x186250, False, 'in a connection'), - 'Superbunny Cave - Bottom': (0xea7f, 0x186253, False, 'in a connection'), - 'Spike Cave': (0xea8b, 0x18625f, False, 'beyond spikes'), - 'Hookshot Cave - Top Right': (0xeb51, 0x186325, False, 'across pits'), - 'Hookshot Cave - Top Left': (0xeb54, 0x186328, False, 'across pits'), - 'Hookshot Cave - Bottom Right': (0xeb5a, 0x18632e, False, 'across pits'), - 'Hookshot Cave - Bottom Left': (0xeb57, 0x18632b, False, 'across pits'), - 'Floating Island': (0x180141, 0x186350, False, 'on an island'), - 'Mimic Cave': (0xe9c5, 0x186199, False, 'in a cave of mimicry'), - 'Swamp Palace - Entrance': (0xea9d, 0x186271, False, 'in Swamp Palace'), - 'Swamp Palace - Map Chest': (0xe986, 0x18615a, False, 'in Swamp Palace'), - 'Swamp Palace - Big Chest': (0xe989, 0x18615d, False, 'in Swamp Palace'), - 'Swamp Palace - Compass Chest': (0xeaa0, 0x186274, False, 'in Swamp Palace'), - 'Swamp Palace - Big Key Chest': (0xeaa6, 0x18627a, False, 'in Swamp Palace'), - 'Swamp Palace - West Chest': (0xeaa3, 0x186277, False, 'in Swamp Palace'), - 'Swamp Palace - Flooded Room - Left': (0xeaa9, 0x18627d, False, 'in Swamp Palace'), - 'Swamp Palace - Flooded Room - Right': (0xeaac, 0x186280, False, 'in Swamp Palace'), - 'Swamp Palace - Waterfall Room': (0xeaaf, 0x186283, False, 'in Swamp Palace'), - 'Swamp Palace - Boss': (0x180154, 0x186342, False, 'with Arrghus'), - "Thieves' Town - Big Key Chest": (0xea04, 0x1861d8, False, "in Thieves' Town"), - "Thieves' Town - Map Chest": (0xea01, 0x1861d5, False, "in Thieves' Town"), - "Thieves' Town - Compass Chest": (0xea07, 0x1861db, False, "in Thieves' Town"), - "Thieves' Town - Ambush Chest": (0xea0a, 0x1861de, False, "in Thieves' Town"), - "Thieves' Town - Attic": (0xea0d, 0x1861e1, False, "in Thieves' Town"), - "Thieves' Town - Big Chest": (0xea10, 0x1861e4, False, "in Thieves' Town"), - "Thieves' Town - Blind's Cell": (0xea13, 0x1861e7, False, "in Thieves' Town"), - "Thieves' Town - Boss": (0x180156, 0x186344, False, 'with Blind'), - 'Skull Woods - Compass Chest': (0xe992, 0x186166, False, 'in Skull Woods'), - 'Skull Woods - Map Chest': (0xe99b, 0x18616f, False, 'in Skull Woods'), - 'Skull Woods - Big Chest': (0xe998, 0x18616c, False, 'in Skull Woods'), - 'Skull Woods - Pot Prison': (0xe9a1, 0x186175, False, 'in Skull Woods'), - 'Skull Woods - Pinball Room': (0xe9c8, 0x18619c, False, 'in Skull Woods'), - 'Skull Woods - Big Key Chest': (0xe99e, 0x186172, False, 'in Skull Woods'), - 'Skull Woods - Bridge Room': (0xe9fe, 0x1861d2, False, 'near Mothula'), - 'Skull Woods - Boss': (0x180155, 0x186343, False, 'with Mothula'), - 'Ice Palace - Compass Chest': (0xe9d4, 0x1861a8, False, 'in Ice Palace'), - 'Ice Palace - Freezor Chest': (0xe995, 0x186169, False, 'in Ice Palace'), - 'Ice Palace - Big Chest': (0xe9aa, 0x18617e, False, 'in Ice Palace'), - 'Ice Palace - Iced T Room': (0xe9e3, 0x1861b7, False, 'in Ice Palace'), - 'Ice Palace - Spike Room': (0xe9e0, 0x1861b4, False, 'in Ice Palace'), - 'Ice Palace - Big Key Chest': (0xe9a4, 0x186178, False, 'in Ice Palace'), - 'Ice Palace - Map Chest': (0xe9dd, 0x1861b1, False, 'in Ice Palace'), - 'Ice Palace - Boss': (0x180157, 0x186345, False, 'with Kholdstare'), - 'Misery Mire - Big Chest': (0xea67, 0x18623b, False, 'in Misery Mire'), - 'Misery Mire - Map Chest': (0xea6a, 0x18623e, False, 'in Misery Mire'), - 'Misery Mire - Main Lobby': (0xea5e, 0x186232, False, 'in Misery Mire'), - 'Misery Mire - Bridge Chest': (0xea61, 0x186235, False, 'in Misery Mire'), - 'Misery Mire - Spike Chest': (0xe9da, 0x1861ae, False, 'in Misery Mire'), - 'Misery Mire - Compass Chest': (0xea64, 0x186238, False, 'in Misery Mire'), - 'Misery Mire - Big Key Chest': (0xea6d, 0x186241, False, 'in Misery Mire'), - 'Misery Mire - Boss': (0x180158, 0x186346, False, 'with Vitreous'), - 'Turtle Rock - Compass Chest': (0xea22, 0x1861f6, False, 'in Turtle Rock'), - 'Turtle Rock - Roller Room - Left': (0xea1c, 0x1861f0, False, 'in Turtle Rock'), - 'Turtle Rock - Roller Room - Right': (0xea1f, 0x1861f3, False, 'in Turtle Rock'), - 'Turtle Rock - Chain Chomps': (0xea16, 0x1861ea, False, 'in Turtle Rock'), - 'Turtle Rock - Big Key Chest': (0xea25, 0x1861f9, False, 'in Turtle Rock'), - 'Turtle Rock - Big Chest': (0xea19, 0x1861ed, False, 'in Turtle Rock'), - 'Turtle Rock - Crystaroller Room': (0xea34, 0x186208, False, 'in Turtle Rock'), - 'Turtle Rock - Eye Bridge - Bottom Left': (0xea31, 0x186205, False, 'in Turtle Rock'), - 'Turtle Rock - Eye Bridge - Bottom Right': (0xea2e, 0x186202, False, 'in Turtle Rock'), - 'Turtle Rock - Eye Bridge - Top Left': (0xea2b, 0x1861ff, False, 'in Turtle Rock'), - 'Turtle Rock - Eye Bridge - Top Right': (0xea28, 0x1861fc, False, 'in Turtle Rock'), - 'Turtle Rock - Boss': (0x180159, 0x186347, False, 'with Trinexx'), - 'Palace of Darkness - Shooter Room': (0xea5b, 0x18622f, False, 'in Palace of Darkness'), - 'Palace of Darkness - The Arena - Bridge': (0xea3d, 0x186211, False, 'in Palace of Darkness'), - 'Palace of Darkness - Stalfos Basement': (0xea49, 0x18621d, False, 'in Palace of Darkness'), - 'Palace of Darkness - Big Key Chest': (0xea37, 0x18620b, False, 'in Palace of Darkness'), - 'Palace of Darkness - The Arena - Ledge': (0xea3a, 0x18620e, False, 'in Palace of Darkness'), - 'Palace of Darkness - Map Chest': (0xea52, 0x186226, False, 'in Palace of Darkness'), - 'Palace of Darkness - Compass Chest': (0xea43, 0x186217, False, 'in Palace of Darkness'), - 'Palace of Darkness - Dark Basement - Left': (0xea4c, 0x186220, False, 'in Palace of Darkness'), - 'Palace of Darkness - Dark Basement - Right': (0xea4f, 0x186223, False, 'in Palace of Darkness'), - 'Palace of Darkness - Dark Maze - Top': (0xea55, 0x186229, False, 'in Palace of Darkness'), - 'Palace of Darkness - Dark Maze - Bottom': (0xea58, 0x18622c, False, 'in Palace of Darkness'), - 'Palace of Darkness - Big Chest': (0xea40, 0x186214, False, 'in Palace of Darkness'), - 'Palace of Darkness - Harmless Hellway': (0xea46, 0x18621a, False, 'in Palace of Darkness'), - 'Palace of Darkness - Boss': (0x180153, 0x186341, False, 'with Helmasaur King'), - "Ganons Tower - Bob's Torch": (0x180161, 0x186363, False, "in Ganon's Tower"), - 'Ganons Tower - Hope Room - Left': (0xead9, 0x1862ad, False, "in Ganon's Tower"), - 'Ganons Tower - Hope Room - Right': (0xeadc, 0x1862b0, False, "in Ganon's Tower"), - 'Ganons Tower - Tile Room': (0xeae2, 0x1862b6, False, "in Ganon's Tower"), - 'Ganons Tower - Compass Room - Top Left': (0xeae5, 0x1862b9, False, "in Ganon's Tower"), - 'Ganons Tower - Compass Room - Top Right': (0xeae8, 0x1862bc, False, "in Ganon's Tower"), - 'Ganons Tower - Compass Room - Bottom Left': (0xeaeb, 0x1862bf, False, "in Ganon's Tower"), - 'Ganons Tower - Compass Room - Bottom Right': (0xeaee, 0x1862c2, False, "in Ganon's Tower"), - 'Ganons Tower - DMs Room - Top Left': (0xeab8, 0x18628c, False, "in Ganon's Tower"), - 'Ganons Tower - DMs Room - Top Right': (0xeabb, 0x18628f, False, "in Ganon's Tower"), - 'Ganons Tower - DMs Room - Bottom Left': (0xeabe, 0x186292, False, "in Ganon's Tower"), - 'Ganons Tower - DMs Room - Bottom Right': (0xeac1, 0x186295, False, "in Ganon's Tower"), - 'Ganons Tower - Map Chest': (0xead3, 0x1862a7, False, "in Ganon's Tower"), - 'Ganons Tower - Firesnake Room': (0xead0, 0x1862a4, False, "in Ganon's Tower"), - 'Ganons Tower - Randomizer Room - Top Left': (0xeac4, 0x186298, False, "in Ganon's Tower"), - 'Ganons Tower - Randomizer Room - Top Right': (0xeac7, 0x18629b, False, "in Ganon's Tower"), - 'Ganons Tower - Randomizer Room - Bottom Left': (0xeaca, 0x18629e, False, "in Ganon's Tower"), - 'Ganons Tower - Randomizer Room - Bottom Right': (0xeacd, 0x1862a1, False, "in Ganon's Tower"), - "Ganons Tower - Bob's Chest": (0xeadf, 0x1862b3, False, "in Ganon's Tower"), - 'Ganons Tower - Big Chest': (0xead6, 0x1862aa, False, "in Ganon's Tower"), - 'Ganons Tower - Big Key Room - Left': (0xeaf4, 0x1862c8, False, "in Ganon's Tower"), - 'Ganons Tower - Big Key Room - Right': (0xeaf7, 0x1862cb, False, "in Ganon's Tower"), - 'Ganons Tower - Big Key Chest': (0xeaf1, 0x1862c5, False, "in Ganon's Tower"), - 'Ganons Tower - Mini Helmasaur Room - Left': (0xeafd, 0x1862d1, False, "atop Ganon's Tower"), - 'Ganons Tower - Mini Helmasaur Room - Right': (0xeb00, 0x1862d4, False, "atop Ganon's Tower"), - 'Ganons Tower - Pre-Moldorm Chest': (0xeb03, 0x1862d7, False, "atop Ganon's Tower"), - 'Ganons Tower - Validation Chest': (0xeb06, 0x1862da, False, "atop Ganon's Tower"), - 'Ganon': (None, None, False, 'from me'), - 'Agahnim 1': (None, None, False, 'from Ganon\'s wizardry form'), - 'Agahnim 2': (None, None, False, 'from Ganon\'s wizardry form'), - 'Floodgate': (None, None, False, None), - 'Frog': (None, None, False, None), - 'Missing Smith': (None, None, False, None), - 'Dark Blacksmith Ruins': (None, None, False, None), - 'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], None, True, 'Eastern Palace'), - 'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], None, True, 'Desert Palace'), - 'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], None, True, 'Tower of Hera'), - 'Palace of Darkness - Prize': ([0x120A1, 0x53F00, 0x53F01, 0x180056, 0x18007D, 0xC702], None, True, 'Palace of Darkness'), - 'Swamp Palace - Prize': ([0x120A0, 0x53F6C, 0x53F6D, 0x180055, 0x180071, 0xC701], None, True, 'Swamp Palace'), - 'Thieves\' Town - Prize': ([0x120A6, 0x53F36, 0x53F37, 0x18005B, 0x180077, 0xC707], None, True, 'Thieves\' Town'), - 'Skull Woods - Prize': ([0x120A3, 0x53F12, 0x53F13, 0x180058, 0x18007B, 0xC704], None, True, 'Skull Woods'), - 'Ice Palace - Prize': ([0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], None, True, 'Ice Palace'), - 'Misery Mire - Prize': ([0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], None, True, 'Misery Mire'), - 'Turtle Rock - Prize': ([0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')} diff --git a/ItemList.py b/ItemList.py index 73e40043..7355a47f 100644 --- a/ItemList.py +++ b/ItemList.py @@ -269,7 +269,7 @@ take_any_locations = [ 'Bonk Fairy (Dark)', 'Lake Hylia Healer Fairy', 'Swamp Healer Fairy', 'Desert Healer Fairy', 'Dark Lake Hylia Healer Fairy', 'Dark Lake Hylia Ledge Healer Fairy', 'Dark Desert Healer Fairy', 'Dark Death Mountain Healer Fairy', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', - 'Kakariko Gamble Game', 'Capacity Upgrade', '50 Rupee Cave', 'Lost Woods Gamble', 'Hookshot Fairy', + 'Kakariko Gamble Game', '50 Rupee Cave', 'Lost Woods Gamble', 'Hookshot Fairy', 'Palace of Darkness Hint', 'East Dark World Hint', 'Archery Game', 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Spike Cave', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', 'Dark Desert Hint'] @@ -287,9 +287,8 @@ def set_up_take_anys(world, player): entrance = world.get_region(reg, player).entrances[0] connect_entrance(world, entrance, old_man_take_any, player) entrance.target = 0x58 - old_man_take_any.shop = Shop(old_man_take_any, 0x0112, ShopType.TakeAny, 0xE2, True) + old_man_take_any.shop = Shop(old_man_take_any, 0x0112, ShopType.TakeAny, 0xE2, True, True) world.shops.append(old_man_take_any.shop) - old_man_take_any.shop.active = True swords = [item for item in world.itempool if item.type == 'Sword' and item.player == player] if swords: @@ -310,9 +309,8 @@ def set_up_take_anys(world, player): entrance = world.get_region(reg, player).entrances[0] connect_entrance(world, entrance, take_any, player) entrance.target = target - take_any.shop = Shop(take_any, room_id, ShopType.TakeAny, 0xE3, True) + take_any.shop = Shop(take_any, room_id, ShopType.TakeAny, 0xE3, True, True) world.shops.append(take_any.shop) - take_any.shop.active = True take_any.shop.add_inventory(0, 'Blue Potion', 0, 0) take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0) @@ -365,26 +363,19 @@ def fill_prizes(world, attempts=15): def set_up_shops(world, player): - # Changes to basic Shops # TODO: move hard+ mode changes for sheilds here, utilizing the new shops - for shop in world.shops: - shop.active = True - if world.retro[player]: rss = world.get_region('Red Shield Shop', player).shop - rss.active = True - rss.add_inventory(2, 'Single Arrow', 80) - - # Randomized changes to Shops - if world.retro[player]: - for shop in random.sample([s for s in world.shops if s.replaceable and s.region.player == player], 5): - shop.active = True + if not rss.locked: + rss.add_inventory(2, 'Single Arrow', 80) + for shop in random.sample([s for s in world.shops if s.custom and not s.locked and s.region.player == player], 5): + shop.locked = True shop.add_inventory(0, 'Single Arrow', 80) shop.add_inventory(1, 'Small Key (Universal)', 100) shop.add_inventory(2, 'Bombs (10)', 50) + rss.locked = True - #special shop types def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, retro): pool = [] diff --git a/Main.py b/Main.py index 5d5b974a..1fb0f11b 100644 --- a/Main.py +++ b/Main.py @@ -10,7 +10,7 @@ import zlib from BaseClasses import World, CollectionState, Item, Region, Location, Shop from Items import ItemFactory -from Regions import create_regions, mark_light_world_regions +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 @@ -71,6 +71,7 @@ def main(args, seed=None): create_regions(world, player) else: create_inverted_regions(world, player) + create_shops(world, player) create_dungeons(world, player) logger.info('Shuffling the World about.') @@ -251,6 +252,7 @@ def copy_world(world): create_regions(ret, player) else: create_inverted_regions(ret, player) + create_shops(ret, player) create_dungeons(ret, player) copy_dynamic_regions_and_locations(world, ret) @@ -262,7 +264,6 @@ def copy_world(world): for shop in world.shops: copied_shop = ret.get_region(shop.region.name, shop.region.player).shop - copied_shop.active = shop.active copied_shop.inventory = copy.copy(shop.inventory) # connect copied world @@ -308,7 +309,7 @@ def copy_dynamic_regions_and_locations(world, ret): # Note: ideally exits should be copied here, but the current use case (Take anys) do not require this if region.shop: - new_reg.shop = Shop(new_reg, region.shop.room_id, region.shop.type, region.shop.shopkeeper_config, region.shop.replaceable) + new_reg.shop = Shop(new_reg, region.shop.room_id, region.shop.type, region.shop.shopkeeper_config, region.shop.custom, region.shop.locked) ret.shops.append(new_reg.shop) for location in world.dynamic_locations: diff --git a/Regions.py b/Regions.py index 93959edf..e46b21c1 100644 --- a/Regions.py +++ b/Regions.py @@ -293,22 +293,9 @@ def create_regions(world, player): create_dw_region(player, 'Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop']) ] - for region_name, (room_id, shopkeeper, replaceable) in shop_table.items(): - region = world.get_region(region_name, player) - shop = Shop(region, room_id, ShopType.Shop, shopkeeper, replaceable) - region.shop = shop - world.shops.append(shop) - for index, (item, price) in enumerate(default_shop_contents[region_name]): - shop.add_inventory(index, item, price) - - region = world.get_region('Capacity Upgrade', player) - shop = Shop(region, 0x0115, ShopType.UpgradeShop, 0x04, False) - region.shop = shop - world.shops.append(shop) - shop.add_inventory(0, 'Bomb Upgrade (+5)', 100, 7) - shop.add_inventory(1, 'Arrow Upgrade (+5)', 100, 7) world.initialize_regions() + def create_lw_region(player, name, locations=None, exits=None): return _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits) @@ -364,37 +351,35 @@ def mark_light_world_regions(world, player): seen.add(exit.connected_region) queue.append(exit.connected_region) -# (room_id, shopkeeper, replaceable) -shop_table = { - 'Cave Shop (Dark Death Mountain)': (0x0112, 0xC1, True), - 'Red Shield Shop': (0x0110, 0xC1, True), - 'Dark Lake Hylia Shop': (0x010F, 0xC1, True), - 'Dark World Lumberjack Shop': (0x010F, 0xC1, True), - 'Village of Outcasts Shop': (0x010F, 0xC1, True), - 'Dark World Potion Shop': (0x010F, 0xC1, True), - 'Light World Death Mountain Shop': (0x00FF, 0xA0, True), - 'Kakariko Shop': (0x011F, 0xA0, True), - 'Cave Shop (Lake Hylia)': (0x0112, 0xA0, True), - 'Potion Shop': (0x0109, 0xFF, False), - # Bomb Shop not currently modeled as a shop, due to special nature of items -} -# region, [item] -# slot, item, price, max=0, replacement=None, replacement_price=0 -# item = (item, price) +def create_shops(world, player): + for region_name, (room_id, type, shopkeeper, custom, locked, inventory) in shop_table.items(): + if world.mode[player] == 'inverted' and region_name == 'Dark Lake Hylia Shop': + locked = True + inventory = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)] + region = world.get_region(region_name, player) + shop = Shop(region, room_id, type, shopkeeper, custom, locked) + region.shop = shop + world.shops.append(shop) + for index, item in enumerate(inventory): + shop.add_inventory(index, *item) + +# (type, room_id, shopkeeper, custom, locked, [items]) +# item = (item, price, max=0, replacement=None, replacement_price=0) _basic_shop_defaults = [('Red Potion', 150), ('Small Heart', 10), ('Bombs (10)', 50)] _dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)] -default_shop_contents = { - 'Cave Shop (Dark Death Mountain)': _basic_shop_defaults, - 'Red Shield Shop': [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)], - 'Dark Lake Hylia Shop': _dark_world_shop_defaults, - 'Dark World Lumberjack Shop': _dark_world_shop_defaults, - 'Village of Outcasts Shop': _dark_world_shop_defaults, - 'Dark World Potion Shop': _dark_world_shop_defaults, - 'Light World Death Mountain Shop': _basic_shop_defaults, - 'Kakariko Shop': _basic_shop_defaults, - 'Cave Shop (Lake Hylia)': _basic_shop_defaults, - 'Potion Shop': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)], +shop_table = { + 'Cave Shop (Dark Death Mountain)': (0x0112, ShopType.Shop, 0xC1, True, False, _basic_shop_defaults), + 'Red Shield Shop': (0x0110, ShopType.Shop, 0xC1, True, False, [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)]), + 'Dark Lake Hylia Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults), + 'Dark World Lumberjack Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults), + 'Village of Outcasts Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults), + 'Dark World Potion Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults), + 'Light World Death Mountain Shop': (0x00FF, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults), + 'Kakariko Shop': (0x011F, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults), + 'Cave Shop (Lake Hylia)': (0x0112, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults), + 'Potion Shop': (0x0109, ShopType.Shop, 0xFF, False, True, [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)]), + 'Capacity Upgrade': (0x0115, ShopType.UpgradeShop, 0x04, True, True, [('Bomb Upgrade (+5)', 100, 7), ('Arrow Upgrade (+5)', 100, 7)]) } location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'), diff --git a/Rom.py b/Rom.py index 5db57484..7ad87f63 100644 --- a/Rom.py +++ b/Rom.py @@ -1255,7 +1255,7 @@ def patch_race_rom(rom): RaceRom.encrypt(rom) def write_custom_shops(rom, world, player): - shops = [shop for shop in world.shops if shop.replaceable and shop.active and shop.region.player == player] + shops = [shop for shop in world.shops if shop.custom and shop.region.player == player] shop_data = bytearray() items_data = bytearray()