diff --git a/BaseClasses.py b/BaseClasses.py index de3ad469..9f1b3706 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1519,7 +1519,10 @@ class CollectionState(object): if not item: return changed = False - if item.name.startswith('Progressive '): + if item.name == "Sword and Shield": + self.prog_items["Fighter Sword", item.player] += 1 + self.prog_items["Blue Shield", item.player] += 1 + elif item.name.startswith('Progressive '): if 'Sword' in item.name: if self.has('Golden Sword', item.player): pass @@ -2895,6 +2898,10 @@ class Item(object): def compass(self): return self.type == 'Compass' + @property + def event(self): + return self.type == 'Event' + @property def dungeon(self): if not self.smallkey and not self.bigkey and not self.map and not self.compass: diff --git a/CLI.py b/CLI.py index 8711f649..889ca73d 100644 --- a/CLI.py +++ b/CLI.py @@ -89,6 +89,7 @@ def parse_cli(argv, no_defaults=False): parser.add_argument('--count', default=defval(int(settings["count"]) if settings["count"] != "" and settings["count"] is not None else 1), help="\n".join(fish.translate("cli", "help", "count")), type=int) parser.add_argument('--tries', default=defval(int(settings["tries"]) if settings["tries"] != "" and settings["tries"] is not None else 1), help="\n".join(fish.translate("cli", "help", "tries")), type=int) parser.add_argument('--customitemarray', default={}, help=argparse.SUPPRESS) + parser.add_argument('--skip_money_balance', action="store_true", help=argparse.SUPPRESS) # included for backwards compatibility parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255)) @@ -106,7 +107,14 @@ def parse_cli(argv, no_defaults=False): ret = parser.parse_args(argv) if ret.keysanity: - ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = ['wild'] * 4 + if ret.mapshuffle == "none": + ret.mapshuffle = "wild" + if ret.compassshuffle == "none": + ret.compassshuffle = "wild" + if ret.keyshuffle == "none": + ret.keyshuffle = "wild" + if ret.bigkeyshuffle == "none": + ret.bigkeyshuffle = "wild" if ret.keydropshuffle: ret.dropshuffle = 'keys' if ret.dropshuffle == 'none' else ret.dropshuffle @@ -178,7 +186,7 @@ def parse_cli(argv, no_defaults=False): 'decoupledoors', 'door_type_mode', 'bonk_drops', 'trap_door_mode', 'key_logic_algorithm', 'door_self_loops', 'any_enemy_logic', 'aga_randomness', - 'money_balance']: + 'money_balance', 'patches']: 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/Fill.py b/Fill.py index 2b920c9f..35c19d7d 100644 --- a/Fill.py +++ b/Fill.py @@ -252,6 +252,7 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl or (world.algorithm == 'vanilla_fill' and item_to_place.is_near_dungeon_item(world)))) \ or valid_dungeon_placement(item_to_place, location, world): return location + if item_to_place.smallkey or item_to_place.bigkey or item_to_place.prize: location.item = None location.event = False @@ -311,7 +312,9 @@ def valid_dungeon_placement(item, location, world): dungeon = check_dungeon if dungeon: layout = world.dungeon_layouts[location.player][dungeon.name] - if not is_dungeon_item(item, world) or item.player != location.player: + if item.event: + return True + elif not is_dungeon_item(item, world) or item.player != location.player: if item.prize and item.is_near_dungeon_item(world): return item.dungeon_object == dungeon and layout.free_items > 0 return layout.free_items > 0 diff --git a/ItemList.py b/ItemList.py index 7ccead35..0d2ebebf 100644 --- a/ItemList.py +++ b/ItemList.py @@ -372,6 +372,13 @@ def generate_itempool(world, player): world.push_precollected(ItemFactory(item, player)) if world.mode[player] == 'standard' and not world.state.has_blunt_weapon(player): + if world.customizer: + placements = world.customizer.get_placements() + if placements: + custom_uncle = placements.get(player, {}).get("Link's Uncle") + if custom_uncle: + placed_items["Link's Uncle"] = custom_uncle + if "Link's Uncle" not in placed_items: found_sword = False found_bow = False @@ -1669,12 +1676,15 @@ def fill_specific_items(world): item_to_place, event_flag = get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_pool) if item_to_place: - world.push_item(loc, item_to_place, False) - loc.locked = True - track_outside_keys(item_to_place, loc, world) - track_dungeon_items(item_to_place, loc, world) - loc.event = (event_flag or item_to_place.advancement - or item_to_place.bigkey or item_to_place.smallkey) + if not loc.item: + world.push_item(loc, item_to_place, False) + loc.locked = True + track_outside_keys(item_to_place, loc, world) + track_dungeon_items(item_to_place, loc, world) + loc.event = (event_flag or item_to_place.advancement + or item_to_place.bigkey or item_to_place.smallkey) + elif loc.item != item_to_place: + logging.getLogger('').warning("Failed to place item %s at location %s because it already contained %s", item_to_place, loc.name, loc.item) else: raise Exception(f'Did not find "{item}" in item pool to place at "{location}"') advanced_placements = world.customizer.get_advanced_placements() @@ -1802,7 +1812,7 @@ def shuffle_event_items(world, player): continue break else: - raise FillError(f'Unable to place followers: {", ".join(list(map(lambda d: d.hint_text, follower_locations)))}') + raise FillError(f'Unable to place followers: {", ".join(list(map(lambda f: f.name, pickup_items)))}') def get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_pool): diff --git a/Items.py b/Items.py index ec6a18b8..f9323e46 100644 --- a/Items.py +++ b/Items.py @@ -55,7 +55,7 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'Bow!\nJoin the archer class 'Master Sword': (True, False, 'Sword', 0x50, 100, 'Master Sword!\nEvil\'s bane!', 'and the master sword', 'sword-wielding kid', 'glow sword for sale', 'fungus for blue slasher', 'sword boy fights again', 'the Master Sword'), 'Tempered Sword': (True, False, 'Sword', 0x02, 150, 'Tempered Sword!\nMore slashy!', 'the tempered sword', 'sword-wielding kid', 'flame sword for sale', 'fungus for red slasher', 'sword boy fights again', 'the Tempered Sword'), 'Fighter Sword': (True, False, 'Sword', 0x49, 50, 'Fighter Sword!\nStarter level slashy!', 'the tiny sword', 'sword-wielding kid', 'tiny sword for sale', 'fungus for tiny slasher', 'sword boy fights again', 'the Fighter Sword'), - 'Sword and Shield': (True, False, 'Sword', 0x00, 'Sword and Shield!\nUncle sword ahoy!', 'the sword and shield', 'sword and shield-wielding kid', 'training set for sale', 'fungus for training set', 'sword and shield boy fights again', 'the small sword and shield'), + 'Sword and Shield': (True, False, 'Sword', 0x00, 50, 'Sword and Shield!\nUncle sword ahoy!', 'the sword and shield', 'sword and shield-wielding kid', 'training set for sale', 'fungus for training set', 'sword and shield boy fights again', 'the small sword and shield'), 'Golden Sword': (True, False, 'Sword', 0x03, 200, 'Golden Sword!\nBest slashy!', 'and the butter sword', 'sword-wielding kid', 'butter for sale', 'cap churned to butter', 'sword boy fights again', 'the Golden Sword'), 'Progressive Sword': (True, False, 'Sword', 0x5E, 150, 'Sword!\nA better sword for your time!', 'the unknown sword', 'sword-wielding kid', 'sword for sale', 'fungus for some slasher', 'sword boy fights again', 'a Sword'), 'Progressive Glove': (True, False, None, 0x61, 150, 'Glove!\nLift more than you can now!', 'and the lift upgrade', 'body-building kid', 'some glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'a Glove'), diff --git a/Main.py b/Main.py index f2c19eff..f19c75d9 100644 --- a/Main.py +++ b/Main.py @@ -67,6 +67,7 @@ from Rom import ( JsonRom, LocalRom, apply_rom_settings, + apply_rom_patches, get_hash_string, patch_race_rom, patch_rom, @@ -335,7 +336,7 @@ def main(args, seed=None, fish=None): for player in range(1, world.players+1): if world.shopsanity[player]: customize_shops(world, player) - if args.algorithm in ['balanced', 'equitable']: + if not args.skip_money_balance and args.algorithm in ['balanced', 'equitable']: balance_money_progression(world) ensure_good_items(world, True) @@ -364,6 +365,8 @@ def main(args, seed=None, fish=None): rom_names.append((player, team, list(rom.name))) world.spoiler.hashes[(player, team)] = get_hash_string(rom.hash) + if args.patches[player]: + apply_rom_patches(rom, map(lambda arg: arg.strip(), args.patches[player].split(","))) apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player], args.fastmenu[player], args.disablemusic[player], args.sprite[player], args.triforce_gfx[player], args.ow_palettes[player], args.uw_palettes[player], args.reduce_flashing[player], @@ -572,7 +575,7 @@ def init_world(args, fish): world.collection_rate = args.collection_rate.copy() world.colorizepots = args.colorizepots.copy() world.aga_randomness = args.aga_randomness.copy() - world.money_balance = args.money_balance.copy() + world.money_balance = {player: int(args.money_balance[player]) for player in range(1, world.players + 1)} # custom settings - these haven't been promoted to full settings yet in_progress_settings = ['force_enemy'] diff --git a/Rom.py b/Rom.py index e3821adc..f4393283 100644 --- a/Rom.py +++ b/Rom.py @@ -994,7 +994,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): set_inverted_mode(world, player, rom, inverted_buffer) uncle_location = world.get_location('Link\'s Uncle', player) - if uncle_location.item is None or uncle_location.item.name not in ['Master Sword', 'Tempered Sword', 'Fighter Sword', 'Golden Sword', 'Progressive Sword']: + if uncle_location.item is None or uncle_location.item.name not in ['Master Sword', 'Tempered Sword', 'Fighter Sword', 'Golden Sword', 'Progressive Sword', 'Sword and Shield']: # disable sword sprite from uncle rom.write_bytes(0x6D263, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) rom.write_bytes(0x6D26B, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) @@ -1779,14 +1779,15 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): rom.write_byte(0x180358, 0x01 if glitches_enabled else 0x00) rom.write_byte(0x18008B, 0x01 if glitches_enabled else 0x00) - # remove shield from uncle - rom.write_bytes(0x6D253, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) - rom.write_bytes(0x6D25B, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) - rom.write_bytes(0x6D283, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) - rom.write_bytes(0x6D28B, [0x00, 0x00, 0xf7, 0xff, 0x00, 0x0E]) - rom.write_bytes(0x6D2CB, [0x00, 0x00, 0xf6, 0xff, 0x02, 0x0E]) - rom.write_bytes(0x6D2FB, [0x00, 0x00, 0xf7, 0xff, 0x02, 0x0E]) - rom.write_bytes(0x6D313, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E]) + if uncle_location.item is None or uncle_location.item.name not in ['Sword and Shield']: + # remove shield from uncle + rom.write_bytes(0x6D253, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) + rom.write_bytes(0x6D25B, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) + rom.write_bytes(0x6D283, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) + rom.write_bytes(0x6D28B, [0x00, 0x00, 0xf7, 0xff, 0x00, 0x0E]) + rom.write_bytes(0x6D2CB, [0x00, 0x00, 0xf6, 0xff, 0x02, 0x0E]) + rom.write_bytes(0x6D2FB, [0x00, 0x00, 0xf7, 0xff, 0x02, 0x0E]) + rom.write_bytes(0x6D313, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E]) write_int16(rom, 0x180183, 0) # Escape fill rupee bow # Uncle / Zelda / Mantle respawn refills (magic, bombs, arrows) @@ -2074,6 +2075,49 @@ def hud_format_text(text): output += b'\x7f\x00' return output[:32] +def read_bytes(f, count): + values = f.read(count) + if len(values) < count: + raise EOFError + return values + +def apply_rom_patches(rom, patches): + for patch in patches: + if not os.path.exists(f"patches/{patch}.ips"): + logging.getLogger('').warning("Patch %s not found -- skipping", patch) + continue + + with open(f"patches/{patch}.ips", "rb") as f: + byte_changes = [] + try: + header = read_bytes(f, 5) + if header != b"PATCH": + logging.getLogger('').warning("Patch %s invalid -- skipping", patch) + continue + + while True: + address = read_bytes(f, 3) + if address == b"EOF": + break + + address = int.from_bytes(address, byteorder="big") + length = int.from_bytes(read_bytes(f, 2), byteorder="big") + + if length > 0: + byte_changes.append((address, list(f.read(length)))) + else: + length = int.from_bytes(read_bytes(f, 2), byteorder="big") + value = read_bytes(f, 1) + byte_changes.append((address, length * list(value))) + + for address, values in byte_changes: + rom.write_bytes(address, values) + + logging.getLogger("").info("Patch %s applied successfully", patch) + + except EOFError: + logging.getLogger('').warning("Patch %s invalid -- skipping", patch) + continue def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite, triforce_gfx, ow_palettes, uw_palettes, reduce_flashing, shuffle_sfx, diff --git a/patches/all_starting.ips b/patches/all_starting.ips new file mode 100644 index 00000000..d3333b8e Binary files /dev/null and b/patches/all_starting.ips differ diff --git a/patches/no_dungeon_item_popups.ips b/patches/no_dungeon_item_popups.ips new file mode 100644 index 00000000..b9865236 Binary files /dev/null and b/patches/no_dungeon_item_popups.ips differ diff --git a/patches/no_escape_assist.ips b/patches/no_escape_assist.ips new file mode 100644 index 00000000..2dfd97c5 Binary files /dev/null and b/patches/no_escape_assist.ips differ diff --git a/patches/pod_key_swap.ips b/patches/pod_key_swap.ips new file mode 100644 index 00000000..ee72cd70 Binary files /dev/null and b/patches/pod_key_swap.ips differ diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index d62965e6..a728cb10 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -754,6 +754,10 @@ "type": "bool" }, "money_balance": {}, + "patches": { + "type": "str", + "help": "suppress" + }, "settingsonload": { "choices": [ "default",