From 6a0aa190a9eeff32d5838449e513f28b03ffcbe2 Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 8 Dec 2025 16:37:04 -0700 Subject: [PATCH 01/76] fix: issue with keys counts not updating correctly --- PastReleaseNotes.md | 21 ++++++++++++++++++++- RELEASENOTES.md | 18 ++---------------- Rom.py | 2 +- data/base2current.bps | Bin 118398 -> 118425 bytes 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/PastReleaseNotes.md b/PastReleaseNotes.md index dbdc2f12..adf09b6a 100644 --- a/PastReleaseNotes.md +++ b/PastReleaseNotes.md @@ -1,10 +1,29 @@ # Past Feature Notes -1.4.3: File Select/End Game screen: Mirror Scroll and Pseudoboots added (Thanks Hiimcody!) +1.5.0 +* Enemy Drop: Added "spies" and shadows for hidden enemies when enemy drop shuffled is enabled +* Pottery: Pots will uncolor when the item inside is collected next time the room is loaded + +1.4.3 +* File Select/End Game screen: Mirror Scroll and Pseudoboots added (Thanks Hiimcody!) # Patch Notes Changelog archive +* 1.5.0 + * Logic: Fixed vanilla key logic for GT basement + * Logic (Playthrough): Fixed an issue where enemy kill rules were not applied during playthrough calculation. (Thanks Catobat for the catch) + * Keysanity/Keydrop Menu for DR: + * Map key information is now controlled by the Dungeon Chest Counts setting. If set to always on, this information will be available right away in the menu and will be on the HUD even when the map is not obtained. + * The key counter on the HUD for the current dungeon now accounts for keys from enemies or pots that are from vanilla key locations. + * The first number on the HUD represents all keys collected either in that dungeon or elsewhere. + * The second number on the HUD is the total keys that can be collected either in that dungeon or elsewhere. + * The key counter on inside the Menu is unchanged. (At the bottom near A button items) + * The first number in the Menu is the current number of keys in your inventory + * The second number is how many keys left to find in chests (not those from pots/enemies unless those item pools are enabled) + * Customizer: free_lamp_cone option added. The logic will account for this, and place the lamp without regard to dark rooms. + * Customizer: force_enemy option added that makes all enemies the specified type if possible. There are known gfx glitches in the overworld. + * Optimization: Improved generation performance (Thanks Catobat!) * 1.4.11 * Rom fixes (all thanks to Codemann, I believe) * Pot bug when at sprite limit diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b614f702..07edbb58 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,18 +1,4 @@ # Patch Notes -* 1.5.0 - * Logic: Fixed vanilla key logic for GT basement - * Logic (Playthrough): Fixed an issue where enemy kill rules were not applied during playthrough calculation. (Thanks Catobat for the catch) - * Enemy Drop: Added "spies" and shadows for hidden enemies when enemy drop shuffled is enabled - * Pottery: Pots will uncolor when the item inside is collected next time the room is loaded - * Keysanity/Keydrop Menu for DR: - * Map key information is now controlled by the Dungeon Chest Counts setting. If set to always on, this information will be available right away in the menu and will be on the HUD even when the map is not obtained. - * The key counter on the HUD for the current dungeon now accounts for keys from enemies or pots that are from vanilla key locations. - * The first number on the HUD represents all keys collected either in that dungeon or elsewhere. - * The second number on the HUD is the total keys that can be collected either in that dungeon or elsewhere. - * The key counter on inside the Menu is unchanged. (At the bottom near A button items) - * The first number in the Menu is the current number of keys in your inventory - * The second number is how many keys left to find in chests (not those from pots/enemies unless those item pools are enabled) - * Customizer: free_lamp_cone option added. The logic will account for this, and place the lamp without regard to dark rooms. - * Customizer: force_enemy option added that makes all enemies the specified type if possible. There are known gfx glitches in the overworld. - * Optimization: Improved generation performance (Thanks Catobat!) +* 1.5.1 + * Bugfix: Fixed an issue with keys not counting correctly diff --git a/Rom.py b/Rom.py index f71b43c4..6b8198b2 100644 --- a/Rom.py +++ b/Rom.py @@ -42,7 +42,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '98131bfbcb4f2f305befa7a0ae7dc44d' +RANDOMIZERBASEHASH = '71dcfe005725a2afcaf45698eaee971f' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index fb4e4e514484792328e6b83dc0ba822036337a57..53a2b5636037e8862dc71e10e4439cc65f0d20a3 100644 GIT binary patch delta 1685 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 R5lku+TXu0h{(lyV{tqH*%e?>q From cfa448947d1b804daef6e8ee2e908bd1d63bc81b Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 9 Dec 2025 08:09:52 -0700 Subject: [PATCH 02/76] chore: bump version --- Main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Main.py b/Main.py index 5a67ca15..fe98b9d8 100644 --- a/Main.py +++ b/Main.py @@ -38,7 +38,7 @@ from source.enemizer.DamageTables import DamageTable from source.enemizer.Enemizer import randomize_enemies from source.rom.DataTables import init_data_tables -version_number = '1.5.0' +version_number = '1.5.1' version_branch = '-u' __version__ = f'{version_number}{version_branch}' From bb2228d70a670a6886a862f1327a2b79c9f219fa Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 9 Dec 2025 11:20:34 -0700 Subject: [PATCH 03/76] fix: keys --- Main.py | 2 +- Rom.py | 2 +- data/base2current.bps | Bin 118425 -> 118474 bytes 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Main.py b/Main.py index fe98b9d8..2f1cb6a4 100644 --- a/Main.py +++ b/Main.py @@ -38,7 +38,7 @@ from source.enemizer.DamageTables import DamageTable from source.enemizer.Enemizer import randomize_enemies from source.rom.DataTables import init_data_tables -version_number = '1.5.1' +version_number = '1.5.2' version_branch = '-u' __version__ = f'{version_number}{version_branch}' diff --git a/Rom.py b/Rom.py index 6b8198b2..d4c3c894 100644 --- a/Rom.py +++ b/Rom.py @@ -42,7 +42,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '71dcfe005725a2afcaf45698eaee971f' +RANDOMIZERBASEHASH = '3f033ac891acfcedce26c00d5cd880ed' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 53a2b5636037e8862dc71e10e4439cc65f0d20a3..d13f4483e65f1b2935b0d6fac0e3a117e44cbc60 100644 GIT binary patch delta 3853 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 delta 3919 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 Date: Wed, 7 Jan 2026 10:59:36 -0700 Subject: [PATCH 04/76] fix: specific fix for vanilla entrance shuffle with no doors to properly process dungeon in the correct order for key logic --- BaseClasses.py | 10 ++++++++++ DoorShuffle.py | 42 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index 3d7e80b5..b16f456b 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -284,6 +284,16 @@ class World(object): return portal raise RuntimeError('No such portal %s for player %d' % (portal_name, player)) + def get_portal_unsafe(self, portal_name, player): + if (portal_name, player) in self._portal_cache: + return self._portal_cache[(portal_name, player)] + else: + for portal in self.dungeon_portals[player]: + if portal.name == portal_name and portal.player == player: + self._portal_cache[(portal_name, player)] = portal + return portal + return None + def is_atgt_swapped(self, player): return self.mode[player] == 'inverted' diff --git a/DoorShuffle.py b/DoorShuffle.py index 32b15ec5..793e5ead 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -104,6 +104,9 @@ def link_doors_prep(world, player): world.get_portal('Turtle Rock Eye Bridge', player).destination = True else: analyze_portals(world, player) + if world.doorShuffle[player] == 'vanilla': # these are always not destinations + world.get_portal('Desert Back', player).destination = False + world.get_portal('Skull 3', player).destination = False for portal in world.dungeon_portals[player]: connect_portal(portal, world, player) @@ -233,11 +236,21 @@ def vanilla_key_logic(world, player): enabled_entrances = world.enabled_entrances[player] = {} builder_queue = deque(builders) last_key, loops = None, 0 + + # --- Precompute all potential portals for each builder using dungeon_portals --- + from DungeonGenerator import dungeon_portals + all_potential_portals_map = {} + for builder in builders: + portal_names = list(dungeon_portals.get(builder.name, [])) + all_potential_portals_map[builder.name] = set(world.get_portal(portal_name, player).door.entrance.parent_region.name for portal_name in portal_names) + while len(builder_queue) > 0: builder = builder_queue.popleft() origin_list = entrances_map[builder.name] find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, builder.name) - if len(origin_list) <= 0: + all_potential_origins = all_potential_portals_map[builder.name] + enabled = entrances_map[builder.name] + if len(origin_list) <= 0 or should_delay_processing(enabled, all_potential_origins, potentials, connections, world, player): if last_key == builder.name or loops > 1000: origin_name = (world.get_region(origin_list[0], player).entrances[0].parent_region.name if len(origin_list) > 0 else 'no origin') @@ -310,6 +323,33 @@ def validate_vanilla_reservation(dungeon, world, player): return validate_key_layout(world.key_layout[player][dungeon.name], world, player) +def should_delay_processing(enabled_origins, potential_origins, potentials, connections, world, player): + disabled_origins = potential_origins.difference(set(enabled_origins)) + main_targets = [] + for do in disabled_origins: + region = world.get_region(do, player) + portal = _find_portal_for_region(region, world, player) + if portal and not portal.destination: + main_targets.append(region) + if not main_targets: + return False # No non-destination disabled origins found + enabling_regions = {connections[r.name] for r in main_targets} + for enabling_region in enabling_regions: + dungeon_names = { + portal.door.entrance.parent_region.dungeon.name + for dungeon_r in potentials[enabling_region] + if (portal := _find_portal_for_region(world.get_region(dungeon_r, player), world, player)) + } + if len(dungeon_names) > 1: + return True + return False + + +def _find_portal_for_region(region, world, player): + return next((p for ent in region.entrances + if (p := world.get_portal_unsafe(ent.parent_region.name.rstrip(' Portal'), player))), None) + + # some useful functions oppositemap = { Direction.South: Direction.North, From 3fab26f42328a5bbc347217fab580fab889eae98 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 15 Jan 2026 14:56:03 -0700 Subject: [PATCH 05/76] fix: prevent locked location from being moved by rupee balancing --- Fill.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Fill.py b/Fill.py index 70864fce..d466f044 100644 --- a/Fill.py +++ b/Fill.py @@ -936,6 +936,8 @@ def balance_money_progression(world): return [loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)] def interesting_item(location, item, world, player): + if location.event or location.locked: + return True if item.advancement: return True if item.type is not None or item.name.startswith('Rupee'): From a2b23110c4dd7310dbe53e6d1b76d00e83014925 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 16 Jan 2026 15:46:37 -0700 Subject: [PATCH 06/76] fix: started using the map_hud_mode flag (somewhat against my better judgement, there's some crufty asm lying about) --- PastReleaseNotes.md | 2 ++ RELEASENOTES.md | 5 +++-- Rom.py | 40 ++++++++++++++++++++------------- data/base2current.bps | Bin 118474 -> 118471 bytes resources/app/gui/lang/en.json | 4 ++-- 5 files changed, 32 insertions(+), 19 deletions(-) diff --git a/PastReleaseNotes.md b/PastReleaseNotes.md index adf09b6a..7890e51f 100644 --- a/PastReleaseNotes.md +++ b/PastReleaseNotes.md @@ -10,6 +10,8 @@ # Patch Notes Changelog archive +* 1.5.1 + * Bugfix: Fixed an issue with keys not counting correctly * 1.5.0 * Logic: Fixed vanilla key logic for GT basement * Logic (Playthrough): Fixed an issue where enemy kill rules were not applied during playthrough calculation. (Thanks Catobat for the catch) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 07edbb58..98c16bcc 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,4 +1,5 @@ # Patch Notes -* 1.5.1 - * Bugfix: Fixed an issue with keys not counting correctly +* Logic: Key logic fix for part of a dungeon located at Skull 3 (or other similar restricted entrancs). Appropriate key logic was not being applied, causing progression issues. This mostly affect crosskey style seeds. +* Standard: Rupee balancing algorithm can no longer switch out the weapon on uncle for money. +* Dungeon Counters: Some fixes for inconsistent tracking and display of dungeon counters on the keysanity menu. \ No newline at end of file diff --git a/Rom.py b/Rom.py index d4c3c894..e56b6985 100644 --- a/Rom.py +++ b/Rom.py @@ -42,7 +42,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '3f033ac891acfcedce26c00d5cd880ed' +RANDOMIZERBASEHASH = '794b7cd02f429873c3ad6dc74490279c' class JsonRom(object): @@ -465,9 +465,9 @@ def patch_rom(world, rom, player, team, is_mystery=False): itemid = 0x5A if not location.locked and ((location.item.smallkey and world.keyshuffle[player] == 'none') or ( - location.item.bigkey and world.bigkeyshuffle[player] == 'none') or ( - location.item.map and world.mapshuffle[player] == 'none') or ( - location.item.compass and world.compassshuffle[player] == 'none')): + location.item.bigkey and world.bigkeyshuffle[player] == 'none') or ( + location.item.map and world.mapshuffle[player] == 'none') or ( + location.item.compass and world.compassshuffle[player] == 'none')): itemid = handle_native_dungeon(location, itemid) rom.write_byte(location.address, itemid) @@ -556,15 +556,15 @@ def patch_rom(world, rom, player, team, is_mystery=False): if world.mirrorscroll[player] or world.doorShuffle[player] != 'vanilla': dr_flags |= DROptions.Town_Portal if world.doorShuffle[player] == 'vanilla': - dr_flags |= DROptions.Eternal_Mini_Bosses + dr_flags |= DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] not in ['vanilla', 'basic']: dr_flags |= DROptions.Map_Info if ((world.collection_rate[player] or world.goal[player] == 'completionist') - and world.goal[player] not in ['triforcehunt', 'trinity', 'ganonhunt']): + and world.goal[player] not in ['triforcehunt', 'trinity', 'ganonhunt']): dr_flags |= DROptions.Debug rom.write_byte(snes_to_pc(0x308039), 1) - if world.doorShuffle[player] not in ['vanilla', 'basic'] and world.logic[player] != 'nologic'\ - and world.mixed_travel[player] == 'prevent': + if world.doorShuffle[player] not in ['vanilla', 'basic'] and world.logic[player] != 'nologic' \ + and world.mixed_travel[player] == 'prevent': # PoD Falling Bridge or Hammjump # 1FA607: db $2D, $79, $69 ; 0x0069: Vertical Rail ↕ | { 0B, 1E } | Size: 05 # 1FA60A: db $14, $99, $5D ; 0x005D: Large Horizontal Rail ↔ | { 05, 26 } | Size: 01 @@ -649,9 +649,9 @@ def patch_rom(world, rom, player, team, is_mystery=False): if world.doorShuffle[player] == 'basic': rom.write_byte(0x138002, 1) for door in world.doors: - if door.dest is not None and isinstance(door.dest, Door) and\ - door.player == player and door.type in [DoorType.Normal, DoorType.SpiralStairs, - DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]: + if door.dest is not None and isinstance(door.dest, Door) and \ + door.player == player and door.type in [DoorType.Normal, DoorType.SpiralStairs, + DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]: rom.write_bytes(door.getAddress(), door.dest.getTarget(door)) for paired_door in world.paired_doors[player]: rom.write_bytes(paired_door.address_a(world, player), paired_door.rom_data_a(world, player)) @@ -1108,7 +1108,7 @@ def patch_rom(world, rom, player, team, is_mystery=False): gametype = 0x04 # item if (world.shuffle[player] != 'vanilla' or world.doorShuffle[player] != 'vanilla' - or world.dropshuffle[player] != 'none' or world.pottery[player] != 'none'): + or world.dropshuffle[player] != 'none' or world.pottery[player] != 'none'): gametype |= 0x02 # entrance/door rom.write_byte(0x180211, gametype) # Game type @@ -1201,8 +1201,18 @@ def patch_rom(world, rom, player, team, is_mystery=False): | (0x02 if world.compassshuffle[player] else 0x00) | (0x04 if world.mapshuffle[player] else 0x00) | (0x08 if world.bigkeyshuffle[player] else 0x00))) # free roaming item text boxes - rom.write_byte(0x18003B, 0x01 if world.mapshuffle[player] else 0x00) # maps showing crystals on overworld + map_hud_mode = 0x00 + if world.dungeon_counters[player] == 'on': + map_hud_mode = 0x02 # always on + elif world.dungeon_counters[player] == 'off': + pass + elif world.keyshuffle[player] != 'universal' and (world.mapshuffle[player] != 'none' or world.doorShuffle[player] != 'vanilla' + or world.dropshuffle[player] != 'none' or world.pottery[player] not in ['none', 'cave'] or world.dungeon_counters[player] == 'pickup'): + map_hud_mode = 0x01 # show on pickup + rom.write_byte(0x18003A, map_hud_mode) + + rom.write_byte(0x18003B, 0x01 if world.mapshuffle[player] else 0x00) # maps showing crystals on overworld # compasses showing dungeon count compass_mode = 0x00 if world.clock_mode != 'none' or world.dungeon_counters[player] == 'off': @@ -1408,7 +1418,7 @@ def patch_rom(world, rom, player, team, is_mystery=False): write_enemy_shuffle_settings(world, player, rom) if (world.doorShuffle[player] != 'vanilla' or world.dropshuffle[player] != 'none' - or world.pottery[player] != 'none'): + or world.pottery[player] != 'none'): for room in world.rooms: if room.player == player and room.modified: if room.index in world.data_tables[player].room_list: @@ -1453,7 +1463,7 @@ def patch_rom(world, rom, player, team, is_mystery=False): (hashint >> 10) & 0x1F, (hashint >> 5) & 0x1F, hashint & 0x1F, - ] + ] rom.write_bytes(0x180215, code) rom.hash = code diff --git a/data/base2current.bps b/data/base2current.bps index d13f4483e65f1b2935b0d6fac0e3a117e44cbc60..b499ded82f8231c4e574e1c36ca11b7618fe370b 100644 GIT binary patch delta 7225 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 delta 7122 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 diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 92318d72..bd8f9fa6 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -92,11 +92,11 @@ "randomizer.dungeon.experimental": "Enable Experimental Features", - "randomizer.dungeon.dungeon_counters": "Dungeon Chest Counters", + "randomizer.dungeon.dungeon_counters": "Dungeon Counters", "randomizer.dungeon.dungeon_counters.default": "Auto", "randomizer.dungeon.dungeon_counters.off": "Off", "randomizer.dungeon.dungeon_counters.on": "On", - "randomizer.dungeon.dungeon_counters.pickup": "On Compass Pickup", + "randomizer.dungeon.dungeon_counters.pickup": "On Item Pickup", "randomizer.dungeon.mixed_travel": "Mixed Dungeon Travel ", "randomizer.dungeon.mixed_travel.prevent": "Prevent Mixed Dungeon Travel", From fabad625d2aa9c88dd6702202a053d2e24169655 Mon Sep 17 00:00:00 2001 From: theclearmouse <105736589+theclearmouse@users.noreply.github.com> Date: Sun, 25 Jan 2026 22:28:19 -0500 Subject: [PATCH 07/76] add main tournament 2025 winner --- Text.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Text.py b/Text.py index 016bb0b0..90aa3fbf 100644 --- a/Text.py +++ b/Text.py @@ -1795,6 +1795,7 @@ class TextTable(object): text['telepathic_tile_ice_stalfos_knights_room'] = CompressedTextMapper.convert("{NOBORDER}\nKnock 'em down and then bomb them dead.") text['telepathic_tile_tower_of_hera_entrance'] = CompressedTextMapper.convert("{NOBORDER}\nThis is a bad place, with a guy who will make you fall…\n\n\na lot.") text['houlihan_room'] = CompressedTextMapper.convert("Randomizer tournament winners\n{HARP}\n" + " ~~~2025~~~\nGammachuu\n\n" " ~~~2024~~~\nGammachuu\n\n" " ~~~2023~~~\nGanonsGoneWild\n\n" " ~~~2022~~~\nObscure\n\n" From 31682fb8a245d35a8441ad08277038153b67c491 Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 26 Jan 2026 14:58:17 -0700 Subject: [PATCH 08/76] doc: update release note --- RELEASENOTES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 98c16bcc..4e3ee4a8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,4 +2,5 @@ * Logic: Key logic fix for part of a dungeon located at Skull 3 (or other similar restricted entrancs). Appropriate key logic was not being applied, causing progression issues. This mostly affect crosskey style seeds. * Standard: Rupee balancing algorithm can no longer switch out the weapon on uncle for money. -* Dungeon Counters: Some fixes for inconsistent tracking and display of dungeon counters on the keysanity menu. \ No newline at end of file +* Dungeon Counters: Some fixes for inconsistent tracking and display of dungeon counters on the keysanity menu. +* Text: Updated main tournament winner (Thanks clearmouse!) \ No newline at end of file From eda03d865779d257912e3b58ce3ef280db680702 Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 26 Jan 2026 15:10:51 -0700 Subject: [PATCH 09/76] chore: version bump --- Main.py | 2 +- RELEASENOTES.md | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Main.py b/Main.py index 2f1cb6a4..00efbacc 100644 --- a/Main.py +++ b/Main.py @@ -38,7 +38,7 @@ from source.enemizer.DamageTables import DamageTable from source.enemizer.Enemizer import randomize_enemies from source.rom.DataTables import init_data_tables -version_number = '1.5.2' +version_number = '1.5.3' version_branch = '-u' __version__ = f'{version_number}{version_branch}' diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4e3ee4a8..4b522d5f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,6 +1,7 @@ # Patch Notes -* Logic: Key logic fix for part of a dungeon located at Skull 3 (or other similar restricted entrancs). Appropriate key logic was not being applied, causing progression issues. This mostly affect crosskey style seeds. -* Standard: Rupee balancing algorithm can no longer switch out the weapon on uncle for money. -* Dungeon Counters: Some fixes for inconsistent tracking and display of dungeon counters on the keysanity menu. -* Text: Updated main tournament winner (Thanks clearmouse!) \ No newline at end of file +* 1.5.3 + * Logic: Key logic fix for part of a dungeon located at Skull 3 (or other similar restricted entrancs). Appropriate key logic was not being applied, causing progression issues. This mostly affect crosskey style seeds. + * Standard: Rupee balancing algorithm can no longer switch out the weapon on uncle for money. + * Dungeon Counters: Some fixes for inconsistent tracking and display of dungeon counters on the keysanity menu. + * Text: Updated main tournament winner (Thanks clearmouse!) \ No newline at end of file From bf92fe91c2f2858ab718ed75d00139fab9eaa65a Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 30 Jan 2026 10:05:29 -0700 Subject: [PATCH 10/76] doc: new documentation helps fix: issue with blank shop items --- CLAUDE.md | 257 ++++++++++ Fill.py | 2 +- Main.py | 2 +- PastReleaseNotes.md | 5 + RELEASENOTES.md | 9 +- _config.yml | 140 +++++- .../2026-01-30-welcome-to-door-randomizer.md | 74 +++ blog/index.md | 100 ++++ features.md | 473 ++++++++++++++++++ index.md | 81 +++ installation.md | 369 ++++++++++++++ known-issues.md | 373 ++++++++++++++ roadmap.md | 234 +++++++++ 13 files changed, 2111 insertions(+), 8 deletions(-) create mode 100644 CLAUDE.md create mode 100644 _posts/2026-01-30-welcome-to-door-randomizer.md create mode 100644 blog/index.md create mode 100644 features.md create mode 100644 index.md create mode 100644 installation.md create mode 100644 known-issues.md create mode 100644 roadmap.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..a67098ad --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,257 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**ALttPDoorRandomizer** is a sophisticated dungeon and door randomizer for *The Legend of Zelda: A Link to the Past* (SNES). It extends the standard ALTTP Entrance Randomizer with advanced dungeon shuffling capabilities that can rearrange dungeon interiors at the door and sector level. + +- **Repository**: https://github.com/aerinon/ALttPDoorRandomizer +- **Python Version**: 3.10+ +- **Current Branch**: DoorDevUnstable (dev branch: NewGeneration) +- **Discord**: #door-rando and #bug-reports channels at ALTTP Randomizer discord + +## Development Commands + +### Running the Randomizer +```bash +# CLI interface +python DungeonRandomizer.py + +# GUI interface +python Gui.py + +``` + +### Dependencies +```bash +# Install platform-specific dependencies +python resources/ci/common/local_install.py + +# For multiworld server/client +# Run the same command to install multiworld dependencies +``` + +### Testing +```bash +# Run test suite +python TestSuite.py + +# Run test suite with specific door shuffle mode +python TestSuite.py --dr basic --count 10 --tense 2 + +# Individual test files (unittest-based) +python -m pytest test/ +python -m pytest test/dungeons/TestDarkPalace.py +``` + +### ROM Generation +```bash +# Basic door shuffle seed +python DungeonRandomizer.py --doorShuffle crossed --intensity 2 + +# Suppress ROM output (for testing) +python DungeonRandomizer.py --suppress_rom --spoiler none + +# Create BPS patch +python DungeonRandomizer.py --bps +``` + +## Architecture Overview + +### Generation Pipeline + +The randomizer follows this high-level flow (orchestrated in `Main.py`): + +1. **World Setup** → Initialize World object, parse settings, set seed +2. **Region Creation** → Build game regions, locations, entrances (`Regions.py`) +3. **Dungeon Structure** → Create dungeon objects and rooms (`Dungeons.py`, `RoomData.py`) +4. **Door/Entrance Shuffling** → Shuffle dungeon interiors and/or overworld entrances +5. **Logic Rules** → Set access requirements for all locations (`Rules.py`) +6**Item Placement** → Generate item pool and place items with logic constraints (`Fill.py`) +7. **ROM Patching** → Apply changes to base ROM (`Rom.py`) +8. **Output** → Generate ROM file, spoiler log, and/or multiworld data + +### Core Data Structures (BaseClasses.py) + +- **World**: Container for all game state, regions, dungeons, items, and settings +- **Region**: Geographic area with locations and exits +- **Location**: Item placement spot with access rules +- **Item**: Game items with advancement/priority flags +- **Entrance**: Connections between regions with conditional access +- **CollectionState**: Tracks player's collected items for logic evaluation + +### Dungeon Generation System + +The dungeon shuffling system uses a three-stage algorithm: + +#### 1. DungeonGenLocalSearch (source/dungeon/DungeonGenLocalSearch.py) +**Purpose**: Assigns dungeon sectors to dungeons using constraint satisfaction + +**Balancing Constraints**: +- **Polarity**: North/South and East/West door balance +- **Crystal Balance**: Blue/orange barriers must have matching switches +- **Portal Balance**: Entrance distribution +- **Location Balance**: Ensures dungeons have accessible non-big-key locations +- **Branching**: Dead ends vs branches ratio (insufficient branches = unreachable areas) +- **Transitivity**: Validates that doors can actually connect + +**Algorithm**: Local search with greedy constraint satisfaction, prioritizing crystal switches → portals → locations → branching → polarity → transitivity + +#### 2. DungeonGenTransitivity (source/dungeon/DungeonGenTransitivity.py) +**Purpose**: Validates that door connections are possible for a given sector assignment + +**Key Features**: +- Verifies all doors can be validly connected (e.g., North↔South, East↔West, Stairs↔Stairs) +- Propagates crystal switch states through connections +- Detects forced connections and impossible layouts +- Uses depth-first search with constraint propagation +- Handles special mechanics (portals, crystal barriers, special rooms) + +#### 3. DungeonStitcherV2 (source/dungeon/DungeonStitcherV2.py) +**Purpose**: Realizes actual door connections from validated sector assignments + +**Key Features**: +- Generates random valid door pairings +- Assigns portal connections (dungeon entrances) +- Simulates player traversal to verify connectivity +- Manages crystal barrier state propagation + +### Key Logic Components + +**Rules.py** (~7000 lines): Sets logical requirements for accessing locations +- Supports multiple logic modes: noglitches, owglitches, hybridglitches, nologic +- Handles multiple key logic algorithms: partial, strict, dangerous, experimental +- Manages dungeon-specific access rules and boss requirements + +**Fill.py** (~2000 lines): Item placement algorithms +- **Balanced**: Random distribution +- **Vanilla Fill**: Attempts vanilla item locations when possible +- **Major Location Restriction**: Major items → major locations +- **Dungeon Restriction**: Major items → dungeons +- **District Restriction**: Items distributed by geographic region + +**KeyPlacement.py** / **NewKeyLogic.py**: Advanced key door logic +- Handles small key placement with multiple satisfiability algorithms +- Prevents self-locking situations +- Manages big key placement rules +- Validates minimum key requirements for dungeon completion + +### Source Module Organization + +``` +source/ +├── dungeon/ # Dungeon generation algorithms and key logic +├── overworld/ # Entrance shuffling (EntranceShuffle2.py) +├── item/ # Item placement utilities (FillUtil.py, District.py) +├── enemizer/ # Enemy randomization system +├── gui/ # GUI components (tkinter-based) +├── rom/ # ROM patching utilities +├── classes/ # Utility classes (BabelFish, CustomSettings) +├── meta/ # Metadata and build tools +└── limited/ # Limited run coordination +``` + +### Important Files by Size/Complexity + +- **DoorShuffle.py** (~8500 lines): Door shuffling orchestration +- **Rules.py** (~7000 lines): Location access logic +- **EnemyList.py** (~6000 lines): Enemy definitions +- **Doors.py** (~5500 lines): Door object definitions +- **Rom.py** (~5500 lines): ROM patching +- **Regions.py** (~5000 lines): World region creation +- **DungeonGenerator.py** (~5000 lines): Legacy dungeon generation +- **EntranceShuffle2.py** (~5000 lines): Entrance shuffling logic +- **BaseClasses.py** (~4000 lines): Core data structures +- **KeyDoorShuffle.py** (~3500 lines): Key door logic +- **PotShuffle.py** (~3500 lines): Pot randomization + +## Key Concepts + +### Door Shuffle Modes +- **Vanilla**: No shuffling +- **Basic**: Shuffle within each dungeon +- **Partitioned**: Shuffle in pools (Light World, Early Dark World, Late Dark World) +- **Crossed**: Full shuffle between all dungeons + +### Intensity Levels +- **Level 1**: Normal doors and spiral staircases +- **Level 2**: + open edges and straight staircases +- **Level 3**: + dungeon lobbies + +### Key Shuffle Modes +- **In Dungeon** (keyshuffle=none): Keys restricted to their dungeon +- **Randomized** (keyshuffle=wild): Keys can be anywhere +- **Universal** (keyshuffle=universal): Keys work in any dungeon + +### Fill Algorithms (--algorithm) +See Fill.py for implementation details. The algorithm affect where item may be placed. + +### Logic Modes +- **noglitches**: Standard logic +- **owglitches**: Overworld glitches (OOB, clips, superbunny) +- **hybridglitches**: + major underworld glitches (not compatible with door shuffle) +- **nologic**: No logical constraints + +## Testing Strategy + +Tests verify location accessibility with various item combinations: +- **test/dungeons/**: Per-dungeon location access tests +- **test/vanilla/**: Vanilla world logic tests +- **test/owg/**: Overworld glitch logic tests +- **test/inverted/**: Inverted mode tests +- **TestBase.py**: Base test class with utility methods + +Use `TestSuite.py` for batch generation testing across modes (Open/Standard/Inverted) and settings. + +## Important Development Notes + +### Starting Items & Special Items +- **Mirror Scroll**: Dungeon-only mirror (starting item in door shuffle) +- **Bomb Bag**: Optional item that gates bomb usage +- **Pseudo Boots**: Allows dashing but gates certain sequence breaks + +### Branch Structure +- **DoorDev**: Main development branch (use for PRs) +- **DoorDevUnstable**: Current working branch +- **Dev/Master**: Do NOT use for PRs + +### Git Workflow +Recent commits show work on: +- Limited run coordination +- Custom rooms support +- Key logic improvements (transitivity, portal checks, placement rules) +- Isolated region detection fixes +- Big key placement satisfiability + +### Documentation Resources +- **Algorithm.md**: Detailed dungeon generation algorithm documentation +- **docs/ai_context/**: Comprehensive architectural documentation by topic +- **docs/Customizer.md**: Custom seed configuration guide +- **docs/DR_hint_reference.md**: Hint system documentation +- **docs/BUILDING.md**: Build and installation instructions +- **README.md**: User-facing feature documentation + +## Common Gotchas + +1. **Transitivity Checks**: Most expensive constraint check - always run last +2. **Self-Locking Keys**: Key logic must prevent situations where players cannot access the rest of the dungeon +3. **Crystal Barriers**: Blue/orange barriers require matching switches in dungeon +4. **Portal Distribution**: Each dungeon needs at least one entrance +5. **Branching Balance**: Too many dead ends without branches = unreachable areas +6**Polarity Balance**: N/S and E/W door counts must allow valid connections + +## CLI Argument Reference + +Key arguments from `resources/app/cli/args.json`: +- `--doorShuffle [vanilla|basic|partitioned|crossed]` +- `--intensity [1|2|3]` +- `--keyshuffle [none|wild|universal]` +- `--key_logic [partial|strict|dangerous|experimental]` +- `--algorithm [balanced|vanilla_fill|major_only|dungeon_only|district]` +- `--logic [noglitches|owglitches|hybridglitches|nologic]` +- `--mode [open|standard|inverted]` +- `--shuffle`: Entrance randomizer options (vanilla, simple, restricted, full, crossed, insanity, etc.) +- `--pottery`: Pot shuffling modes +- `--shopsanity`: Enable shop item randomization +- `--enemizer`: Enemy randomization options diff --git a/Fill.py b/Fill.py index d466f044..349ff83e 100644 --- a/Fill.py +++ b/Fill.py @@ -980,7 +980,7 @@ def balance_money_progression(world): slot = shop_to_location_table[location.parent_region.name].index(location.name) shop = location.parent_region.shop shop_item = shop.inventory[slot] - if location.item and interesting_item(location, location.item, world, location.item.player): + if shop_item and location.item and interesting_item(location, location.item, world, location.item.player): if location.item.name.startswith('Rupee') and loc_player == location.item.player: if shop_item['price'] < rupee_chart[location.item.name]: wallet[loc_player] -= shop_item['price'] # will get picked up in the location_free block diff --git a/Main.py b/Main.py index 00efbacc..3099c0b0 100644 --- a/Main.py +++ b/Main.py @@ -38,7 +38,7 @@ from source.enemizer.DamageTables import DamageTable from source.enemizer.Enemizer import randomize_enemies from source.rom.DataTables import init_data_tables -version_number = '1.5.3' +version_number = '1.5.4' version_branch = '-u' __version__ = f'{version_number}{version_branch}' diff --git a/PastReleaseNotes.md b/PastReleaseNotes.md index 7890e51f..7cae577d 100644 --- a/PastReleaseNotes.md +++ b/PastReleaseNotes.md @@ -10,6 +10,11 @@ # Patch Notes Changelog archive +* 1.5.3 + * Logic: Key logic fix for part of a dungeon located at Skull 3 (or other similar restricted entrancs). Appropriate key logic was not being applied, causing progression issues. This mostly affect crosskey style seeds. + * Standard: Rupee balancing algorithm can no longer switch out the weapon on uncle for money. + * Dungeon Counters: Some fixes for inconsistent tracking and display of dungeon counters on the keysanity menu. + * Text: Updated main tournament winner (Thanks clearmouse!) * 1.5.1 * Bugfix: Fixed an issue with keys not counting correctly * 1.5.0 diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4b522d5f..ae96add3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,7 +1,6 @@ # Patch Notes -* 1.5.3 - * Logic: Key logic fix for part of a dungeon located at Skull 3 (or other similar restricted entrancs). Appropriate key logic was not being applied, causing progression issues. This mostly affect crosskey style seeds. - * Standard: Rupee balancing algorithm can no longer switch out the weapon on uncle for money. - * Dungeon Counters: Some fixes for inconsistent tracking and display of dungeon counters on the keysanity menu. - * Text: Updated main tournament winner (Thanks clearmouse!) \ No newline at end of file +* 1.5.4 + * Documentation: New AI-assisted documentation [Site (I really hope this works)](https//aerinon.github.io/ALttPDoorRandomizer) + * Generation Error: Fixed Issue with Shop Code and Take Any Caves (thanks Codemann for assistance) + diff --git a/_config.yml b/_config.yml index c7418817..33a55de0 100644 --- a/_config.yml +++ b/_config.yml @@ -1 +1,139 @@ -theme: jekyll-theme-slate \ No newline at end of file +# Site settings +title: ALttP Dungeon Randomizer +description: Advanced dungeon randomization for The Legend of Zelda - A Link to the Past +baseurl: "/ALttPDoorRandomizer" +url: "https://aerinon.github.io" + +# Theme +theme: jekyll-theme-slate +remote_theme: just-the-docs/just-the-docs + +# Logo and branding +# logo: "/assets/images/logo.png" + +# Navigation +nav_enabled: true +nav_external_links: + - title: GitHub + url: https://github.com/aerinon/ALttPDoorRandomizer + - title: Discord + url: https://discordapp.com/invite/alttprandomizer + - title: Download + url: https://github.com/aerinon/ALttPDoorRandomizer/releases + +# Search +search_enabled: true +search: + heading_level: 2 + previews: 3 + preview_words_before: 5 + preview_words_after: 10 + tokenizer_separator: /[\s/]+/ + rel_url: true + button: true + +# Heading anchors +heading_anchors: true + +# Aux links +aux_links: + "Door Randomizer on GitHub": + - "https://github.com/aerinon/ALttPDoorRandomizer" + "ALTTP Rando Discord": + - "https://discordapp.com/invite/alttprandomizer" + +aux_links_new_tab: true + +# Footer +footer_content: "Copyright © 2026 ALttP Door Randomizer Team. Based on ALttP Entrance Randomizer." + +# Back to top link +back_to_top: true +back_to_top_text: "Back to top" + +# Collections for blog posts +collections: + posts: + output: true + permalink: /blog/:year/:month/:day/:title/ + +# Defaults +defaults: + - scope: + path: "" + type: "posts" + values: + layout: "default" + - scope: + path: "" + values: + layout: "default" + +# Build settings +markdown: kramdown +kramdown: + input: GFM + syntax_highlighter: rouge + syntax_highlighter_opts: + css_class: 'highlight' + +plugins: + - jekyll-feed + - jekyll-seo-tag + - jekyll-sitemap + +# Exclude from processing +exclude: + - .git/ + - .github/ + - .gitignore + - .gitattributes + - source/ + - test/ + - resources/ + - data/ + - debug/ + - analysis/ + - analysis2/ + - alttp_dungeon_randomizer.egg-info/ + - DR/ + - "*.py" + - "*.pyc" + - "*.pyd" + - "*.pyo" + - "*.egg-info" + - "*.spec" + - requirements*.txt + - setup.py + - TestSuite.py + - DungeonRandomizer.py + - Gui.py + - MultiServer.py + - MultiClient.py + - CLAUDE.md + - LICENSE + - libbz2-dev + - tlogbfs.txt + +# Include files +include: + - _posts + - blog + - docs/Customizer.md + - docs/BUILDING.md + - docs/DR_hint_reference.md + +# Color scheme (for just-the-docs theme) +color_scheme: dark + +# Google Analytics (optional - add your tracking ID) +# ga_tracking: UA-XXXXXXXX-X + +# Compress HTML +compress_html: + clippings: all + comments: all + endings: all + startings: [] + blanklines: false + profile: false diff --git a/_posts/2026-01-30-welcome-to-door-randomizer.md b/_posts/2026-01-30-welcome-to-door-randomizer.md new file mode 100644 index 00000000..fbe4eb23 --- /dev/null +++ b/_posts/2026-01-30-welcome-to-door-randomizer.md @@ -0,0 +1,74 @@ +--- +layout: default +title: "Welcome to the ALttP Door Randomizer Blog" +date: 2026-01-26 +author: Door Randomizer Team +category: development +excerpt: "Introducing the new Door Randomizer documentation site and blog. Learn about what's new, what's coming, and how to get involved." +--- + +# Welcome to the ALttP Door Randomizer Blog + +We're excited to launch the official documentation site and development blog for ALttP Door Randomizer! This site serves as a central hub for all things Door Rando - from comprehensive feature documentation to release notes and development updates. + +## What's New Here? + +### Comprehensive Documentation + +We've reorganized and expanded our documentation to make it easier to find what you need: + +- **[Features Guide](/features)** - Complete reference for all randomizer settings and modes +- **[Installation & Usage](/installation)** - Step-by-step setup instructions for all platforms +- **[Known Issues](/known-issues)** - Tracking current bugs and limitations +- **[Roadmap](/roadmap)** - Our vision for the future of Door Randomizer + +### Development Blog + +This blog will be your source for: + +- **Feature highlights** - Deep dives into specific features +- **Development updates** - Behind-the-scenes looks at ongoing work +- **Technical articles** - How the randomizer works under the hood + +## What is Door Randomizer? + +If you're new here, Door Randomizer takes the ALttP randomizer experience to the next level by shuffling the connections between rooms within dungeons. Walk through a door in Eastern Palace and you might end up in Tower of Hera, Ice Palace, or anywhere else! + +Key features include: + +- **Four shuffle modes** - From basic (within dungeon) to crossed (full chaos) +- **Three intensity levels** - Control how thoroughly dungeons are shuffled +- **Advanced key logic** - Multiple algorithms to prevent self-locking +- **Pottery shuffle** - Randomize items under pots with multiple modes +- **Shopsanity** - 32+ shop locations with intelligent pricing +- **Enemizer integration** - Enemy and boss randomization with logic +- **Custom seeds** - Create your own dungeons and rooms + +[Learn more about features →](/features) + +[Read the Customizer guide →](https://github.com/aerinon/ALttPDoorRandomizer/blob/DoorDev/docs/Customizer.md) + +## What's Coming Next? + +We have plans for Door Randomizer's future. Check out our [Roadmap](/roadmap) for details. (Still very much a work in progress!) + +## Community + +Join the Door Randomizer community: + +- **Discord**: [ALTTP Randomizer Discord](https://discordapp.com/invite/alttprandomizer) + - `#door-rando` - General discussion and help + - `#bug-reports` - Report bugs and issues +- **GitHub**: [aerinon/ALttPDoorRandomizer](https://github.com/aerinon/ALttPDoorRandomizer) + +## Stay Tuned + +We'll be posting irregular updates here as development continues. + +Thank you to everyone who has contributed to Door Randomizer through code, testing, feedback, and community support. We're excited about the future and can't wait to see what lies in store! + +Happy randomizing! + +--- + +*Have questions or feedback about the new documentation site? Let us know in Discord!* diff --git a/blog/index.md b/blog/index.md new file mode 100644 index 00000000..392cc00d --- /dev/null +++ b/blog/index.md @@ -0,0 +1,100 @@ +--- +layout: default +title: Blog +nav_order: 6 +has_children: false +--- + +# Development Blog +{: .no_toc } + +Latest updates, release notes, and development insights for ALttP Door Randomizer. +{: .fs-6 .fw-300 } + +--- + +## Recent Posts + +{% assign posts = site.posts | sort: 'date' | reverse %} +{% for post in posts limit:10 %} +
+

+ {{ post.title }} +

+

+ {{ post.date | date: "%B %d, %Y" }} + {% if post.author %} • by {{ post.author }}{% endif %} +

+

{{ post.excerpt | strip_html | truncatewords: 50 }}

+

Read more →

+
+{% endfor %} + +--- + +## Categories + +Browse posts by category: + +- [Release Notes](#release-notes) +- [Development Updates](#development-updates) +- [Feature Highlights](#feature-highlights) +- [Technical Deep Dives](#technical-deep-dives) + +### Release Notes + +{% assign release_posts = site.posts | where: "category", "release" %} +{% for post in release_posts limit:5 %} +- [{{ post.title }}]({{ post.url | relative_url }}) - {{ post.date | date: "%B %d, %Y" }} +{% endfor %} + +### Development Updates + +{% assign dev_posts = site.posts | where: "category", "development" %} +{% for post in dev_posts limit:5 %} +- [{{ post.title }}]({{ post.url | relative_url }}) - {{ post.date | date: "%B %d, %Y" }} +{% endfor %} + +### Feature Highlights + +{% assign feature_posts = site.posts | where: "category", "features" %} +{% for post in feature_posts limit:5 %} +- [{{ post.title }}]({{ post.url | relative_url }}) - {{ post.date | date: "%B %d, %Y" }} +{% endfor %} + +### Technical Deep Dives + +{% assign feature_posts = site.posts | where: "category", "features" %} +{% for post in feature_posts limit:5 %} +- [{{ post.title }}]({{ post.url | relative_url }}) - {{ post.date | date: "%B %d, %Y" }} + {% endfor %} + +--- + +## Subscribe + +Stay up to date with Door Randomizer development: + +- **Discord**: Join [ALTTP Randomizer Discord](https://discordapp.com/invite/alttprandomizer) in `#door-rando` + +--- + +## Archive + +View all posts by year: + +{% assign posts_by_year = site.posts | group_by_exp: "post", "post.date | date: '%Y'" %} +{% for year in posts_by_year %} +### {{ year.name }} +{% for post in year.items %} +- [{{ post.title }}]({{ post.url | relative_url }}) - {{ post.date | date: "%B %d, %Y" }} +{% endfor %} +{% endfor %} + +--- + +## Contributing to the Blog + +Want to write a guest post or contribute content? + +Contact us on Discord to discuss your ideas! diff --git a/features.md b/features.md new file mode 100644 index 00000000..2a404b18 --- /dev/null +++ b/features.md @@ -0,0 +1,473 @@ +--- +layout: default +title: Features +nav_order: 2 +--- + +# Features Guide +{: .no_toc } + +Comprehensive documentation of all Door Randomizer features and settings. +{: .fs-6 .fw-300 } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Important Differences from Other Randomizers + +Most of these apply only when door shuffle is not vanilla. + +### Starting Item + +You start with a **Mirror Scroll** (looks like a map), a simplified mirror that only works in dungeons, not the overworld, and can't erase blocks like the Mirror. + +### Navigation Changes + +- Holes in Mire Torches Top and Mire Torches Bottom fall through to rooms below (you only need fire to get the chest) +- You can Hookshot from the left Mire wooden Bridge to the right one +- In the PoD Arena, you can bonk with Boots between the two blue crystal barriers against the ladder to reach the Arena Bridge chest and door (Bomb Jump also possible but not in logic - Boots are required) +- Flooded Rooms in Swamp can be traversed backward and may be required. Flippers are needed to get out of the water + +### Logic Differences + +- The chest in southeast Skull Woods that is traditionally a guaranteed Small Key in ER is not guaranteed here +- Fire Rod is not in logic for dark rooms (hard enough to figure out which dark room you are in) +- The hammerjump (and some other skips) are not in logic by default (see the mixed_travel setting for details). Doing so in a crossed dungeon seed can put you into another dungeon with the wrong dungeon id + +### Boss Differences + +- You have to find the attic floor and bomb it open and bring the maiden to the light to fight Blind. In cross dungeon door shuffle, the attic can be in any dungeon. If you bring the maiden to the boss arena, she will hint where the cracked floor can be found +- GT Bosses do not respawn after killing them in this mode +- Enemizer change: The attic/maiden sequence is now active and required when Blind is the boss of Thieves' Town even when bosses are shuffled + +### Crystal Switches + +- You can hit the PoD crystal switch in the Sexy Statue room with a bomb from the balcony above without jumping down +- GT Crystal Conveyor room (it has gibdos) - You can hit the crystal switch with a bomb when the blue barrier is up from the far side so you can leave the room to the left with blue barriers down +- PoD Arena Bridge - If entering from the bridge, you can circle round and hit the switch, then fall into the hole to respawn at the bridge again with the crystal barriers different + +--- + +## Dungeon Settings + +### Door Shuffle + +Controls how dungeon doors are shuffled: + +- **Vanilla** - Doors are not shuffled +- **Basic** - Doors are shuffled only within a single dungeon +- **Partitioned** - Dungeons are shuffled in 3 pools: Light World, Early Dark World, Late Dark World (Late Dark are the four dungeons that require Mitts in vanilla, including Ganon's Tower) +- **Crossed** - Doors are shuffled between dungeons as well + +CLI: `--doorShuffle [vanilla|basic|partitioned|crossed]` + +### Intensity + +Controls which types of connections are shuffled: + +- **Level 1** - Normal doors and spiral staircases are shuffled +- **Level 2** - Same as Level 1 plus open edges and both types of straight staircases are shuffled +- **Level 3** - Same as Level 2 plus Dungeon Lobbies are shuffled + +CLI: `--intensity [1|2|3]` + +### Door Type Shuffle + +Controls which types of doors can be shuffled (only active if door shuffle is not vanilla): + +- **Small Key Doors, Bomb Doors, Dash Doors** - Standard shuffled doors +- **Adds Big Key Doors** - Big key doors are now shuffled in addition to those above +- **Adds Trap Doors** - All trap doors that are permanently shut in vanilla are shuffled, excluding boss trap doors +- **Increases all Door Types** - Chaos mode where each door type per dungeon is randomized between 1 less and 4 more + +CLI: `--door_type_mode [original|big|all|chaos]` + +### Trap Door Removal + +Options for making dungeon traversal more convenient: + +- **No Removal** - Does not remove any trap doors +- **Removed If Blocking Path** - Dungeon generation removes annoying trap doors if necessary (boss trap doors never shuffled) +- **Remove Boss Traps** - Boss traps are removed, including the one near Mothula +- **Remove All Annoying Traps** - Removes all trap doors that are annoying, including boss traps + +CLI: `--trap_door_mode [vanilla|optional|boss|oneway]` + +### Dungeon Items + +#### Small Keys + +- **In Dungeon** - Small keys are restricted to their own dungeon +- **Randomized** - Small keys can be found anywhere +- **Universal** - Small keys work in any dungeon, can be found anywhere, and at least one shop will sell keys (like original Legend of Zelda) + +CLI: `--keyshuffle [none|wild|universal]` + +All other dungeon items (maps, compasses, Big Keys) can be restricted to their own dungeon or shuffled in the general pool. + +### Key Logic Algorithm + +Determines how small key door logic works: + +- **Partial Protection** - Assumes you always have full inventory and worst case usage. Accounts for dark room and bunny revival glitches +- **Strict** - Small key doors require all small keys to be available to be in logic +- **Dangerous** - Assumes you never use keys out of logic (not recommended) + +CLI: `--key_logic [partial|strict|dangerous]` + +### Decouple Doors + +Similar to insanity mode in ER where door entrances and exits are not paired anymore. Tends to remove more logic from dungeons as many rooms will not be required to traverse. + +CLI: `--decoupledoors` + +### Allow Self-Looping Spiral Stairs + +If enabled, spiral stairs are allowed to lead to themselves. + +CLI: `--door_self_loops` + +### Experimental Features + +You will start as a bunny if your spawn point is in the dark world. + +CLI: `--experimental` + +### Crossed Dungeon Specific Settings + +#### Dungeon Chest Counters + +- **Auto** - Picks an appropriate setting based on other settings +- **On** - Dungeon counters on HUD always displayed +- **Off** - Dungeon counters on HUD never displayed +- **On Compass Pickup** - Dungeons with a compass item will display the counter once the compass is found + +CLI: `--dungeon_counters [auto|on|off|pickup]` + +#### Mixed Travel + +Controls Hammerjump, PoD Arena hovering, and Mire Big Key Chest bomb jump: + +- **Prevent (default)** - Rails are added to prevent these tricks. Recommended for those learning crossed dungeon mode +- **Allow** - Rooms are left alone and it is up to player discretion whether to use these tricks +- **Force** - The two disjointed sections are forced to be in the same dungeon but the glitches are never logically required + +CLI: `--mixed_travel [prevent|allow|force]` + +#### Standardize Palettes + +- **Standardize (default)** - Rooms in the same dungeon have their palettes changed to match +- **Original** - Rooms/supertiles keep their original palettes + +CLI: `--standardize_palettes` + +--- + +## Pool Expansions + +### Pottery + +Controls which pots (and large blocks) are in the locations pool: + +- **None** - No pots are in the pool, like normal randomizer +- **Key Pots** - The pots that have keys are in the pool +- **Cave Pots** - The pots that are not found in dungeons are in the pool (includes Spike Cave large block) +- **Cave + Keys Pots** - Both non-dungeon pots and pots that used to have keys +- **Reduced Dungeon Pots** - Cave+Keys plus roughly 25% of dungeon pots (dynamic mode with colored pots) +- **Clustered Dungeon Pots** - Like reduced but pots grouped by logical sets, roughly 50% chosen (dynamic mode) +- **Excludes Empty Pots** - All pots that had some sort of objects under them +- **Dungeon Pots** - The pots that are in dungeons +- **Lottery** - All pots and large blocks are in the pool + +CLI: `--pottery [none|keys|cave|cavekeys|reduced|clustered|nonempty|dungeon|lottery]` + +#### Colorize Pots + +Colors the pots that have been chosen to be part of the location pool. Forced on for dynamic modes (clustered and reduced). + +CLI: `--colorizepots` + +### Shuffle Enemy Drops + +Controls whether enemies that drop items are randomized: + +- **None** - Special enemies drop keys normally +- **Keys** - Enemies that drop keys are added to the randomization pool (includes Hyrule Castle Big Key) +- **Underworld** - Enemies in the underworld are added to the randomization pool. Blue square indicates if there are any enemies on the supertile that still have an available drop + +CLI: `--dropshuffle [none|keys|underworld]` + +### Shopsanity + +Adds 32 shop locations (9 more in retro) to the general location pool. Multi-world supported. + +Shop locations include: +- Lake Hylia Cave Shop (3 items) +- Kakariko Village Shop (3 items) +- Potion Shop (3 new items) +- Paradox Cave Shop (3 items) +- Capacity Upgrade Fairy (2 items) +- Dark Lake Hylia Shop (3 items) +- Curiosity/Red Shield Shop (3 items) +- Dark Lumberjack Shop (3 items) +- Dark Potion Shop (3 items) +- Village of Outcast Hammer Peg Shop (3 items) +- Dark Death Mountain Shop (3 items) + +[See README for complete pricing guide and mechanics](/README#shopsanity) + +### Take Any Caves + +Three options: None, Random, and Fixed. Fixed means take any caves replace specific fairy caves: + +- Desert Healer Fairy +- Swamp Healer Fairy (aka Light Hype Cave) +- Dark Death Mountain Healer Fairy +- Dark Lake Hylia Ledge Healer Fairy (aka Shopping Mall Bomb) +- Bonk Fairy (Dark) + +--- + +## Item Randomization + +### New "Items" + +#### Bomb Bag + +Two bomb bags are added to the item pool (look like +10 Capacity upgrades). Bombs are unable to be used until one is found. + +CLI: `--bombbag` + +#### Pseudo Boots + +Dashing is allowed without the boots item however doors and certain rocks remain unopenable until boots are found. Specific sequence breaks like hovering and water-walking are not allowed until boots are found. + +CLI: `--pseudoboots` + +#### Mirror Scroll + +Mirror is usable inside dungeons. Locations that require the mirror are still unattainable. + +CLI: `--mirrorscroll` + +#### Flute Mode + +- **Normal** - Need to activate it at the village statue after finding it +- **Activated** - Can use it immediately upon finding it + +CLI: `--flute_mode` + +#### Bow Mode + +- **Progressive** - Standard progressive bows +- **Silvers separate** - One bow in the pool and silvers are a separate item +- **Retro (progressive)** - Arrows cost rupees, need to purchase single arrow item at shop +- **Retro + Silvers** - Arrows cost rupees, one bow, silvers are separate item + +CLI: `--bow_mode [progressive|silvers|retro|retro_silvers]` + + + +### Goal Options + +- **Trinity** - Find one of 3 triforces to win (pedestal, Ganon, or Murahdahla with 8 of 10 pieces) +- **Ganonhunt** - Collect the requisite triforce pieces, then defeat Ganon (Aga2 not required) +- **Completionist** - Obtain every item in the game and defeat Ganon (forces 100% accessibility) + +### Item Sorting (Fill Algorithms) + +Controls how items are placed in the world: + +#### Balanced + +Most random distribution of items (recommended). + +#### Vanilla Fill + +Attempts to place all items in their vanilla locations when possible. If not possible, prefers "major" locations, then heart piece locations, then the rest. + +#### Major Location Restriction + +Attempts to place major items in major locations. Major locations are where major items are found in the vanilla game. + +#### Dungeon Restriction + +Attempts to place all major items in dungeons. Overflows to overworld if necessary. + +#### District Restriction + +The world is divided into different regions/districts. Districts are chosen at random and filled with major items. Single green rupees indicate chosen districts. + +CLI: `--algorithm [balanced|vanilla_fill|major_only|dungeon_only|district]` + +### Forbidden Boss Items + +Restrict items that can appear on bosses: + +- **None** - Boss may have any item +- **MapCompass** - Map and compass are logically required to defeat a boss (currently bugged) +- **Dungeon** - Small keys and big keys of the dungeon are not allowed on a boss + +CLI: `--restrict_boss_items [none|mapcompass|dungeon]` + +--- + +## Entrance Randomization + +### New Modes + +- **Lite** - Non item entrances are vanilla, dungeons/caves grouped with restrictions +- **Lean** - Same grouping as Lite but cross-world connections allowed +- **Swapped** - Entrances are swapped with each other + +### Shuffle Links House + +In certain ER shuffles (not dungeonssimple or dungeonsfulls), control whether Links House is shuffled or remains vanilla. + +### Skull Woods Shuffle + +Options to reduce annoying Skull Woods layouts: + +- **Original** - Skull woods shuffles classically amongst itself unless insanity mode +- **Restricted** - Skull woods drops are vanilla, entrances stay in skull woods and are shuffled +- **Loose** - Skull woods drops are vanilla, main ER mode's pool determines handling +- **Followlinked** - Follows Linked Drops setting + +### Linked Drops Override + +Controls whether drops should be linked to nearby entrances: + +- **Unset** - Uses the mode's default (linked for all modes except insanity) +- **Linked** - Forces drops to be linked to their entrances +- **Independent** - Decouples drops from entrances + +### Overworld Map + +Option to move indicators on overworld map to reference dungeon location: + +- **Default** - Showing only the prize markers on vanilla dungeon locations +- **Compass** - Compass item controls whether marker is moved to dungeon locations +- **Map** - Map item shows both prize and location of the dungeon + +CLI: `--overworld_map [default|compass|map]` + +--- + +## Enemizer + +Enemizer has been incorporated into the generator. See [Enemizer in DR documentation](https://docs.google.com/document/d/1iwY7Gy50DR3SsdXVaLFIbx4xRBqo9a-e1_jAl5LMCX8/edit?usp=sharing) for extensive details. + +### Enemy Shuffle + +Enemies are shuffled with an enemy ban list that prevents problematic placements (blocking paths, unavoidable damage, glitches). + +### Enemy Damage + +The shuffled setting actually shuffles the damage table unlike the original enemizer. + +### Boss Shuffle: Unique + +Bosses are shuffled with some exceptions: +- Trinexx is not allowed on GT basement when DR is enabled +- Harder bosses in GT basement have logic concessions for low percentage requirements + +New variant: At least one boss of each type for prize bosses will be present guarding prizes. + +### Enemy Health + +Health is taken into account for challenge rooms if magic or ammo is required. + +### Enemy Logic + +Can account for logical access to challenge rooms and item/key drops when enemies are shuffled: + +- **Forbid special enemies** - Special enemies disallowed from drops and challenge rooms +- **Item drops may have special enemies** - Challenge rooms won't have special enemies, but drops may +- **Allow special enemies anywhere** - Both challenge rooms and enemy drops can have special requirements + +--- + +## Glitched Logic + +CLI: `--logic [noglitches|owglitches|hybridglitches|nologic]` + +### Overworld Glitches + +Includes overworld teleports, clips, superbunny, mirror to access Desert Palace East Entrance, bunny pocket access, etc. + +### Hybrid Major Glitches + +**Not compatible with Door Shuffle** + +Includes all Overworld Glitches logic plus: +- Kikiskip to access PoD without MP or DW access +- IP Lobby clip to skip fire requirement +- Various traversals between dungeons (TT ↔ Desert, Spec rock, Paradox, Mire ↔ Hera ↔ Swamp) +- Stealing SK from Mire to open SP +- Using Mire big key to open Hera doors + +--- + +## Game Options + +### MSU Resume + +Turns on MSU resume support. + +CLI: `--msu_resume` + +### Collection Rate + +Display the collection rate unless the triforce piece counter is needed. + +CLI: `--collection_rate` + +### Reduce Flashing + +Accessibility option to reduce some flashing animations in the game. + +CLI: `--reduce_flashing` + +### Shuffle Sound Effects + +Shuffles a large portion of the sound effects. Can be used with the adjuster. + +CLI: `--shuffle_sfx` + +--- + +## Customizer + +Create custom seeds with custom dungeons and rooms. + +See [Customizer documentation](Customizer.md) for details. + +--- + +## Generation Setup & Miscellaneous + +### Create BPS Patches + +Create BPS patch(es) instead of generating ROM(s) for distribution. + +CLI: `--bps` + +### Triforce Hunt Settings + +Controls for triforce piece pool: +- `--triforce_goal_min` / `--triforce_goal_max` - Pieces to collect to win +- `--triforce_pool_min` / `--triforce_pool_max` - Pieces in item pool +- `--triforce_min_difference` / `--triforce_max_difference` - Difference between pool and goal + +### Seed + +Set a seed number to generate. Same seed with same settings on same version will always yield identical output. + +### Count + +Batch generate multiple seeds with same settings. If seed number provided, it will be used to derive subsequent seeds. diff --git a/index.md b/index.md new file mode 100644 index 00000000..a7b5e9fe --- /dev/null +++ b/index.md @@ -0,0 +1,81 @@ +--- +layout: default +title: Home +nav_order: 1 +--- + +# ALttP Dungeon Randomizer + +**A dungeon randomizer for The Legend of Zelda: A Link to the Past** + +Door Randomizer extends the standard ALttP Entrance Randomizer with advanced dungeon shuffling capabilities that can rearrange dungeon interiors at the room level, creating entirely new dungeon experiences. + +[Download Latest Release](https://github.com/aerinon/ALttPDoorRandomizer/releases/latest){: .btn .btn-primary .fs-5 .mb-4 .mb-md-0 .mr-2 } +[View on GitHub](https://github.com/aerinon/ALttPDoorRandomizer){: .btn .fs-5 .mb-4 .mb-md-0 } + +--- + +## What is Door Randomizer? + +Door Randomizer takes the dungeon experience in A Link to the Past to the next level by shuffling the connections between rooms within dungeons. Instead of just shuffling dungeon entrances, Door Randomizer can: + +- **Shuffle doors within dungeons** - Rooms connect in completely new ways +- **Cross dungeon boundaries** - Walk through a door in Eastern Palace and end up in Tower of Hera +- **Adjust intensity levels** - From basic door shuffling to complete dungeon lobby randomization + +## Key Features + +### Basic Randomizer Features + +- Supports many standard ALttP Randomizer features without needing more advanced settings. + +### Door Shuffle Modes +- **Vanilla** - No shuffling, classic experience +- **Basic** - Doors shuffled within each dungeon +- **Partitioned** - Shuffle in pools (Light World, Early Dark World, Late Dark World) +- **Crossed** - Full shuffle between all dungeons + +### Intensity Levels +- **Level 1** - Normal doors and spiral staircases +- **Level 2** - + open edges and straight staircases +- **Level 3** - + dungeon lobbies + +### Advanced Features +- **Multiple key shuffle modes** - In dungeon, randomized, or universal keys +- **Pottery shuffle** - Randomize items under pots with multiple modes +- **Shopsanity** - 32 shop locations added to item pool +- **Enemy randomization** - Built-in enemizer with logic-aware placement +- **Custom seeds** - Create your own custom dungeons and rooms +- **Fill algorithms** - Multiple item placement strategies (balanced, vanilla fill, major only, district) + +## Quick Start + +1. [Download the latest release](https://github.com/aerinon/ALttPDoorRandomizer/releases/latest) +2. Extract the archive for your platform (Windows, Mac, or Linux) +3. Run `DungeonRandomizer.exe` (or the appropriate platform executable) +4. Configure your settings and generate a seed +5. Apply the patch to your A Link to the Past ROM (must be JP 1.0 version) + +[See detailed installation instructions →](/installation) + +## Community + +Join the discussion and get help: + +- **Discord**: [ALTTP Randomizer Discord](https://discordapp.com/invite/alttprandomizer) + - `#door-rando` - General discussion and help + - `#bug-reports` - Report bugs and issues + +## Learn More + +- [Features Guide](/features) - Comprehensive feature documentation +- [Installation & Usage](/installation) - Setup and running the randomizer +- [Known Issues](/known-issues) - Current bugs and limitations +- [Roadmap](/roadmap) - Future development plans +- [Blog](/blog) - Latest updates and release notes + +## Credits + +Based on the [ALttP Entrance Randomizer](https://github.com/KevinCathcart/ALttPEntranceRandomizer) by KevinCathcart et al. + +See [alttpr.com](https://alttpr.com/) for more details on the original VT randomizer. diff --git a/installation.md b/installation.md new file mode 100644 index 00000000..0c02b8ec --- /dev/null +++ b/installation.md @@ -0,0 +1,369 @@ +--- +layout: default +title: Installation & Usage +nav_order: 3 +--- + +# Installation & Usage +{: .no_toc } + +Complete guide to installing and running ALttP Door Randomizer. +{: .fs-6 .fw-300 } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Requirements + +- Python 3.10 or higher (for running from source) +- A legal copy of The Legend of Zelda: A Link to the Past (JP 1.0 version) ROM + +--- + +## Installation Methods + +### Method 1: Pre-built Releases (Recommended) + +This is the easiest method for most users. + +1. Visit the [Releases page](https://github.com/aerinon/ALttPDoorRandomizer/releases) +2. Download the appropriate build for your system: + - **Windows**: `ALttPDoorRandomizer-{version}-windows.zip` + - **macOS**: `ALttPDoorRandomizer-{version}-osx.tar.gz` + - **Linux**: `ALttPDoorRandomizer-{version}-linux-focal.tar.gz` +3. Extract the archive to a folder of your choice +4. Run the executable: + - **Windows**: `DungeonRandomizer.exe` + - **macOS/Linux**: `./DungeonRandomizer` or `./Gui.py` + +### Method 2: Running from Source + +For developers or users who want to run the latest development version. + +#### Step 1: Clone the Repository + +```bash +git clone https://github.com/aerinon/ALttPDoorRandomizer.git +cd ALttPDoorRandomizer +``` + +For the development branch: +```bash +git checkout DoorDevUnstable +``` + +#### Step 2: Install Python Dependencies + +The project includes a script to install platform-specific dependencies: + +```bash +python resources/ci/common/local_install.py +``` + +This will install all necessary Python packages and platform-specific dependencies. + +#### Step 3: Verify Installation + +Test that everything is working: + +```bash +python DungeonRandomizer.py --help +``` + +You should see the help text with all available options. + +--- + +## Usage + +### GUI (Graphical User Interface) + +The GUI provides an easy-to-use interface for generating seeds. + +#### Starting the GUI + +**Pre-built release:** +- **Windows**: Double-click `DungeonRandomizer.exe` +- **macOS/Linux**: Run `./DungeonRandomizer` from terminal + +**From source:** +```bash +python Gui.py +``` + +#### Using the GUI + +1. **Select your ROM**: Click "Select ROM" and choose your ALttP ROM file +2. **Configure settings**: Use the tabs to adjust: + - **Dungeon Settings**: Door shuffle mode, intensity, key logic + - **Item Settings**: Item placement, shopsanity, pottery + - **Entrance Settings**: Entrance randomization options + - **Enemy Settings**: Enemy shuffle, boss shuffle + - **Game Options**: MSU resume, collection rate, etc. +3. **Generate**: Click "Generate" to create your seed +4. **Output**: The patched ROM will be created in the same directory + +### CLI (Command-Line Interface) + +The CLI offers more control and is useful for batch generation or scripting. + +#### Basic Usage + +```bash +python DungeonRandomizer.py [options] +``` + +#### Common Examples + +**Basic crossed dungeon shuffle:** +```bash +python DungeonRandomizer.py --doorShuffle crossed --intensity 2 +``` + +**With key shuffle and shopsanity:** +```bash +python DungeonRandomizer.py --doorShuffle crossed --keyshuffle wild --shopsanity +``` + +**Pottery and enemy drops:** +```bash +python DungeonRandomizer.py --doorShuffle basic --pottery lottery --dropshuffle underworld +``` + +**Generate without creating ROM (testing):** +```bash +python DungeonRandomizer.py --suppress_rom --spoiler none +``` + +**Create BPS patch instead of ROM:** +```bash +python DungeonRandomizer.py --bps --doorShuffle crossed +``` + +**Batch generation:** +```bash +python DungeonRandomizer.py --doorShuffle crossed --count 10 +``` + +#### Key CLI Arguments + +**Dungeon Settings:** +- `--doorShuffle [vanilla|basic|partitioned|crossed]` - Door shuffle mode +- `--intensity [1|2|3]` - Intensity level +- `--keyshuffle [none|wild|universal]` - Key shuffle mode +- `--key_logic [partial|strict|dangerous]` - Key logic algorithm +- `--door_type_mode [original|big|all|chaos]` - Door type shuffle +- `--trap_door_mode [vanilla|optional|boss|oneway]` - Trap door removal + +**Item Settings:** +- `--algorithm [balanced|vanilla_fill|major_only|dungeon_only|district]` - Item placement +- `--pottery [none|keys|cave|cavekeys|reduced|clustered|nonempty|dungeon|lottery]` - Pot shuffle +- `--dropshuffle [none|keys|underworld]` - Enemy drop shuffle +- `--shopsanity` - Enable shop randomization +- `--bombbag` - Enable bomb bag item +- `--pseudoboots` - Enable pseudo boots + +**Entrance Settings:** +- `--shuffle [vanilla|simple|restricted|full|crossed|insanity|...]` - Entrance shuffle mode +- `--overworld_map [default|compass|map]` - Overworld map mode + +**Enemy Settings:** +- `--enemizercli [none|basic|shuffled]` - Enemy shuffle mode +- `--enemy_damage [default|shuffled|chaos]` - Enemy damage +- `--enemy_health [default|easy|hard]` - Enemy health +- `--shufflebosses [none|basic|normal|chaos|unique]` - Boss shuffle + +**Logic:** +- `--logic [noglitches|owglitches|hybridglitches|nologic]` - Logic mode +- `--mode [open|standard|inverted]` - Game mode +- `--goal [ganon|trinity|ganonhunt|completionist]` - Victory condition + +**Generation:** +- `--seed SEED` - Set seed number +- `--count N` - Generate N seeds +- `--suppress_rom` - Don't create ROM (testing) +- `--bps` - Create BPS patch instead of ROM +- `--spoiler [none|spoiler|playthrough]` - Spoiler log type + +**Game Options:** +- `--msu_resume` - Enable MSU resume +- `--collection_rate` - Display collection rate +- `--reduce_flashing` - Reduce flashing animations +- `--shuffle_sfx` - Shuffle sound effects + +For a complete list of options: +```bash +python DungeonRandomizer.py --help +``` + +--- + +## Multiworld Setup + +Door Randomizer supports multiworld seeds where multiple players play different randomized games with shared item pools. + +### Installing Multiworld Dependencies + +Run the same installation script to install multiworld-specific dependencies: + +```bash +python resources/ci/common/local_install.py +``` + +### Generating Multiworld Seeds + +Create YAML files for each player (see `docs/player1.yml`, `docs/player2.yml`, `docs/player3.yml` for individual examples, or `docs/multi_mystery_example.yaml` for a complete multiworld configuration). + +**Example player configuration:** +```yaml +description: Player 1's World +name: Player1 +doorShuffle: crossed +intensity: 2 +keyshuffle: wild +shopsanity: true +``` + +Generate the multiworld using the multi-mystery configuration file: +```bash +python DungeonRandomizer.py --customizer docs/multi_mystery_example.yaml +``` + +### Running Multiworld Server + +```bash +python MultiServer.py +``` + +It will prompt you for the multidata file created from the multiworld generation step. + +### Connecting with Multiworld Client + +```bash +python MultiClient.py +``` + +Enter the server address and your player name to connect. + +--- + +## Testing & Verification + +### Running the Test Suite + +To verify your installation and test the randomizer logic: + +```bash +python TestSuite.py +``` + +**Test with specific settings:** +```bash +python TestSuite.py --dr basic --count 10 --tense 2 +``` + +**Run specific test modules:** +```bash +python -m pytest test/ +python -m pytest test/dungeons/TestDarkPalace.py +``` + +--- + +## Troubleshooting + +### Common Issues + +#### "Python not found" or "python: command not found" + +- **Windows**: Make sure Python is installed and added to PATH during installation +- **macOS/Linux**: Install Python 3.10+ via your package manager or from [python.org](https://www.python.org/) + +#### "No module named 'yaml'" or similar import errors + +Run the dependency installation script: +```bash +python resources/ci/common/local_install.py +``` + +#### Pre-built executable won't run on macOS + +macOS may block unsigned applications. Right-click the app and select "Open" to bypass the warning. + +#### Pre-built executable won't run on Linux + +Ensure the file has execute permissions: +```bash +chmod +x DungeonRandomizer +``` + +#### GUI window is too small or cut off + +Try running from the command line to see if there are any error messages. Some display scaling settings can cause issues. + +#### Generated ROM doesn't work with emulator + +- Ensure you're using a clean US version of ALttP ROM (Japan 1.0) +- Try a different emulator (recommended: SNES9x, RetroArch with bsnes core) +- Verify the ROM was patched successfully (check file size and modification date) + +### Getting Help + +If you encounter issues: + +1. Check the [Known Issues](/known-issues) page +2. Search [GitHub Issues](https://github.com/aerinon/ALttPDoorRandomizer/issues) +3. Ask in the [ALTTP Randomizer Discord](https://discordapp.com/invite/alttprandomizer): + - `#door-rando` - General help and questions + - `#bug-reports` - Report bugs and issues + +When reporting issues, please include: +- Operating system and version +- Python version (`python --version`) +- Door Randomizer version or commit hash +- Full error message or description of the problem +- Settings used (CLI command or screenshot of GUI) + +--- + +## Advanced Usage + +### Custom Presets + +You can create custom preset files to save your favorite settings combinations. + +See the [Customizer documentation](docs/Customizer.md) for details on creating custom seeds with custom dungeons and rooms. + +### Build from Source (Packaging) + +To create a standalone executable from source: + +See [BUILDING.md](docs/BUILDING.md) for detailed build instructions. + +### Development Workflow + +For contributors: + +1. Fork the repository +2. Create a feature branch from `DoorDevUnstable`: + ```bash + git checkout DoorDevUnstable + git checkout -b my-feature + ``` +3. Make your changes +4. Run tests: `python TestSuite.py` +5. Submit a pull request to `DoorDevUnstable` (not main/master) + +--- + +## Next Steps + +- Read the [Features Guide](/features) for detailed information on all settings +- Check the [Known Issues](/known-issues) page for current limitations +- See the [Roadmap](/roadmap) for upcoming features +- Join the community on [Discord](https://discordapp.com/invite/alttprandomizer) diff --git a/known-issues.md b/known-issues.md new file mode 100644 index 00000000..20093d57 --- /dev/null +++ b/known-issues.md @@ -0,0 +1,373 @@ +--- +layout: default +title: Known Issues +nav_order: 4 +--- + +# Known Issues & Bug Tracking +{: .no_toc } + +Current known issues, bugs, and limitations in ALttP Door Randomizer. +{: .fs-6 .fw-300 } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Reporting Bugs + +Before reporting a bug, please: + +1. Check this page to see if the issue is already known +2. Search [existing GitHub issues](https://github.com/aerinon/ALttPDoorRandomizer/issues) +3. Verify you're using the latest version + +### How to Report + +Report bugs in this location: + +- **Discord**: [ALTTP Randomizer Discord](https://discordapp.com/invite/alttprandomizer) in `#bug-reports` or `#door-rando` channels + +### What to Include + +When reporting a bug, please provide: + +- **Spoiler log** if you have one +- If you don't have a spoiler log, include: + - **Seed number** and **settings used** + - **Version** of Door Randomizer (from GUI or `--version`) +- **Website or Platform** (Windows, macOS, Linux) if generator error, or which website was used +- **Description** of the issue +- **Steps to reproduce** the problem +- **Screenshots or videos** (if applicable) +- Optionally, **Save state** (if gameplay issue) (and which emulator/version) + +--- + +## Known Issues + +### Critical Issues + +#### Dungeon Key HUD indicator + +**Status**: Under investigation + +Dungeon Key HUD indicator shows incorrect total number of keys under certain settings. + + +#### Links House on Death Mountain - no logical way off the mountain + +**Status**: Under investigation + +Some ER generations can have Link's House be on Death Mountain and no good way off the mountain. + + +--- + +### Major Issues + +#### Crossed Dungeon Generation Can Be Slow + +**Status**: Performance optimization needed + +Generating crossed dungeon seeds, especially with high intensity and complex settings, can take a significant amount of time (30 seconds to several minutes). + +**Why**: The dungeon generation algorithm uses constraint satisfaction and local search, which can require many attempts to find valid layouts. + +**Workaround**: Be patient, or reduce intensity/complexity of settings. + +#### Rare Impossible Seeds (Generation Failures) + +**Status**: Occasional, investigating + +In rare cases, the generator may fail to place all items or create a valid dungeon layout. This usually happens with very restrictive settings combinations. + +**Workaround**: Try a different seed number, or slightly adjust settings. + +--- + +### Moderate Issues + +#### Some Enemy Placements Can Be Problematic + +**Status**: Ongoing ban list maintenance + +Despite the enemy ban list, some enemy placements can occasionally cause issues: +- Blocking required paths +- Causing unavoidable damage +- Creating glitchy behavior +- Bush enemies not working correctly +- Wallmasters can cause issues in certain locations + +**Workaround**: Report specific problematic enemy locations so they can be added to the ban list. + +**Note**: Thieves are always unkillable and banned from the entire underworld. + + +--- + +### Minor Issues + +#### Forbidden Boss Items "MapCompass" Mode Bugged + +**Status**: Under investigation + +The `mapcompass` option for `--restrict_boss_items` is currently bugged and not recommended for use. + +**Workaround**: Use `dungeon` option instead, or use `none`. + +#### Swamp Trench 2 Keeps Draining + +**Status**: Known issue + +In some configurations, Swamp Trench 2 continues to drain even after the pot is obtained. + +**Workaround**: None currently. This is a known behavior issue. + +#### Killing Certain Red Guards Kills Music + +**Status**: Known issue + +Killing certain red guards in specific locations causes the music to stop playing. + +**Workaround**: None currently. This is a cosmetic issue that doesn't affect gameplay. + +#### Some Lobbies Exitable During Escape (Standard Mode) + +**Status**: Known issue + +In standard mode, some dungeon lobbies can be exited during the escape sequence when they shouldn't be. + +**Workaround**: Avoid leaving during escape if you want to maintain intended flow. + +--- + +### Logic Issues + +These are cases where the logic may not perfectly match what's actually possible in-game: + +#### GT Crystal Paths Room Logic + +**Status**: Under investigation + +Crystal switch logic for GT Crystal Paths room may not properly account for OHKO scenarios when traversing backwards. + +#### OHKO (One-Hit KO) Logic Issues + +**Status**: Under investigation + +In some configurations, unavoidable damage logic or OHKO scenarios may not be properly accounted for. + +#### Hammerjump Logic and Mixed Travel + +**Status**: Configurable behavior + +Hammerjump and similar tricks (PoD Arena hovering, Mire Big Key bomb jump) can unintentionally put you in another dungeon with the wrong dungeon ID. + +**Setting**: Use `--mixed_travel` to control this behavior: +- `prevent`: Adds rails to prevent tricks (default, recommended for learning) +- `allow`: Up to player discretion +- `force`: Sections forced to same dungeon + +--- + +## Mode-Specific Issues + +### Glitched Logic Modes + +Several issues specific to glitched logic modes (OWG, HMG): + +#### Cave State Issues + +**Status**: Under investigation + +Cave state in GT hearts area may not work correctly in glitched modes. + +#### Rain State Issues + +**Status**: Under investigation + +Rain state handling may have issues in glitched logic modes. + +#### Key/Pot SRAM System + +**Status**: Needs restoration + +Old key/pot SRAM system needs to be restored for glitched modes to work properly. + +### Inverted Mode Issues + +#### Dark Sanctuary Inverted Insanity + +**Status**: Under investigation + +Dark Sanctuary with inverted mode and insanity entrance shuffle may have double entrance issues. + +#### Dark Sanctuary at Tavern Error + +**Status**: In progress + +When Dark Sanctuary is at Tavern location, Link's body position may be incorrect. + +### Entrance Randomization Issues + +#### Dungeon Boss Exits in Non-ER + +**Status**: Under investigation + +Dungeon boss exits may not work correctly when entrance randomization is disabled. + +### Decoupled Doors Issues + +#### Sanctuary Palette Detection + +**Status**: Known issue + +In decoupled doors mode, sanctuary palette doesn't correctly identify adjacent rooms. Uses exits instead of entrances for determination. + +**Workaround**: Use coupled doors or original palette mode. + +### Shopsanity Issues + +#### Output YAML: Shopsanity Potions Missing + +**Status**: Under investigation + +When outputting to YAML format, shopsanity potions may not be correctly included in the item pool representation. + +**Workaround**: Check spoiler log for actual item locations. + +--- + +## Known Limitations & Design Decisions + +These items are **not planned to be fixed** as they are either fundamental limitations, intentional design choices, or affect all ALttP randomizers. + +### Game Engine Limitations + +#### Compass Count Stops Working After Triforce + +After obtaining the Triforce, compass counts no longer function correctly. This is true in all ALttP randomizers and is a base game limitation. + +**Workaround**: Complete dungeon exploration before finishing the game if you need counts. + +#### GT Bosses Don't Respawn + +GT bosses don't respawn after killing them. This is intentional base game behavior. + +### Door Randomizer Design Decisions + +#### Hybrid Major Glitches Not Compatible with Door Shuffle + +Hybrid major glitches logic is not compatible with door shuffle modes. This is a fundamental limitation due to how glitch logic interacts with shuffled dungeon interiors. + +**Workaround**: Use Overworld Glitches logic instead, or disable door shuffle. + +#### Fire Rod Not in Logic for Dark Rooms + +Fire Rod is not in logic for dark rooms in door shuffle mode, as it's too difficult to determine which dark room you're in after doors are shuffled. + +**Note**: This is different from Advanced mode in the vanilla randomizer. + +#### Hammerjump Requiring Mixed Travel Settings + +Hammerjump and similar tricks (PoD Arena hovering, Mire Big Key bomb jump) require `--mixed_travel` settings in crossed mode to allow unintended dungeon transitions. + +#### Blind Maiden/Attic Sequence Always Required + +Blind maiden/attic sequence is required even when boss is shuffled. This maintains story consistency. + +#### Southeast Skull Woods Chest Not Guaranteed Key + +In Entrance Randomizer, the southeast Skull Woods chest is guaranteed to be a small key. In Door Randomizer, this is not guaranteed. This is an intentional change from ER. + +### Technical Limitations + +#### Pottery Lottery Mode: Multiworld Item Limit + +In pottery lottery mode, only 256 items for other players can be placed under pots in your world due to technical constraints in how the game tracks items. + +--- + +## Feature Limitations + +See the [Roadmap](/roadmap) for planned features. + +--- + +## Recently Fixed Issues + +See RELEASENOTES.md for the latest fixes. + +And PastReleaseNotes.md for older fixes. + +--- + +## Performance Issues + +### Memory Usage + +**Issue**: Generator can use significant memory for complex seeds. + +**Most affected by**: +- Multiworld with many players +- Pottery lottery mode + +**Workaround**: Close other applications, or increase available RAM. + +--- + +## Emulator Compatibility + +### Known Issues with Emulators + +#### Older SNES9x versions + +Some older versions (< 1.55) may have graphical glitches with certain custom graphics. + +#### ZSNES + +**Not recommended**: ZSNES is outdated and has known accuracy issues. + +--- + +## Workarounds & Tips + +### If Generation Keeps Failing + +1. Switch to a different door shuffle mode +4. Disable some restrictive settings (strict key logic, dungeon item restrictions) +5. Use balanced fill algorithm instead of district restriction + +### If Seed Seems Impossible + +1. Check spoiler log to verify it's beatable +2. Remember new logic differences (see Features guide) +3. Check for required techniques (boots bonking, crystal switch bombs, etc.) +4. Ask for help in Discord with spoiler log + +### If Performance Is Poor + +1. Use pre-built executable instead of running from source +2. Close other applications +3. Update to latest version +4. Use simpler settings combinations + + +--- + +## Issue Status Tracking + +For real-time status of known issues, check: + +- Discord `#bug-reports` and `#door-rando` channels + +--- + +Last updated: 2026-01-30 + +This page is maintained as issues are discovered and fixed. If you notice an issue not listed here, please report it! diff --git a/roadmap.md b/roadmap.md new file mode 100644 index 00000000..9aaef290 --- /dev/null +++ b/roadmap.md @@ -0,0 +1,234 @@ +--- +layout: default +title: Roadmap +nav_order: 5 +--- + +# Development Roadmap +{: .no_toc } + +Future plans and upcoming features for ALttP Door Randomizer. +{: .fs-6 .fw-300 } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Project Vision + +Door Randomizer aims to provide the most comprehensive and flexible dungeon randomization experience for A Link to the Past while maintaining playability and logical consistency. Our focus is on: + +1. **Robust Generation** - Ensuring seeds are always completable and logical +2. **Feature Richness** - Providing extensive customization options +3. **Community Engagement** - Responding to player feedback and needs +4. **Performance** - Improving generation speed and reliability + +--- + +## Bug Fixes + +Critical and important bugs that need to be addressed: + +### Critical Bugs +- Dungeon Key HUD indicator issue (total is incorrect) +- Links House on DM pathing issues (See OWR solution) + +### Minor Bugs +- Resolve Swamp Trench 2 draining issue (pot obtained but still drains) +- Fix OHKO logic issues - Crystal Path backwards in GT +- Fix MapCompass forbidden boss items mode (currently bugged) +- Killing Certain Red Guards Kills Music + +--- + +## Logic & Generation Improvements + +Improving dungeon generation algorithms and logic: + +- Enhanced key logic algorithms (experimental mode in New Generation) +- Rework Uncle weapon/Escape small key +- Make Sanctuary heart the logical one (simplify sanctuary heart logic) +- Investigate rupee balancing issues causing failed generations + +--- + +## New Features + +New functionality and game modes, mostly planned for the NewGeneration branch: + +- Portal placement flexibility (customizability and mixed dungeon pools) +- Smarter trap door placement (reduce forced mirroring) +- DR Keysanity menu improvements +- PitWarp feature +- Door Type pooling customizations +- OWR ER standardization (District ER, Links on Death Mountain issue) +- NewSwappedAlgorithm integration +- Bonk Item Pool from Overworld Randomizer (downstream integration) +- A better hint system +- Prize Shuffle from Overworld Randomizer (downstream integration) +- Map Screen from GK Randomizer (need to investigate) +- Other downstream integrations? (custom goals, follower shuffle, flute shuffle?) + +--- + +## Quality of Life Enhancements + +Improving user experience and usability: + +- Preferred location groups (control item placement with warnings/failures/retries) +- Preset support +- Control output ROM name (customize generated ROM filename) +- Progress indicators for generations +- Better entrance tracking for trackers (investigate race legality) +- Super-tile split palettes (reduce hinting from palette colors) +- Turn off Bob by default for enemizer (fewer graphical glitches) + +--- + +## Performance Improvements + +Making generation faster and more efficient: + +- Reduce memory footprint + +--- + +## Customization & Content Creation + +Tools and features for creating custom content: + +- Re-design customizer file format (improved YAML structure) +- Presets for customizer (start from predefined base) +- Easier enemy customization (simpler interface for customizing specific enemies - better integration with customization system) + +--- + +## Multiworld + +Enhancements for multiplayer seeds: + +- Multiworld documentation +- Ability to have similar randomizations across multiworlds (makes parts of it co-op) + +--- + +## External Tools & Integration + +Integration with external tools and platforms: + +- Tracker integration features +- Web-based API + +--- + +## Technical Debt & Refactoring + +Behind-the-scenes code improvements: + +- Modernize Rules.py (improve readability and maintainability) +- Python upgrade (3.10 deprecated in 2026) + +--- + +## Research & Exploration + +Experimental ideas that may or may not be implemented: + +### Dungeon Randomization Ideas + +- **Subroom randomization** - Randomize smaller rooms within a larger room (subtile vs. supertile doors) +- **Euclidean dungeon layouts** - Dungeon randomization that makes spatial sense (rooms connected geographically) +- **Swapped dungeon generation** - Create dungeons by swapping door destinations (requires new algorithms) + +--- + +## Branch Strategy + +### Current Branches + +- **NewGeneration** - Active development branch for new generation system +- **NewSwappedAlgorithm** - Experimental branch for swapped item algorithm +- **DoorDevUnstable** - Main branch, maintenance mode - only bug fixes and patches (ready to be moved to official stable) +- **DoorDev** - Stale development branch (needs update) +- **Dev/Master** - Do NOT use for PRs. Used by upstream only. + +### Planned Branch Changes + +- Transition from DoorDevUnstable to DoorDev as main maintenance branch +- Transition NewGeneration to main development branch + +--- + +## Completed Features + +Recent accomplishments: + +- ✅ Dynamic colored pots +- ✅ Free lamp cone +- ✅ Experimental key logic improvements + +--- + +## How to Contribute + +Want to help shape the future of Door Randomizer? + +### For Players +- **Test seeds** - Play with different settings and report issues +- **Provide feedback** - Share what you like and what could be better +- **Suggest features** - Tell us what you want to see + +### For Developers +- **Submit PRs to DoorDevUnstable** +- **Join Discord** - Discuss development in `#door-rando` +- **Write tests** - Improve test coverage +- **Improve documentation** - Help others understand the code + +### For Creators +- **Custom rooms** - Design interesting custom rooms +- **Presets** - Create balanced preset configurations +- **Tutorials** - Teach others how to use Door Rando +- **Tools** - Build complementary tools (trackers, analyzers) + +--- + +## Release Schedule + +### Regular Releases +- **Unstable builds** - Automated on every push to DoorDevUnstable +- **Hotfixes** - As needed for critical bugs + +### Upcoming Releases (Tentative) +- **v1.5.x** (Q2 2026) - DoorDevUnstable becomes DoorDev +- **v1.6** (Q3 2026) - Python upgrade +- **v2.0** (Q4 2026) - New Generation branch release + +--- + +## Success Metrics + +How we measure progress: + +### Technical Metrics +- Generation success rate (target: >95%) +- Average generation time (target: <10s for crossed seeds) +- Memory usage (start measuring) +- Test coverage (start measuring) + +--- + +## Feedback & Discussion + +Have thoughts on the roadmap? + +- **Discord**: Join `#door-rando` to discuss + +--- + +*This roadmap is a living document and will be updated regularly as priorities shift and progress is made.* + +Last updated: 2026-01-30 From e27fb51dc9280242608f2b31a9895ccb19dce15a Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 30 Jan 2026 10:09:22 -0700 Subject: [PATCH 11/76] deploy: github action site deploy --- .github/workflows/deploy-pages.yml | 81 ++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 .github/workflows/deploy-pages.yml diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml new file mode 100644 index 00000000..23a1ada4 --- /dev/null +++ b/.github/workflows/deploy-pages.yml @@ -0,0 +1,81 @@ +# GitHub Pages Deployment Workflow +name: 📄 Deploy GitHub Pages + +# Deploy on push to DoorDevUnstable branch +on: + push: + branches: + - DoorDevUnstable + # Allow manual trigger + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + # Build job + build: + name: 🔨 Build Site + runs-on: ubuntu-latest + steps: + - name: ✔️ Checkout + uses: actions/checkout@v4 + + - name: 💎 Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.1' + bundler-cache: false + + - name: 💿 Install dependencies + run: | + gem install bundler + bundle config set --local path 'vendor/bundle' + # Create Gemfile if it doesn't exist + if [ ! -f Gemfile ]; then + cat > Gemfile << 'EOF' + source 'https://rubygems.org' + gem 'github-pages', group: :jekyll_plugins + gem 'jekyll-feed' + gem 'jekyll-seo-tag' + gem 'jekyll-sitemap' + gem 'just-the-docs' + EOF + fi + bundle install + + - name: 🔨 Build with Jekyll + run: bundle exec jekyll build --baseurl "/ALttPDoorRandomizer" + env: + JEKYLL_ENV: production + + - name: 📤 Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./_site + + # Deployment job + deploy: + name: 🚀 Deploy to GitHub Pages + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: 🚀 Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 + + - name: ✅ Deployment complete + run: | + echo "🎉 GitHub Pages deployed successfully!" + echo "📄 Site URL: ${{ steps.deployment.outputs.page_url }}" From 87ba33852f84a13f64b1392f8c8343d4cb4caec4 Mon Sep 17 00:00:00 2001 From: Catobat <69204835+Catobat@users.noreply.github.com> Date: Sun, 1 Feb 2026 12:15:08 +0100 Subject: [PATCH 12/76] Refactor how large screen quadrants are stored in grids --- source/overworld/LayoutGenerator.py | 174 ++++++++------- source/overworld/LayoutVisualizer.py | 307 +++++++++++++-------------- 2 files changed, 252 insertions(+), 229 deletions(-) diff --git a/source/overworld/LayoutGenerator.py b/source/overworld/LayoutGenerator.py index 467941ab..d2245ac7 100644 --- a/source/overworld/LayoutGenerator.py +++ b/source/overworld/LayoutGenerator.py @@ -1,4 +1,3 @@ -import copy import logging import RaceRandom as random import random as _random @@ -8,7 +7,7 @@ from OverworldShuffle import connect_two_way, validate_layout ENABLE_KEEP_SIMILAR_SPECIAL_HANDLING = False PREVENT_WRAPPED_LARGE_SCREENS = False -DRAW_IMAGE = False +DRAW_IMAGE = True large_screen_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35] + [0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75] @@ -49,7 +48,7 @@ class WorldPiece: def __init__( self, - screens: List[List[Optional[Screen]]], + screens: Optional[List[List[Optional[Screen]]]] = None, grid: Optional[List[List[int]]] = None, width: int = 0, height: int = 0, @@ -62,7 +61,7 @@ class WorldPiece: west_edges_water: Optional[List[List[List[OWEdge]]]] = None, east_edges_water: Optional[List[List[List[OWEdge]]]] = None ): - self.screens = screens + self.screens = screens if screens is not None else [] self.grid = grid if grid is not None else [] self.width = width self.height = height @@ -154,7 +153,7 @@ class LayoutGeneratorOptions: """ Configuration options for layout generation. """ - __slots__ = ('horizontal_wrap', 'vertical_wrap', + __slots__ = ('horizontal_wrap', 'vertical_wrap', 'split_large_screens', 'large_screen_pool', 'distortion_chance', 'random_order', 'multi_choice', 'max_delay', 'first_ignore_bonus_points', 'penalty_full_edge_mismatch', 'penalty_partial_edge_mismatch', 'bonus_partial_edge_match', @@ -168,6 +167,7 @@ class LayoutGeneratorOptions: self, horizontal_wrap: bool = True, vertical_wrap: bool = True, + split_large_screens = False, large_screen_pool: bool = False, distortion_chance: float = 0.0, random_order: int = 0, @@ -194,6 +194,7 @@ class LayoutGeneratorOptions: ): self.horizontal_wrap = horizontal_wrap self.vertical_wrap = vertical_wrap + self.split_large_screens = split_large_screens self.large_screen_pool = large_screen_pool self.distortion_chance = distortion_chance self.random_order = random_order @@ -418,8 +419,8 @@ def create_piece_list(world: World, player: int, options: LayoutGeneratorOptions if castle_screen and central_bonk_screen and links_house_screen: piece = create_piece(world, player, [ - [0x1B, 0x1B], - [0x1B, 0x1B], + [0x1B, 0x1C], + [0x23, 0x24], [0x2B, 0x2C] ], overworld_screens) @@ -439,10 +440,18 @@ def create_piece_list(world: World, player: int, options: LayoutGeneratorOptions # Add large screens for screen in all_large_screens: if screen not in used_screens_set: - piece = create_piece(world, player, [[screen.id, screen.id], [screen.id, screen.id]], overworld_screens) - if options.large_screen_pool: - piece.restriction = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35] - piece_list.append(piece) + base_id = screen.id + if options.split_large_screens: + for quadrant_offset in [0x00, 0x01, 0x08, 0x09]: + piece = create_piece(world, player, [[base_id + quadrant_offset]], overworld_screens) + if options.large_screen_pool: + piece.restriction = [large_screen_id + quadrant_offset for large_screen_id in [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35]] + piece_list.append(piece) + else: + piece = create_piece(world, player, [[base_id, base_id + 0x01], [base_id + 0x08, base_id + 0x09]], overworld_screens) + if options.large_screen_pool: + piece.restriction = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35] + piece_list.append(piece) used_screens_set.add(screen) if world.owParallel[player]: used_screens_set.add(screen.parallel) @@ -478,43 +487,50 @@ def create_piece_list(world: World, player: int, options: LayoutGeneratorOptions for k in range(piece.height): for l in range(piece.width): piece.crossed_groups[k].append(-1) - screen_id = piece.main.grid[k][l] - if screen_id != -1: - piece.crossed_groups[k][l] = 1 if screen_id in crossed_group_b else 0 + screen = piece.main.screens[k][l] + if screen: + piece.crossed_groups[k][l] = 1 if screen.id in crossed_group_b else 0 else: if piece.parallel and piece.parallel.screens[k][l]: - piece.crossed_groups[k][l] = 1 if piece.parallel.grid[k][l] in crossed_group_b else 0 + piece.crossed_groups[k][l] = 1 if piece.parallel.screens[k][l].id in crossed_group_b else 0 return piece_list def create_piece(world: World, player: int, grid: List[List[int]], overworld_screens: Dict[int, Screen]) -> Piece: """ - Create piece from grid of screen IDs - Takes 2D array of screen IDs and creates main and parallel pieces + Create piece from grid of cell IDs + Takes 2D array of cell IDs and creates main and parallel pieces """ piece = Piece( - main=WorldPiece(screens=[]), + main=WorldPiece(), width=len(grid[0]), height=len(grid) ) if world.owParallel[player]: - piece.parallel = WorldPiece(screens=[]) + piece.parallel = WorldPiece() found_screens = set() for i in range(piece.height): new_row = [] + new_screen_row = [] new_row_parallel = [] - piece.main.screens.append(new_row) + new_screen_row_parallel = [] + piece.main.grid.append(new_row) + piece.main.screens.append(new_screen_row) if world.owParallel[player]: - piece.parallel.screens.append(new_row_parallel) + piece.parallel.grid.append(new_row_parallel) + piece.parallel.screens.append(new_screen_row_parallel) for j in range(piece.width): - screen = overworld_screens.get(grid[i][j]) - new_row.append(screen) - if world.owParallel[player]: - new_row_parallel.append(screen.parallel if screen else None) + cell_id = grid[i][j] + new_row.append(cell_id) + screen = overworld_screens.get(get_screen_id_from_cell(cell_id)) + new_screen_row.append(screen) + if world.owParallel[player] and screen: + new_row_parallel.append(cell_id - screen.id + screen.parallel.id) + new_screen_row_parallel.append(screen.parallel) if screen and screen not in found_screens: found_screens.add(screen) @@ -532,14 +548,14 @@ def create_piece(world: World, player: int, grid: List[List[int]], overworld_scr def add_piece_data(world: World, player: int, piece: Piece, large_screen_quadrant_info: Dict[int, Dict], large_screen_quadrant_info_land: Dict[int, Dict], large_screen_quadrant_info_water: Dict[int, Dict]) -> None: """ Add computed data to piece - Calls add_piece_grid_info for main and parallel pieces + Calls add_world_piece_edge_info for main and parallel pieces """ num_pieces = 2 if piece.parallel else 1 for p in range(num_pieces): world_piece = piece.main if p == 0 else piece.parallel - world_piece.width = len(world_piece.screens[0]) - world_piece.height = len(world_piece.screens) - add_piece_grid_info(world, player, world_piece, large_screen_quadrant_info, large_screen_quadrant_info_land, large_screen_quadrant_info_water) + world_piece.width = piece.width + world_piece.height = piece.height + add_world_piece_edge_info(world, player, world_piece, large_screen_quadrant_info, large_screen_quadrant_info_land, large_screen_quadrant_info_water) piece.width = piece.main.width piece.height = piece.main.height @@ -572,12 +588,11 @@ def add_piece_data(world: World, player: int, piece: Piece, large_screen_quadran piece.edge_sides = 0 piece.max_edges_per_side = 0 -def add_piece_grid_info(world: World, player: int, piece: WorldPiece, large_screen_quadrant_info: Dict[int, Dict], large_screen_quadrant_info_land: Dict[int, Dict], large_screen_quadrant_info_water: Dict[int, Dict]) -> None: +def add_world_piece_edge_info(world: World, player: int, piece: WorldPiece, large_screen_quadrant_info: Dict[int, Dict], large_screen_quadrant_info_land: Dict[int, Dict], large_screen_quadrant_info_water: Dict[int, Dict]) -> None: """ Populate piece edge information Initializes 8x8 edge arrays and extracts edges from screens """ - piece.grid = [[] for _ in range(8)] piece.north_edges = [[] for _ in range(8)] piece.south_edges = [[] for _ in range(8)] piece.west_edges = [[] for _ in range(8)] @@ -591,7 +606,6 @@ def add_piece_grid_info(world: World, player: int, piece: WorldPiece, large_scre for k in range(piece.height): for l in range(piece.width): - piece.grid[k].append(piece.screens[k][l].id if piece.screens[k][l] else -1) piece.north_edges[k].append([]) piece.south_edges[k].append([]) piece.west_edges[k].append([]) @@ -603,38 +617,41 @@ def add_piece_grid_info(world: World, player: int, piece: WorldPiece, large_scre piece.west_edges_water[k].append([]) piece.east_edges_water[k].append([]) - done_large = set() for k in range(piece.height): for l in range(piece.width): screen = piece.screens[k][l] if not screen: continue + cell_id = piece.grid[k][l] + if screen.big: - if screen.id not in done_large: - done_large.add(screen.id) - quadrant_info = (large_screen_quadrant_info[screen.id] if world.owTerrain[player] - else large_screen_quadrant_info_land[screen.id]) + # Determine quadrant by subtracting cell ID from screen ID + # 0x00 = NW (top-left), 0x01 = NE (top-right), 0x08 = SW (bottom-left), 0x09 = SE (bottom-right) + quadrant_offset = cell_id - screen.id + if quadrant_offset == 0x00: + quadrant_name = "NW" + elif quadrant_offset == 0x01: + quadrant_name = "NE" + elif quadrant_offset == 0x08: + quadrant_name = "SW" + else: + quadrant_name = "SE" - piece.north_edges[k][l] = [e for e in quadrant_info["NW"][Direction.North] if not e.dest] - piece.north_edges[k][l + 1] = [e for e in quadrant_info["NE"][Direction.North] if not e.dest] - piece.south_edges[k + 1][l] = [e for e in quadrant_info["SW"][Direction.South] if not e.dest] - piece.south_edges[k + 1][l + 1] = [e for e in quadrant_info["SE"][Direction.South] if not e.dest] - piece.west_edges[k][l] = [e for e in quadrant_info["NW"][Direction.West] if not e.dest] - piece.west_edges[k + 1][l] = [e for e in quadrant_info["SW"][Direction.West] if not e.dest] - piece.east_edges[k][l + 1] = [e for e in quadrant_info["NE"][Direction.East] if not e.dest] - piece.east_edges[k + 1][l + 1] = [e for e in quadrant_info["SE"][Direction.East] if not e.dest] + quadrant_info = (large_screen_quadrant_info[screen.id] if world.owTerrain[player] + else large_screen_quadrant_info_land[screen.id]) - if not world.owTerrain[player]: - quadrant_info = large_screen_quadrant_info_water[screen.id] - piece.north_edges_water[k][l] = [e for e in quadrant_info["NW"][Direction.North] if not e.dest] - piece.north_edges_water[k][l + 1] = [e for e in quadrant_info["NE"][Direction.North] if not e.dest] - piece.south_edges_water[k + 1][l] = [e for e in quadrant_info["SW"][Direction.South] if not e.dest] - piece.south_edges_water[k + 1][l + 1] = [e for e in quadrant_info["SE"][Direction.South] if not e.dest] - piece.west_edges_water[k][l] = [e for e in quadrant_info["NW"][Direction.West] if not e.dest] - piece.west_edges_water[k + 1][l] = [e for e in quadrant_info["SW"][Direction.West] if not e.dest] - piece.east_edges_water[k][l + 1] = [e for e in quadrant_info["NE"][Direction.East] if not e.dest] - piece.east_edges_water[k + 1][l + 1] = [e for e in quadrant_info["SE"][Direction.East] if not e.dest] + piece.north_edges[k][l] = [e for e in quadrant_info[quadrant_name][Direction.North] if not e.dest] + piece.south_edges[k][l] = [e for e in quadrant_info[quadrant_name][Direction.South] if not e.dest] + piece.west_edges[k][l] = [e for e in quadrant_info[quadrant_name][Direction.West] if not e.dest] + piece.east_edges[k][l] = [e for e in quadrant_info[quadrant_name][Direction.East] if not e.dest] + + if not world.owTerrain[player]: + quadrant_info_water = large_screen_quadrant_info_water[screen.id] + piece.north_edges_water[k][l] = [e for e in quadrant_info_water[quadrant_name][Direction.North] if not e.dest] + piece.south_edges_water[k][l] = [e for e in quadrant_info_water[quadrant_name][Direction.South] if not e.dest] + piece.west_edges_water[k][l] = [e for e in quadrant_info_water[quadrant_name][Direction.West] if not e.dest] + piece.east_edges_water[k][l] = [e for e in quadrant_info_water[quadrant_name][Direction.East] if not e.dest] else: for edge in sorted(screen.edges.values(), key=lambda e: e.midpoint): if not edge.dest: @@ -651,6 +668,19 @@ def add_piece_grid_info(world: World, player: int, piece: WorldPiece, large_scre target = piece.east_edges[k][l] if world.owTerrain[player] or edge.terrain != Terrain.Water else piece.east_edges_water[k][l] target.append(edge) +def get_screen_id_from_cell(cell_id: int) -> int: + """Get the base screen ID from a cell ID. + + For large screens, returns the top-left corner ID. + For small screens, returns the cell ID unchanged. + """ + base_id = cell_id & 0xBF # Remove world bit if present + # Check if this is a quadrant of a large screen + for large_id in large_screen_ids: + if base_id in [large_id, large_id + 0x01, large_id + 0x08, large_id + 0x09]: + return large_id | (cell_id & 0x40) # Preserve world bit + return cell_id + # ============================================================================ # PLACEMENT ALGORITHM # ============================================================================ @@ -1316,11 +1346,22 @@ def format_grid_for_spoiler(grid: List[List[int]]) -> str: return "\n".join(lines) def is_same_large_screen(grid: List[List[int]], row1: int, col1: int, row2: int, col2: int) -> bool: - id1 = grid[row1 % 8][col1 % 8] - id2 = grid[row2 % 8][col2 % 8] + """Checks if two adjacent cells belong to the same large screen with correct quadrant positions.""" + id1, id2 = grid[row1 % 8][col1 % 8], grid[row2 % 8][col2 % 8] if id1 == -1 or id2 == -1: return False - return id1 == id2 and id1 in large_screen_ids + base1, base2 = get_screen_id_from_cell(id1), get_screen_id_from_cell(id2) + if base1 != base2 or base1 not in large_screen_ids: + return False + # Get quadrant offsets (0x00=NW, 0x01=NE, 0x08=SW, 0x09=SE) + q1, q2 = (id1 & 0xBF) - (base1 & 0xBF), (id2 & 0xBF) - (base2 & 0xBF) + # Swap if cell2 is before cell1 + if col1 > col2 or row1 > row2: + q1, q2 = q2, q1 + # Check valid adjacency: east (0x00->0x01, 0x08->0x09) or south (0x00->0x08, 0x01->0x09) + if col1 != col2: + return (q1, q2) in [(0x00, 0x01), (0x08, 0x09)] + return (q1, q2) in [(0x00, 0x08), (0x01, 0x09)] # ============================================================================ # MAIN EXECUTION @@ -1347,6 +1388,7 @@ def generate_random_grid_layout(world: World, player: int, connected_edges: List options = LayoutGeneratorOptions( horizontal_wrap=horizontal_wrap, vertical_wrap=vertical_wrap, + split_large_screens=False, large_screen_pool=False, distortion_chance=0.0, random_order=6 if world.owParallel[player] else 12, @@ -1385,19 +1427,9 @@ def generate_random_grid_layout(world: World, player: int, connected_edges: List connect_edges_for_screen_layout(world, player, result.grid_info, options, connected_edges, prio_edges, overworld_screens, True) grid = result.grid_info.grid - # Make new grid containing cell IDs for the overworld map - map_grid = copy.deepcopy(grid) - for w in range(2): - for i in range(8): - for j in range(8): - screen_id = map_grid[w][i][j] - if screen_id in large_screen_ids and map_grid[w][i][(j + 1) % 8] == screen_id and map_grid[w][(i + 1) % 8][j] == screen_id and map_grid[w][(i + 1) % 8][(j + 1) % 8] == screen_id: - map_grid[w][i][(j + 1) % 8] = screen_id + 0x01 - map_grid[w][(i + 1) % 8][j] = screen_id + 0x08 - map_grid[w][(i + 1) % 8][(j + 1) % 8] = screen_id + 0x09 - world.owgrid[player] = map_grid - world.owlayoutmap_lw[player] = {id & 0xBF: i for i, id in enumerate(sum(map_grid[0], []))} - world.owlayoutmap_dw[player] = {id & 0xBF: i for i, id in enumerate(sum(map_grid[1], []))} + world.owgrid[player] = grid + world.owlayoutmap_lw[player] = {id & 0xBF: i for i, id in enumerate(sum(grid[0], []))} + world.owlayoutmap_dw[player] = {id & 0xBF: i for i, id in enumerate(sum(grid[1], []))} world.spoiler.set_map('layout_grid_lw', format_grid_for_spoiler(grid[0]), grid[0], player) if not world.owParallel[player]: diff --git a/source/overworld/LayoutVisualizer.py b/source/overworld/LayoutVisualizer.py index 8dbdea34..f76ddc58 100644 --- a/source/overworld/LayoutVisualizer.py +++ b/source/overworld/LayoutVisualizer.py @@ -4,7 +4,18 @@ from datetime import datetime from typing import Dict, List from PIL import Image, ImageDraw from BaseClasses import Direction, OWEdge -from source.overworld.LayoutGenerator import Screen +from source.overworld.LayoutGenerator import Screen, get_screen_id_from_cell + +def get_quadrant_from_cell_id(cell_id: int, screen_id: int) -> str: + offset = (cell_id & 0xBF) - (screen_id & 0xBF) + if offset == 0x00: + return "NW" + elif offset == 0x01: + return "NE" + elif offset == 0x08: + return "SW" + else: + return "SE" def get_edge_lists(grid: List[List[List[int]]], overworld_screens: Dict[int, Screen], @@ -13,7 +24,7 @@ def get_edge_lists(grid: List[List[List[int]]], Get list of edges for each cell and direction. Args: - grid: 3D list [world][row][col] containing screen IDs + grid: 3D list [world][row][col] containing cell IDs overworld_screens: Dict of screen_id -> Screen objects large_screen_quadrant_info: Dict of screen_id -> quadrant info for large screens @@ -24,47 +35,27 @@ def get_edge_lists(grid: List[List[List[int]]], GRID_SIZE = 8 edge_lists = {} - # Large screen base IDs - large_screen_base_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35, - 0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75] - for world_idx in range(2): - # Build a map of screen_id -> list of (row, col) positions for large screens - large_screen_positions = {} for row in range(GRID_SIZE): for col in range(GRID_SIZE): - screen_id = grid[world_idx][row][col] - if screen_id != -1 and screen_id in large_screen_base_ids: - if screen_id not in large_screen_positions: - large_screen_positions[screen_id] = [] - large_screen_positions[screen_id].append((row, col)) + cell_id = grid[world_idx][row][col] - for row in range(GRID_SIZE): - for col in range(GRID_SIZE): - screen_id = grid[world_idx][row][col] - - if screen_id == -1: + if cell_id == -1: # Empty cell - no edges for direction in [Direction.North, Direction.South, Direction.East, Direction.West]: edge_lists[(world_idx, row, col, direction)] = [] continue + screen_id = get_screen_id_from_cell(cell_id) screen = overworld_screens.get(screen_id) if not screen: for direction in [Direction.North, Direction.South, Direction.East, Direction.West]: edge_lists[(world_idx, row, col, direction)] = [] continue - is_large = screen_id in large_screen_base_ids - - if is_large: - # For large screens, determine which quadrant this cell is - # Find all positions of this large screen and determine quadrant - positions = large_screen_positions.get(screen_id, [(row, col)]) - - # Determine quadrant by finding relative position - # The quadrant is determined by which cells are adjacent - quadrant = determine_large_screen_quadrant(row, col, positions, GRID_SIZE) + if screen.big: + # For large screens, determine quadrant from cell ID + quadrant = get_quadrant_from_cell_id(cell_id, screen_id) # Get edges for this quadrant if screen_id in large_screen_quadrant_info: @@ -85,45 +76,6 @@ def get_edge_lists(grid: List[List[List[int]]], return edge_lists -def determine_large_screen_quadrant(row: int, col: int, positions: List[tuple], grid_size: int) -> str: - """ - Determine which quadrant (NW, NE, SW, SE) a cell is in for a large screen. - Handles wrapping correctly by checking adjacency patterns. - - Args: - row: Current cell row - col: Current cell column - positions: List of all (row, col) positions for this large screen - grid_size: Size of the grid (8) - - Returns: - Quadrant string: "NW", "NE", "SW", or "SE" - """ - positions_set = set(positions) - - # Check which adjacent cells also belong to this large screen - has_right = ((row, (col + 1) % grid_size) in positions_set) - has_below = (((row + 1) % grid_size, col) in positions_set) - has_left = ((row, (col - 1) % grid_size) in positions_set) - has_above = (((row - 1) % grid_size, col) in positions_set) - - # Determine quadrant based on adjacency - # NW: has right and below neighbors - # NE: has left and below neighbors - # SW: has right and above neighbors - # SE: has left and above neighbors - - if has_right and has_below: - return "NW" - elif has_left and has_below: - return "NE" - elif has_right and has_above: - return "SW" - elif has_left and has_above: - return "SE" - else: - raise Exception("?") - def is_crossed_edge(edge: OWEdge, overworld_screens: Dict[int, Screen]) -> bool: if edge.dest is None: return False @@ -132,6 +84,40 @@ def is_crossed_edge(edge: OWEdge, overworld_screens: Dict[int, Screen]) -> bool: dest_screen = overworld_screens.get(edge.dest.owIndex) return source_screen.dark_world != dest_screen.dark_world +def are_large_screen_cells_connected(cell_id1: int, cell_id2: int, quadrant1: str, quadrant2: str, direction: str) -> bool: + """ + Check if two cells of a large screen are connected (should have no border between them). + + For cells to be connected: + 1. They must be from the same large screen (same base screen ID) + 2. Their quadrants must be adjacent in the expected direction + + Args: + cell_id1: Cell ID of the first cell + cell_id2: Cell ID of the second cell + quadrant1: Quadrant of the first cell ("NW", "NE", "SW", "SE") + quadrant2: Quadrant of the second cell + direction: Direction from cell1 to cell2 ("east", "south") + + Returns: + True if the cells should have no border between them + """ + # Must be from the same large screen + screen_id1 = get_screen_id_from_cell(cell_id1) + screen_id2 = get_screen_id_from_cell(cell_id2) + if screen_id1 != screen_id2: + return False + + # Check if quadrants are properly adjacent + if direction == "east": + # For east connection: NW->NE or SW->SE + return (quadrant1 == "NW" and quadrant2 == "NE") or (quadrant1 == "SW" and quadrant2 == "SE") + elif direction == "south": + # For south connection: NW->SW or NE->SE + return (quadrant1 == "NW" and quadrant2 == "SW") or (quadrant1 == "NE" and quadrant2 == "SE") + + return False + def visualize_layout(grid: List[List[List[int]]], output_dir: str, overworld_screens: Dict[int, Screen], large_screen_quadrant_info: Dict[int, Dict]) -> None: @@ -162,78 +148,42 @@ def visualize_layout(grid: List[List[List[int]]], output_dir: str, output_height = world_height output_img = Image.new('RGB', (output_width, output_height), color='black') - # Large screen base IDs (defined once for reuse) - large_screen_base_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35, - 0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75] - # Process both worlds for world_idx in range(2): x_offset = 0 if world_idx == 0 else (world_width + gap) - # Build a map of screen_id -> list of (row, col) positions for large screens - large_screen_positions = {} - for row in range(GRID_SIZE): - for col in range(GRID_SIZE): - screen_id = grid[world_idx][row][col] - if screen_id != -1 and screen_id in large_screen_base_ids: - if screen_id not in large_screen_positions: - large_screen_positions[screen_id] = [] - large_screen_positions[screen_id].append((row, col)) - # Process each cell in the grid individually - # This handles wrapped large screens correctly by drawing each quadrant separately for row in range(GRID_SIZE): for col in range(GRID_SIZE): - screen_id = grid[world_idx][row][col] + cell_id = grid[world_idx][row][col] - if screen_id == -1: + if cell_id == -1: # Empty cell - fill with black (already black from initialization) continue - is_large = screen_id in large_screen_base_ids + screen_id = get_screen_id_from_cell(cell_id) + screen = overworld_screens.get(screen_id) + if not screen: + continue - # Calculate source position in the world image - source_row = (screen_id % 0x40) >> 3 - source_col = screen_id % 0x08 - world_img = lightworld_img if screen_id < 0x40 else darkworld_img + is_large = screen.big - if is_large: - # For large screens, determine which quadrant this cell represents - positions = large_screen_positions.get(screen_id, [(row, col)]) - quadrant = determine_large_screen_quadrant(row, col, positions, GRID_SIZE) + # Calculate source position in the world image based on cell_id + # For large screens, cell_id already encodes the quadrant position + source_row = (cell_id % 0x40) >> 3 + source_col = cell_id % 0x08 + world_img = lightworld_img if cell_id < 0x40 else darkworld_img - # Map quadrant to source offset within the 2x2 large screen - quadrant_offsets = { - "NW": (0, 0), - "NE": (1, 0), - "SW": (0, 1), - "SE": (1, 1) - } - q_col_offset, q_row_offset = quadrant_offsets[quadrant] + source_x = source_col * SOURCE_CELL_SIZE + source_y = source_row * SOURCE_CELL_SIZE - # Calculate source position for this quadrant - source_x = (source_col + q_col_offset) * SOURCE_CELL_SIZE - source_y = (source_row + q_row_offset) * SOURCE_CELL_SIZE - - # Crop single cell from source (the specific quadrant) - cropped = world_img.crop(( - source_x, - source_y, - source_x + SOURCE_CELL_SIZE, - source_y + SOURCE_CELL_SIZE - )) - else: - # Small screen (1x1) - source_x = source_col * SOURCE_CELL_SIZE - source_y = source_row * SOURCE_CELL_SIZE - - # Crop single cell from source - cropped = world_img.crop(( - source_x, - source_y, - source_x + SOURCE_CELL_SIZE, - source_y + SOURCE_CELL_SIZE - )) + # Crop single cell from source + cropped = world_img.crop(( + source_x, + source_y, + source_x + SOURCE_CELL_SIZE, + source_y + SOURCE_CELL_SIZE + )) # Resize to output size (64x64 pixels) resized = cropped.resize( @@ -257,52 +207,93 @@ def visualize_layout(grid: List[List[List[int]]], output_dir: str, for world_idx in range(2): x_offset = 0 if world_idx == 0 else (world_width + gap) - # Build large screen positions map for this world - large_screen_positions = {} - for row in range(GRID_SIZE): - for col in range(GRID_SIZE): - screen_id = grid[world_idx][row][col] - if screen_id != -1 and screen_id in large_screen_base_ids: - if screen_id not in large_screen_positions: - large_screen_positions[screen_id] = [] - large_screen_positions[screen_id].append((row, col)) - # Draw borders for each cell + # For large screens, only draw borders where cells are not connected for row in range(GRID_SIZE): for col in range(GRID_SIZE): - screen_id = grid[world_idx][row][col] + cell_id = grid[world_idx][row][col] - if screen_id == -1: + if cell_id == -1: continue - is_large = screen_id in large_screen_base_ids + screen_id = get_screen_id_from_cell(cell_id) + screen = overworld_screens.get(screen_id) + if not screen: + continue + + is_large = screen.big dest_x = x_offset + col * OUTPUT_CELL_SIZE dest_y = row * OUTPUT_CELL_SIZE if is_large: - # For large screens, determine which quadrant this cell is - positions = large_screen_positions.get(screen_id, [(row, col)]) - quadrant = determine_large_screen_quadrant(row, col, positions, GRID_SIZE) + quadrant = get_quadrant_from_cell_id(cell_id, screen_id) - # Draw border only on the outer edges of the large screen - # (not on internal edges between quadrants) - # NW: draw top and left borders - # NE: draw top and right borders - # SW: draw bottom and left borders - # SE: draw bottom and right borders - - if quadrant in ["NW", "NE"]: - # Draw top border - draw.line([(dest_x, dest_y), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y)], fill='black', width=BORDER_WIDTH) + # Check each border direction + # Top border: draw if this is a north quadrant OR if the cell above is not connected + draw_top = True if quadrant in ["SW", "SE"]: - # Draw bottom border - draw.line([(dest_x, dest_y + OUTPUT_CELL_SIZE - 1), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH) - if quadrant in ["NW", "SW"]: - # Draw left border - draw.line([(dest_x, dest_y), (dest_x, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH) + # Check if cell above is connected + above_row = (row - 1) % GRID_SIZE + above_cell_id = grid[world_idx][above_row][col] + if above_cell_id != -1: + above_screen_id = get_screen_id_from_cell(above_cell_id) + above_screen = overworld_screens.get(above_screen_id) + if above_screen and above_screen.big: + above_quadrant = get_quadrant_from_cell_id(above_cell_id, above_screen_id) + if are_large_screen_cells_connected(above_cell_id, cell_id, above_quadrant, quadrant, "south"): + draw_top = False + + # Bottom border: draw if this is a south quadrant OR if the cell below is not connected + draw_bottom = True + if quadrant in ["NW", "NE"]: + # Check if cell below is connected + below_row = (row + 1) % GRID_SIZE + below_cell_id = grid[world_idx][below_row][col] + if below_cell_id != -1: + below_screen_id = get_screen_id_from_cell(below_cell_id) + below_screen = overworld_screens.get(below_screen_id) + if below_screen and below_screen.big: + below_quadrant = get_quadrant_from_cell_id(below_cell_id, below_screen_id) + if are_large_screen_cells_connected(cell_id, below_cell_id, quadrant, below_quadrant, "south"): + draw_bottom = False + + # Left border: draw if this is a west quadrant OR if the cell to the left is not connected + draw_left = True if quadrant in ["NE", "SE"]: - # Draw right border + # Check if cell to the left is connected + left_col = (col - 1) % GRID_SIZE + left_cell_id = grid[world_idx][row][left_col] + if left_cell_id != -1: + left_screen_id = get_screen_id_from_cell(left_cell_id) + left_screen = overworld_screens.get(left_screen_id) + if left_screen and left_screen.big: + left_quadrant = get_quadrant_from_cell_id(left_cell_id, left_screen_id) + if are_large_screen_cells_connected(left_cell_id, cell_id, left_quadrant, quadrant, "east"): + draw_left = False + + # Right border: draw if this is an east quadrant OR if the cell to the right is not connected + draw_right = True + if quadrant in ["NW", "SW"]: + # Check if cell to the right is connected + right_col = (col + 1) % GRID_SIZE + right_cell_id = grid[world_idx][row][right_col] + if right_cell_id != -1: + right_screen_id = get_screen_id_from_cell(right_cell_id) + right_screen = overworld_screens.get(right_screen_id) + if right_screen and right_screen.big: + right_quadrant = get_quadrant_from_cell_id(right_cell_id, right_screen_id) + if are_large_screen_cells_connected(cell_id, right_cell_id, quadrant, right_quadrant, "east"): + draw_right = False + + # Draw the borders + if draw_top: + draw.line([(dest_x, dest_y), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y)], fill='black', width=BORDER_WIDTH) + if draw_bottom: + draw.line([(dest_x, dest_y + OUTPUT_CELL_SIZE - 1), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH) + if draw_left: + draw.line([(dest_x, dest_y), (dest_x, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH) + if draw_right: draw.line([(dest_x + OUTPUT_CELL_SIZE - 1, dest_y), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH) else: # Small screen - draw border around single cell @@ -315,8 +306,8 @@ def visualize_layout(grid: List[List[List[int]]], output_dir: str, # Draw edge connection indicators for each cell for row in range(GRID_SIZE): for col in range(GRID_SIZE): - screen_id = grid[world_idx][row][col] - if screen_id == -1: + cell_id = grid[world_idx][row][col] + if cell_id == -1: continue dest_x = x_offset + col * OUTPUT_CELL_SIZE From d3f8e5fd58859713d6d528e82e88f3099a94cede Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 3 Feb 2026 13:17:42 -0700 Subject: [PATCH 13/76] fix: key counts when door shuffle is off --- Main.py | 2 +- PastReleaseNotes.md | 3 +++ RELEASENOTES.md | 6 +++--- Rom.py | 6 ++++++ index.md | 10 +++++----- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/Main.py b/Main.py index 3099c0b0..3e3a3952 100644 --- a/Main.py +++ b/Main.py @@ -38,7 +38,7 @@ from source.enemizer.DamageTables import DamageTable from source.enemizer.Enemizer import randomize_enemies from source.rom.DataTables import init_data_tables -version_number = '1.5.4' +version_number = '1.5.5' version_branch = '-u' __version__ = f'{version_number}{version_branch}' diff --git a/PastReleaseNotes.md b/PastReleaseNotes.md index 7cae577d..e2915b60 100644 --- a/PastReleaseNotes.md +++ b/PastReleaseNotes.md @@ -10,6 +10,9 @@ # Patch Notes Changelog archive +* 1.5.4 + * Documentation: New AI-assisted documentation [Site](https://aerinon.github.io/ALttPDoorRandomizer) + * Generation Error: Fixed Issue with Shop Code and Take Any Caves (thanks Codemann for assistance) * 1.5.3 * Logic: Key logic fix for part of a dungeon located at Skull 3 (or other similar restricted entrancs). Appropriate key logic was not being applied, causing progression issues. This mostly affect crosskey style seeds. * Standard: Rupee balancing algorithm can no longer switch out the weapon on uncle for money. diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ae96add3..55a2d0b3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,6 +1,6 @@ # Patch Notes -* 1.5.4 - * Documentation: New AI-assisted documentation [Site (I really hope this works)](https//aerinon.github.io/ALttPDoorRandomizer) - * Generation Error: Fixed Issue with Shop Code and Take Any Caves (thanks Codemann for assistance) +* 1.5.5 + * HUD: Key counters are correct even when door shuffle is off + diff --git a/Rom.py b/Rom.py index e56b6985..66436210 100644 --- a/Rom.py +++ b/Rom.py @@ -646,6 +646,12 @@ def patch_rom(world, rom, player, team, is_mystery=False): for room in world.rooms: if room.player == player and room.palette is not None: rom.write_byte(0x13f200+room.index, room.palette) + else: + if world.keyshuffle[player] != 'universal': + for name, layout in world.key_layout[player].items(): + offset = compass_data[name][4]//2 + rom.write_byte(0x13f020+offset, layout.max_chests + layout.max_drops) # not currently used + rom.write_byte(0x187010+offset, layout.max_chests) if world.doorShuffle[player] == 'basic': rom.write_byte(0x138002, 1) for door in world.doors: diff --git a/index.md b/index.md index a7b5e9fe..8e3843ae 100644 --- a/index.md +++ b/index.md @@ -68,11 +68,11 @@ Join the discussion and get help: ## Learn More -- [Features Guide](/features) - Comprehensive feature documentation -- [Installation & Usage](/installation) - Setup and running the randomizer -- [Known Issues](/known-issues) - Current bugs and limitations -- [Roadmap](/roadmap) - Future development plans -- [Blog](/blog) - Latest updates and release notes +- [Features Guide](/features.html) - Comprehensive feature documentation +- [Installation & Usage](/installation.html) - Setup and running the randomizer +- [Known Issues](/known-issues.html) - Current bugs and limitations +- [Roadmap](/roadmap.html) - Future development plans +- [Blog](/blog.html) - Latest updates and release notes ## Credits From 27acabadeff3edd05e76faad65c0da5aff440770 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 3 Feb 2026 13:22:02 -0700 Subject: [PATCH 14/76] doc: update known issues --- known-issues.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/known-issues.md b/known-issues.md index 20093d57..15b3daeb 100644 --- a/known-issues.md +++ b/known-issues.md @@ -52,13 +52,6 @@ When reporting a bug, please provide: ### Critical Issues -#### Dungeon Key HUD indicator - -**Status**: Under investigation - -Dungeon Key HUD indicator shows incorrect total number of keys under certain settings. - - #### Links House on Death Mountain - no logical way off the mountain **Status**: Under investigation From 0133bd1da78e679347797023078a11e5f65f11de Mon Sep 17 00:00:00 2001 From: Catobat <69204835+Catobat@users.noreply.github.com> Date: Tue, 3 Feb 2026 22:46:58 +0100 Subject: [PATCH 15/76] Implement piece merging --- source/overworld/LayoutGenerator.py | 368 +++++++++++++++++++++------- 1 file changed, 274 insertions(+), 94 deletions(-) diff --git a/source/overworld/LayoutGenerator.py b/source/overworld/LayoutGenerator.py index d2245ac7..8f87f098 100644 --- a/source/overworld/LayoutGenerator.py +++ b/source/overworld/LayoutGenerator.py @@ -1,4 +1,5 @@ import logging +from DungeonGenerator import GenerationException import RaceRandom as random import random as _random from typing import List, Dict, Optional, Set, Tuple @@ -6,7 +7,6 @@ from BaseClasses import OWEdge, World, Direction, Terrain from OverworldShuffle import connect_two_way, validate_layout ENABLE_KEEP_SIMILAR_SPECIAL_HANDLING = False -PREVENT_WRAPPED_LARGE_SCREENS = False DRAW_IMAGE = True large_screen_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35] + [0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75] @@ -78,8 +78,7 @@ class Piece: """ Represents a piece consisting of a main and optionally a parallel world piece. """ - __slots__ = ('main', 'parallel', 'world', 'width', 'height', - 'invalid_wrap_row', 'invalid_wrap_column', 'restriction', + __slots__ = ('main', 'parallel', 'world', 'width', 'height', 'restriction', 'crossed_groups', 'delay', 'order', 'edge_sides', 'max_edges_per_side') def __init__( @@ -89,8 +88,6 @@ class Piece: world: int = 0, width: int = 0, height: int = 0, - invalid_wrap_row: Optional[List[int]] = None, - invalid_wrap_column: Optional[List[int]] = None, restriction: Optional[List[int]] = None, crossed_groups: Optional[List[List[int]]] = None, delay: int = 0, @@ -103,8 +100,6 @@ class Piece: self.world = world # 0 or 1 self.width = width self.height = height - self.invalid_wrap_row = invalid_wrap_row if invalid_wrap_row is not None else [] - self.invalid_wrap_column = invalid_wrap_column if invalid_wrap_column is not None else [] self.restriction = restriction self.crossed_groups = crossed_groups if crossed_groups is not None else [] self.delay = delay @@ -400,74 +395,42 @@ def define_large_screen_quadrants( def create_piece_list(world: World, player: int, options: LayoutGeneratorOptions, crossed_group_b: List[int], overworld_screens: Dict[int, Screen], large_screen_quadrant_info: Dict[int, Dict], large_screen_quadrant_info_land: Dict[int, Dict], large_screen_quadrant_info_water: Dict[int, Dict]) -> List[Piece]: piece_list: List[Piece] = [] - used_screens_set = set() - - all_large_screens = [s for s in overworld_screens.values() if s.big] - all_small_screens = [s for s in overworld_screens.values() if not s.big] + # Determine which screens to process + all_screens = list(overworld_screens.values()) if world.owParallel[player]: # In Parallel, only use light world screens # Each piece will automatically handle both worlds through parallel mechanism - all_large_screens = [s for s in all_large_screens if not s.dark_world] - all_small_screens = [s for s in all_small_screens if not s.dark_world] + all_screens = [s for s in all_screens if not s.dark_world] - # In Standard mode, screens 0x1B, 0x2B, 0x2C are glued together as a single piece - if world.mode[player] == 'standard': - castle_screen = overworld_screens.get(0x1B) - central_bonk_screen = overworld_screens.get(0x2B) - links_house_screen = overworld_screens.get(0x2C) - - if castle_screen and central_bonk_screen and links_house_screen: - piece = create_piece(world, player, [ - [0x1B, 0x1C], - [0x23, 0x24], - [0x2B, 0x2C] - ], overworld_screens) - - if options.large_screen_pool: - piece.restriction = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35] - - piece_list.append(piece) - used_screens_set.add(castle_screen) - used_screens_set.add(central_bonk_screen) - used_screens_set.add(links_house_screen) - - if world.owParallel[player]: - used_screens_set.add(castle_screen.parallel) - used_screens_set.add(central_bonk_screen.parallel) - used_screens_set.add(links_house_screen.parallel) - - # Add large screens - for screen in all_large_screens: - if screen not in used_screens_set: - base_id = screen.id - if options.split_large_screens: - for quadrant_offset in [0x00, 0x01, 0x08, 0x09]: - piece = create_piece(world, player, [[base_id + quadrant_offset]], overworld_screens) - if options.large_screen_pool: - piece.restriction = [large_screen_id + quadrant_offset for large_screen_id in [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35]] - piece_list.append(piece) - else: - piece = create_piece(world, player, [[base_id, base_id + 0x01], [base_id + 0x08, base_id + 0x09]], overworld_screens) + # Phase 1: Create individual 1x1 pieces for all cells + for screen in all_screens: + if screen.big: + # Create 4 pieces for large screen quadrants + for offset in [0x00, 0x01, 0x08, 0x09]: + piece = create_piece(world, player, [[screen.id + offset]], overworld_screens) if options.large_screen_pool: - piece.restriction = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35] + piece.restriction = [large_id + offset for large_id in [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35]] piece_list.append(piece) - used_screens_set.add(screen) - if world.owParallel[player]: - used_screens_set.add(screen.parallel) - - # Add small screens - for screen in all_small_screens: - if screen not in used_screens_set: + else: piece = create_piece(world, player, [[screen.id]], overworld_screens) if options.large_screen_pool: piece.restriction = [s.id for s in overworld_screens.values() if not s.big] piece_list.append(piece) - used_screens_set.add(screen) - if world.owParallel[player]: - used_screens_set.add(screen.parallel) - # Add piece data + # Phase 2: Apply options via merging + + # Merge large screens if not split + if not options.split_large_screens: + for large_id in large_screen_ids: + if large_id in [s.id for s in all_screens if s.big]: + piece_list = merge_pieces(piece_list, [[large_id, large_id + 0x01], [large_id + 0x08, large_id + 0x09]], world, player, overworld_screens) + + # Standard mode: merge castle area + if world.mode[player] == 'standard': + piece_list = merge_pieces(piece_list, [[0x23, 0x24], [0x2B, 0x2C]], world, player, overworld_screens) + + # Phase 3: Add piece data for piece in piece_list: add_piece_data(world, player, piece, large_screen_quadrant_info, large_screen_quadrant_info_land, large_screen_quadrant_info_water) # Handle crossed groups @@ -502,13 +465,13 @@ def create_piece(world: World, player: int, grid: List[List[int]], overworld_scr Takes 2D array of cell IDs and creates main and parallel pieces """ piece = Piece( - main=WorldPiece(), + main=WorldPiece(width=len(grid[0]), height=len(grid)), width=len(grid[0]), height=len(grid) ) if world.owParallel[player]: - piece.parallel = WorldPiece() + piece.parallel = WorldPiece(width=len(grid[0]), height=len(grid)) found_screens = set() @@ -526,25 +489,254 @@ def create_piece(world: World, player: int, grid: List[List[int]], overworld_scr for j in range(piece.width): cell_id = grid[i][j] new_row.append(cell_id) - screen = overworld_screens.get(get_screen_id_from_cell(cell_id)) - new_screen_row.append(screen) - if world.owParallel[player] and screen: - new_row_parallel.append(cell_id - screen.id + screen.parallel.id) - new_screen_row_parallel.append(screen.parallel) - - if screen and screen not in found_screens: + screen = None if cell_id == -1 else overworld_screens.get(get_screen_id_from_cell(cell_id)) + if screen: found_screens.add(screen) - piece.world = 1 if screen.dark_world else 0 - if screen.big and PREVENT_WRAPPED_LARGE_SCREENS: - # For large screens, prevent wrapping at the second row/column - # This ensures the 2x2 piece doesn't split across the grid boundary - if (i + 1) not in piece.invalid_wrap_row: - piece.invalid_wrap_row.append(i + 1) - if (j + 1) not in piece.invalid_wrap_column: - piece.invalid_wrap_column.append(j + 1) + new_screen_row.append(screen) + if world.owParallel[player]: + if screen: + new_row_parallel.append(cell_id - screen.id + screen.parallel.id) + new_screen_row_parallel.append(screen.parallel) + else: + new_row_parallel.append(-1) + new_screen_row_parallel.append(None) + + worlds = set(s.dark_world for s in found_screens if s is not None) + if len(worlds) != 1: + raise GenerationException("Piece contains screens from both Light World and Dark World") + piece.world = 1 if True in worlds else 0 return piece +def get_piece_cells(piece: Piece) -> Set[int]: + """Get all cell IDs contained in a piece.""" + cells = set() + for row in piece.main.grid: + for cell in row: + if cell != -1: + cells.add(cell) + return cells + +def expand_arrangement(arrangement: List[List[int]], pieces: List[Piece]) -> List[List[int]]: + """ + Expand an arrangement to include all cells from the pieces being merged. + + When merging pieces, if a piece contains cells not in the original arrangement, + we need to expand the arrangement to include those cells in their correct + relative positions. + + Raises an exception if the relative positions of cells within pieces conflict + with the requested arrangement (e.g., contradictory merge operations). + """ + # Build a mapping of cell_id -> (row, col) for all cells in all pieces + # relative to a common coordinate system + cell_positions: Dict[int, Tuple[int, int]] = {} + # Also track position -> cell_id to detect when two cells would occupy the same position + position_to_cell: Dict[Tuple[int, int], int] = {} + + # First, map cells from the original arrangement + for i, row in enumerate(arrangement): + for j, cell in enumerate(row): + if cell != -1: + cell_positions[cell] = (i, j) + position_to_cell[(i, j)] = cell + + # For each piece, determine where its cells should go + for piece in pieces: + # Find a cell that's already in our arrangement to anchor this piece + anchor_cell = None + anchor_piece_pos = None + for i, row in enumerate(piece.main.grid): + for j, cell in enumerate(row): + if cell != -1 and cell in cell_positions: + anchor_cell = cell + anchor_piece_pos = (i, j) + break + if anchor_cell is not None: + break + + # Calculate offset between piece coordinates and arrangement coordinates + anchor_arr_pos = cell_positions[anchor_cell] + offset_row = anchor_arr_pos[0] - anchor_piece_pos[0] + offset_col = anchor_arr_pos[1] - anchor_piece_pos[1] + + # Add all cells from this piece to cell_positions, checking for conflicts + for i, row in enumerate(piece.main.grid): + for j, cell in enumerate(row): + if cell != -1: + new_pos = (i + offset_row, j + offset_col) + if cell in cell_positions: + # Cell already has a position - verify it's consistent + if cell_positions[cell] != new_pos: + raise GenerationException( + f"Cannot merge: cell 0x{cell:02X} has conflicting positions. " + f"Existing position {cell_positions[cell]} conflicts with " + f"position {new_pos} from piece containing cells " + f"{[c for row in piece.main.grid for c in row if c != -1]}. " + f"This indicates contradictory merge operations." + ) + elif new_pos in position_to_cell: + # Position is already occupied by a different cell + existing_cell = position_to_cell[new_pos] + raise GenerationException( + f"Cannot merge: cell 0x{cell:02X} would be placed at position {new_pos}, " + f"but that position is already occupied by cell 0x{existing_cell:02X}. " + f"This indicates contradictory merge operations." + ) + else: + cell_positions[cell] = new_pos + position_to_cell[new_pos] = cell + + # Find the bounding box of all cells + if not cell_positions: + return arrangement + + min_row = min(pos[0] for pos in cell_positions.values()) + max_row = max(pos[0] for pos in cell_positions.values()) + min_col = min(pos[1] for pos in cell_positions.values()) + max_col = max(pos[1] for pos in cell_positions.values()) + + # Create new arrangement with normalized coordinates + new_height = max_row - min_row + 1 + new_width = max_col - min_col + 1 + new_arrangement = [[-1] * new_width for _ in range(new_height)] + + for cell, (row, col) in cell_positions.items(): + new_arrangement[row - min_row][col - min_col] = cell + + return new_arrangement + +def calculate_merged_restrictions(pieces: List[Piece], arrangement: List[List[int]]) -> Optional[List[int]]: + """ + Calculate restrictions for the merged piece. + + For each piece with restrictions, we translate the restrictions to account + for the piece's position in the merged arrangement. The final restriction + is the intersection of all translated restrictions. + + For example, when merging 4 quadrant pieces into a 2x2: + - NW piece (at position 0,0) has restrictions like [0x00, 0x03, ...] - no translation needed + - NE piece (at position 0,1) has restrictions like [0x01, 0x04, ...] - translate left by 1 + - SW piece (at position 1,0) has restrictions like [0x08, 0x0B, ...] - translate up by 1 + - SE piece (at position 1,1) has restrictions like [0x09, 0x0C, ...] - translate up and left by 1 + + After translation, all should give [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35] + """ + if not any(p.restriction for p in pieces): + return None + + # Build mapping from cell to position in arrangement + cell_to_new_pos = {} + for i, row in enumerate(arrangement): + for j, cell in enumerate(row): + if cell != -1: + cell_to_new_pos[cell] = (i, j) + + # For each piece, translate its restrictions + translated_restrictions = [] + for piece in pieces: + if piece.restriction is None: + continue + + # Find the first cell in this piece and its position in the arrangement + piece_cell = None + piece_old_pos = None + for i, row in enumerate(piece.main.grid): + for j, cell in enumerate(row): + if cell != -1 and cell in cell_to_new_pos: + piece_cell = cell + piece_old_pos = (i, j) + break + if piece_cell is not None: + break + + if piece_cell is None: + continue + + new_pos = cell_to_new_pos[piece_cell] + # The offset is how much we need to shift the restriction positions + # to get the top-left corner position of the merged piece + offset_row = new_pos[0] - piece_old_pos[0] + offset_col = new_pos[1] - piece_old_pos[1] + + # Translate restrictions: shift each restriction position back by the offset + # to get the position where the merged piece's top-left corner would be + translated = [] + for r in piece.restriction: + r_row = r // 8 + r_col = r % 8 + new_r_row = r_row - offset_row + new_r_col = r_col - offset_col + if 0 <= new_r_row < 8 and 0 <= new_r_col < 8: + translated.append(new_r_row * 8 + new_r_col) + translated_restrictions.append(set(translated)) + + # Intersection of all translated restrictions + result = translated_restrictions[0] + for tr in translated_restrictions[1:]: + result &= tr + + return list(result) + +def merge_pieces(piece_list: List[Piece], arrangement: List[List[int]], world: World, player: int, overworld_screens: Dict[int, Screen]) -> List[Piece]: + """ + Merge pieces according to the specified arrangement. + + The arrangement is a 2D list where: + - Positive values are cell IDs that must be included + - -1 indicates a flexible/empty position + + Example: [[0x00, 0x01], [0x08, 0x09]] merges 4 pieces into a 2x2 piece + + If a piece being merged contains additional cells not in the arrangement, + the arrangement is automatically expanded to include all cells from all + pieces being merged. + """ + # Collect all cell IDs from arrangement, excluding -1 + target_cells = set() + for row in arrangement: + for cell in row: + if cell != -1: + target_cells.add(cell) + + # Find all pieces containing any of the target cells + pieces_to_merge = [] + remaining_pieces = [] + + for piece in piece_list: + piece_cells = get_piece_cells(piece) + if piece_cells & target_cells: + pieces_to_merge.append(piece) + else: + remaining_pieces.append(piece) + + # Validate: all target cells must be found + found_cells = set() + for piece in pieces_to_merge: + piece_cells = get_piece_cells(piece) + # Check for overlapping cells between pieces (indicates contradictory merges) + overlap = found_cells & piece_cells + if overlap: + raise GenerationException(f"Cannot merge: cells {overlap} appear in multiple pieces (contradictory merge operations)") + found_cells.update(piece_cells) + + if not target_cells.issubset(found_cells): + missing = target_cells - found_cells + raise GenerationException(f"Cannot merge: cells {missing} not found in any piece") + + # If pieces contain additional cells not in the arrangement, expand the arrangement + if found_cells != target_cells: + arrangement = expand_arrangement(arrangement, pieces_to_merge) + + # Create the merged piece + merged_piece = create_piece(world, player, arrangement, overworld_screens) + + # Calculate merged restrictions + merged_piece.restriction = calculate_merged_restrictions(pieces_to_merge, arrangement) + + remaining_pieces.append(merged_piece) + return remaining_pieces + def add_piece_data(world: World, player: int, piece: Piece, large_screen_quadrant_info: Dict[int, Dict], large_screen_quadrant_info_land: Dict[int, Dict], large_screen_quadrant_info_water: Dict[int, Dict]) -> None: """ Add computed data to piece @@ -553,13 +745,8 @@ def add_piece_data(world: World, player: int, piece: Piece, large_screen_quadran num_pieces = 2 if piece.parallel else 1 for p in range(num_pieces): world_piece = piece.main if p == 0 else piece.parallel - world_piece.width = piece.width - world_piece.height = piece.height add_world_piece_edge_info(world, player, world_piece, large_screen_quadrant_info, large_screen_quadrant_info_land, large_screen_quadrant_info_water) - piece.width = piece.main.width - piece.height = piece.main.height - # Calculate edge_sides and max_edges_per_side: 0 for multi-cell pieces if piece.width == 1 and piece.height == 1: edge_sides = 0 @@ -739,8 +926,6 @@ def random_place_piece( piece_main = piece.main piece_parallel = piece.parallel wrld = piece.world - invalid_wrap_row = piece.invalid_wrap_row - invalid_wrap_column = piece.invalid_wrap_column restriction = piece.restriction piece_width = piece.width piece_height = piece.height @@ -751,13 +936,8 @@ def random_place_piece( i_range = height if vertical_wrap else height - piece_height + 1 for i in range(i_range): - if i >= height - piece_height + 1 and (height - i) in invalid_wrap_row: - continue - j_range = width if horizontal_wrap else width - piece_width + 1 for j in range(j_range): - if j >= width - piece_width + 1 and (width - j) in invalid_wrap_column: - continue if restriction and (i * 8 + j) not in restriction: continue @@ -1268,7 +1448,7 @@ def connect_edge_sets(world: World, player: int, edge_set_1: List[OWEdge], edge_ for k in range(len(edge_set_2)): connect_two_way(world, edges_to_connect[k].name, edge_set_2[k].name, player, connected_edges, final_placement) else: - raise Exception("There should never be multiple edges with high priority in an edge set") + raise GenerationException("There should never be multiple edges with high priority in an edge set") # ============================================================================ # GRID FORMATTING @@ -1452,4 +1632,4 @@ def generate_random_grid_layout(world: World, player: int, connected_edges: List except Exception as e: logger.warning(f"Warning: Could not create visualization: {e}") else: - raise Exception(f"Layout generation FAILED after {result.failures} attempts and {elapsed_time:.3f} seconds") \ No newline at end of file + raise GenerationException(f"Layout generation FAILED after {result.failures} attempts and {elapsed_time:.3f} seconds") \ No newline at end of file From 086d1bbc8505569f43ee1a5320bf042b884747ec Mon Sep 17 00:00:00 2001 From: Catobat <69204835+Catobat@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:10:04 +0100 Subject: [PATCH 16/76] Place pieces with only one possible position first --- source/overworld/LayoutGenerator.py | 156 +++++++++++++++++++++++++++- 1 file changed, 153 insertions(+), 3 deletions(-) diff --git a/source/overworld/LayoutGenerator.py b/source/overworld/LayoutGenerator.py index 8f87f098..2764cb25 100644 --- a/source/overworld/LayoutGenerator.py +++ b/source/overworld/LayoutGenerator.py @@ -273,6 +273,25 @@ def create_empty_grid_info(edge_connection_seed: float) -> GridInfo: edge_connection_seed=edge_connection_seed ) +def copy_grid_info(source: GridInfo, edge_connection_seed: float) -> GridInfo: + """ + Create a deep copy of a GridInfo object with a new edge_connection_seed. + Only copies the grid data structures, not the OWEdge references (which are shared). + """ + return GridInfo( + grid=[[row[:] for row in world_grid] for world_grid in source.grid], + north_edges_grid=[[[cell[:] for cell in row] for row in world_grid] for world_grid in source.north_edges_grid], + south_edges_grid=[[[cell[:] for cell in row] for row in world_grid] for world_grid in source.south_edges_grid], + west_edges_grid=[[[cell[:] for cell in row] for row in world_grid] for world_grid in source.west_edges_grid], + east_edges_grid=[[[cell[:] for cell in row] for row in world_grid] for world_grid in source.east_edges_grid], + north_edges_water_grid=[[[cell[:] for cell in row] for row in world_grid] for world_grid in source.north_edges_water_grid], + south_edges_water_grid=[[[cell[:] for cell in row] for row in world_grid] for world_grid in source.south_edges_water_grid], + west_edges_water_grid=[[[cell[:] for cell in row] for row in world_grid] for world_grid in source.west_edges_water_grid], + east_edges_water_grid=[[[cell[:] for cell in row] for row in world_grid] for world_grid in source.east_edges_water_grid], + crossed_groups=[row[:] for row in source.crossed_groups], + edge_connection_seed=edge_connection_seed + ) + def initialize_screens(world: World, player: int) -> Dict[int, Screen]: overworld_screens: Dict[int, Screen] = {} screen_edges_map = group_owedges_by_screens(world, player) @@ -1171,21 +1190,152 @@ def random_place_piece( return PiecePlacementResult(success=True, piece=piece, score_major=used_score_major, score_minor=used_score_minor) +def place_single_restriction_pieces( + world: World, + player: int, + grid_info: GridInfo, + options: LayoutGeneratorOptions, + pieces: List[Piece] +) -> Tuple[List[Piece], int]: + """ + Place pieces that have a restriction list with only a single element. + These pieces are forced into a single position, so we can place them deterministically. + + This function iteratively: + 1. Validates restriction lists against current grid state and grid bounds + 2. Places pieces with single-element restrictions + 3. Repeats until no more pieces can be placed + + Returns a tuple of (remaining_pieces, count_of_placed_pieces). + """ + use_crossed_groups = (world.owCrossed[player] == 'polar' and world.owMixed[player]) or world.owCrossed[player] == 'grouped' + + remaining_pieces = list(pieces) + placed_count = 0 + + placed_this_iteration = True + while placed_this_iteration: + placed_this_iteration = False + + # Validate and update restriction lists for all remaining pieces + for piece in remaining_pieces: + if piece.restriction is None: + continue + + valid_positions = [] + for position in piece.restriction: + row = position // 8 + column = position % 8 + wrld = piece.world + piece_crossed_groups = piece.crossed_groups + + # Check if this position is valid + is_valid = True + + # Check if piece would go outside grid bounds when wrapping is disabled + if not options.horizontal_wrap and column + piece.width > 8: + is_valid = False + if not options.vertical_wrap and row + piece.height > 8: + is_valid = False + + # Check for overlap with already placed pieces + if is_valid: + for k in range(piece.height): + if not is_valid: + break + row_idx = (row + k) % 8 + for l in range(piece.width): + col_idx = (column + l) % 8 + + # Check main world overlap + if grid_info.grid[wrld][row_idx][col_idx] != -1 and piece.main.screens[k][l]: + is_valid = False + break + + # Check parallel world overlap + if piece.parallel and grid_info.grid[1 - wrld][row_idx][col_idx] != -1 and piece.parallel.screens[k][l]: + is_valid = False + break + + # Check crossed groups + if use_crossed_groups and grid_info.crossed_groups[row_idx][col_idx] != -1 and grid_info.crossed_groups[row_idx][col_idx] != piece_crossed_groups[k][l]: + is_valid = False + break + + if is_valid: + valid_positions.append(position) + + # Update the restriction list + if len(valid_positions) == 0: + raise GenerationException(f"No valid positions remaining for piece with restriction list (original: {piece.restriction})") + + piece.restriction = valid_positions + + # Place pieces with single-element restrictions + new_remaining_pieces = [] + for piece in remaining_pieces: + # Check if this piece has exactly one restriction position + if piece.restriction is not None and len(piece.restriction) == 1: + position = piece.restriction[0] + row = position // 8 + column = position % 8 + wrld = piece.world + piece_crossed_groups = piece.crossed_groups + + # Place the piece on the grid + for k in range(piece.height): + row_idx = (row + k) % 8 + for l in range(piece.width): + col_idx = (column + l) % 8 + num_pieces = 2 if piece.parallel else 1 + for p in range(num_pieces): + world_piece = piece.main if p == 0 else piece.parallel + w = wrld if p == 0 else 1 - wrld + + grid_info.grid[w][row_idx][col_idx] = world_piece.grid[k][l] + grid_info.north_edges_grid[w][row_idx][col_idx] = world_piece.north_edges[k][l] + grid_info.south_edges_grid[w][row_idx][col_idx] = world_piece.south_edges[k][l] + grid_info.west_edges_grid[w][row_idx][col_idx] = world_piece.west_edges[k][l] + grid_info.east_edges_grid[w][row_idx][col_idx] = world_piece.east_edges[k][l] + + if not world.owTerrain[player]: + grid_info.north_edges_water_grid[w][row_idx][col_idx] = world_piece.north_edges_water[k][l] + grid_info.south_edges_water_grid[w][row_idx][col_idx] = world_piece.south_edges_water[k][l] + grid_info.west_edges_water_grid[w][row_idx][col_idx] = world_piece.west_edges_water[k][l] + grid_info.east_edges_water_grid[w][row_idx][col_idx] = world_piece.east_edges_water[k][l] + + if use_crossed_groups: + grid_info.crossed_groups[row_idx][col_idx] = piece_crossed_groups[k][l] + + placed_count += 1 + placed_this_iteration = True + else: + new_remaining_pieces.append(piece) + + remaining_pieces = new_remaining_pieces + + return remaining_pieces, placed_count + def get_random_layout(world: World, player: int, connected_edges_cache: List[str], pieces_to_place: List[Piece], options: LayoutGeneratorOptions, prio_edges: List[str], overworld_screens: Dict[int, Screen]) -> LayoutGeneratorResult: total_score = 0 best_score = -1000000 worst_score = 1000000 best_grid_info = None + # Pre-place pieces with single-element restriction lists + base_grid_info = create_empty_grid_info(0.0) + remaining_pieces, preplaced_count = place_single_restriction_pieces(world, player, base_grid_info, options, pieces_to_place) + successes = 0 failures = 0 run = 0 while run < options.min_runs or (run * successes < options.target_runs_times_successes and run < options.max_runs): run += 1 connected_edges = connected_edges_cache.copy() - piece_list = pieces_to_place.copy() + piece_list = remaining_pieces.copy() - grid_info = create_empty_grid_info(random.random()) + # Copy the pre-placed grid with a new random seed for edge connections + grid_info = copy_grid_info(base_grid_info, random.random()) for piece in piece_list: piece.delay = 0 @@ -1212,7 +1362,7 @@ def get_random_layout(world: World, player: int, connected_edges_cache: List[str for i in range(1, min(options.multi_choice, len(piece_list))): pieces.append(piece_list[i]) - result = random_place_piece(world, player, grid_info, options, pieces, len(placed_pieces) < options.first_ignore_bonus_points) + result = random_place_piece(world, player, grid_info, options, pieces, len(placed_pieces) + preplaced_count < options.first_ignore_bonus_points) if not result.success: failures += 1 From f5d547cc5e47b57d288dd0988ade1a7fb27623bd Mon Sep 17 00:00:00 2001 From: Catobat <69204835+Catobat@users.noreply.github.com> Date: Thu, 5 Feb 2026 21:11:24 +0100 Subject: [PATCH 17/76] Handle trimming and wrapping for large pieces --- source/overworld/LayoutGenerator.py | 161 ++++++++++++++++++++++++---- 1 file changed, 143 insertions(+), 18 deletions(-) diff --git a/source/overworld/LayoutGenerator.py b/source/overworld/LayoutGenerator.py index 2764cb25..8b86e516 100644 --- a/source/overworld/LayoutGenerator.py +++ b/source/overworld/LayoutGenerator.py @@ -449,6 +449,12 @@ def create_piece_list(world: World, player: int, options: LayoutGeneratorOptions if world.mode[player] == 'standard': piece_list = merge_pieces(piece_list, [[0x23, 0x24], [0x2B, 0x2C]], world, player, overworld_screens) + # Trim pieces by removing empty rows/columns on edges + piece_list = [trim_piece(piece) for piece in piece_list] + + # Validate piece sizes and apply wrapping if needed + piece_list = validate_and_wrap_pieces(piece_list, options, world, player, overworld_screens) + # Phase 3: Add piece data for piece in piece_list: add_piece_data(world, player, piece, large_screen_quadrant_info, large_screen_quadrant_info_land, large_screen_quadrant_info_water) @@ -536,6 +542,59 @@ def get_piece_cells(piece: Piece) -> Set[int]: cells.add(cell) return cells +def trim_piece(piece: Piece) -> Piece: + """ + Trim a piece by removing any full rows or columns on the edges that only consist of -1. + Adjusts position restrictions when present. + """ + # Find the bounds of non-empty cells + min_row, max_row = piece.height, -1 + min_col, max_col = piece.width, -1 + + for i in range(piece.height): + for j in range(piece.width): + has_content = piece.main.grid[i][j] != -1 + if piece.parallel: + has_content = has_content or piece.parallel.grid[i][j] != -1 + if has_content: + min_row = min(min_row, i) + max_row = max(max_row, i) + min_col = min(min_col, j) + max_col = max(max_col, j) + + if max_row < 0 or (min_row == 0 and max_row == piece.height - 1 and min_col == 0 and max_col == piece.width - 1): + return piece + + new_height = max_row - min_row + 1 + new_width = max_col - min_col + 1 + piece.width = new_width + piece.height = new_height + + # Trim piece + piece.main.grid = [row[min_col:max_col + 1] for row in piece.main.grid[min_row:max_row + 1]] + piece.main.screens = [row[min_col:max_col + 1] for row in piece.main.screens[min_row:max_row + 1]] + piece.main.width = new_width + piece.main.height = new_height + + if piece.parallel: + piece.parallel.grid = [row[min_col:max_col + 1] for row in piece.parallel.grid[min_row:max_row + 1]] + piece.parallel.screens = [row[min_col:max_col + 1] for row in piece.parallel.screens[min_row:max_row + 1]] + piece.parallel.width = new_width + piece.parallel.height = new_height + + # Adjust restrictions if present + if piece.restriction is not None: + adjusted_restrictions = [] + for pos in piece.restriction: + old_row = pos // 8 + old_col = pos % 8 + new_row = (old_row + min_row) % 8 + new_col = (old_col + min_col) % 8 + adjusted_restrictions.append(new_row * 8 + new_col) + piece.restriction = adjusted_restrictions + + return piece + def expand_arrangement(arrangement: List[List[int]], pieces: List[Piece]) -> List[List[int]]: """ Expand an arrangement to include all cells from the pieces being merged. @@ -546,19 +605,25 @@ def expand_arrangement(arrangement: List[List[int]], pieces: List[Piece]) -> Lis Raises an exception if the relative positions of cells within pieces conflict with the requested arrangement (e.g., contradictory merge operations). + + Note: This function uses wrap-aware position checking. Positions that differ + by multiples of 8 are considered equivalent (for wrapping support). This allows + arrangements like [[0x10, 0x11, 0x12, 0x13, 0x14]] and [[0x14, 0x15, 0x16, 0x17, 0x10]] + to be merged into a valid horizontal loop. """ # Build a mapping of cell_id -> (row, col) for all cells in all pieces # relative to a common coordinate system cell_positions: Dict[int, Tuple[int, int]] = {} - # Also track position -> cell_id to detect when two cells would occupy the same position - position_to_cell: Dict[Tuple[int, int], int] = {} + # Track wrapped_position -> cell_id to detect when two different cells would occupy the same position after wrapping + wrapped_position_to_cell: Dict[Tuple[int, int], int] = {} # First, map cells from the original arrangement for i, row in enumerate(arrangement): for j, cell in enumerate(row): if cell != -1: cell_positions[cell] = (i, j) - position_to_cell[(i, j)] = cell + wrapped_pos = (i % 8, j % 8) + wrapped_position_to_cell[wrapped_pos] = cell # For each piece, determine where its cells should go for piece in pieces: @@ -584,27 +649,33 @@ def expand_arrangement(arrangement: List[List[int]], pieces: List[Piece]) -> Lis for j, cell in enumerate(row): if cell != -1: new_pos = (i + offset_row, j + offset_col) + # Normalize position for wrapping (positions differing by 8 are equivalent) + wrapped_pos = (new_pos[0] % 8, new_pos[1] % 8) + if cell in cell_positions: - # Cell already has a position - verify it's consistent - if cell_positions[cell] != new_pos: + # Cell already has a position - verify it's consistent after wrapping + existing_pos = cell_positions[cell] + existing_wrapped = (existing_pos[0] % 8, existing_pos[1] % 8) + if existing_wrapped != wrapped_pos: raise GenerationException( f"Cannot merge: cell 0x{cell:02X} has conflicting positions. " - f"Existing position {cell_positions[cell]} conflicts with " - f"position {new_pos} from piece containing cells " + f"Existing position {existing_pos} (wrapped: {existing_wrapped}) conflicts with " + f"position {new_pos} (wrapped: {wrapped_pos}) from piece containing cells " f"{[c for row in piece.main.grid for c in row if c != -1]}. " f"This indicates contradictory merge operations." ) - elif new_pos in position_to_cell: - # Position is already occupied by a different cell - existing_cell = position_to_cell[new_pos] + # Same cell at same wrapped position - this is fine (loop detected) + elif wrapped_pos in wrapped_position_to_cell: + # Position is already occupied by a different cell after wrapping + existing_cell = wrapped_position_to_cell[wrapped_pos] raise GenerationException( - f"Cannot merge: cell 0x{cell:02X} would be placed at position {new_pos}, " - f"but that position is already occupied by cell 0x{existing_cell:02X}. " - f"This indicates contradictory merge operations." + f"Cannot merge: cell 0x{cell:02X} would be placed at position {new_pos} " + f"(wrapped: {wrapped_pos}), but that position is already occupied by " + f"cell 0x{existing_cell:02X}. This indicates contradictory merge operations." ) else: cell_positions[cell] = new_pos - position_to_cell[new_pos] = cell + wrapped_position_to_cell[wrapped_pos] = cell # Find the bounding box of all cells if not cell_positions: @@ -684,10 +755,9 @@ def calculate_merged_restrictions(pieces: List[Piece], arrangement: List[List[in for r in piece.restriction: r_row = r // 8 r_col = r % 8 - new_r_row = r_row - offset_row - new_r_col = r_col - offset_col - if 0 <= new_r_row < 8 and 0 <= new_r_col < 8: - translated.append(new_r_row * 8 + new_r_col) + new_r_row = (r_row - offset_row) % 8 + new_r_col = (r_col - offset_col) % 8 + translated.append(new_r_row * 8 + new_r_col) translated_restrictions.append(set(translated)) # Intersection of all translated restrictions @@ -756,6 +826,61 @@ def merge_pieces(piece_list: List[Piece], arrangement: List[List[int]], world: W remaining_pieces.append(merged_piece) return remaining_pieces +def validate_and_wrap_pieces(piece_list: List[Piece], options: LayoutGeneratorOptions, world: World, player: int, overworld_screens: Dict[int, Screen]) -> List[Piece]: + """ + Validate that all pieces are at most 8x8 in size. + If a piece is too large, attempt to reduce its size using wrapping. + """ + result_pieces = [] + + for piece in piece_list: + if piece.width <= 8 and piece.height <= 8: + result_pieces.append(piece) + continue + + # Piece is too large, need to apply wrapping + if piece.width > 8 and not options.horizontal_wrap: + raise GenerationException( + f"Piece has width {piece.width} which exceeds 8, but horizontal wrapping is not enabled. " + f"Cells: {[c for row in piece.main.grid for c in row if c != -1]}" + ) + + if piece.height > 8 and not options.vertical_wrap: + raise GenerationException( + f"Piece has height {piece.height} which exceeds 8, but vertical wrapping is not enabled. " + f"Cells: {[c for row in piece.main.grid for c in row if c != -1]}" + ) + + # Calculate wrapped dimensions + wrapped_width = min(piece.width, 8) + wrapped_height = min(piece.height, 8) + + # Create new wrapped grid, checking for conflicts + wrapped_grid = [[-1] * wrapped_width for _ in range(wrapped_height)] + + for i in range(piece.height): + wrapped_i = i % 8 + for j in range(piece.width): + wrapped_j = j % 8 + cell = piece.main.grid[i][j] + + if cell != -1: + existing = wrapped_grid[wrapped_i][wrapped_j] + if existing != -1 and existing != cell: + raise GenerationException( + f"Wrapping conflict: cell 0x{cell:02X} at position ({i}, {j}) " + f"would wrap to ({wrapped_i}, {wrapped_j}) which already contains cell 0x{existing:02X}. " + f"Piece cells: {[c for row in piece.main.grid for c in row if c != -1]}" + ) + wrapped_grid[wrapped_i][wrapped_j] = cell + + # Create the wrapped piece + wrapped_piece = create_piece(world, player, wrapped_grid, overworld_screens) + wrapped_piece.restriction = piece.restriction + result_pieces.append(wrapped_piece) + + return result_pieces + def add_piece_data(world: World, player: int, piece: Piece, large_screen_quadrant_info: Dict[int, Dict], large_screen_quadrant_info_land: Dict[int, Dict], large_screen_quadrant_info_water: Dict[int, Dict]) -> None: """ Add computed data to piece From 78c389e2d50f671dee736930bb5797871d32787e Mon Sep 17 00:00:00 2001 From: Catobat <69204835+Catobat@users.noreply.github.com> Date: Sun, 8 Feb 2026 21:57:46 +0100 Subject: [PATCH 18/76] Add Grid Layout Shuffle Customizer options for custom arrangements, restricted positions and split large screens --- docs/Customizer.md | 35 +++++ source/classes/CustomSettings.py | 54 ++++++- source/overworld/LayoutGenerator.py | 234 ++++++++++++++++++++++++++-- 3 files changed, 312 insertions(+), 11 deletions(-) diff --git a/docs/Customizer.md b/docs/Customizer.md index 5d5e083d..5ac2e080 100644 --- a/docs/Customizer.md +++ b/docs/Customizer.md @@ -257,10 +257,45 @@ someDescription: `grid` contains additional options that only have an effect when `ow_layout` is set to `grid`. +#### fixed_arrangements + +Use this to dictate the relative positioning between multiple screens (or quadrants of large screens). Screens and quadrants are addressed by their OW Slot ID (independently of their world), ranging from 0x00 to 0x3F. An `arrangement` is given as a list of rows with equal lenghts. If you do not want to specify a full rectangle of screens, you can use `.` as a placeholder to allow the generator to place any screen there. The `world` property can be set to `light`, `dark` or `both` (default value) and determines for which worlds the arrangement applies. + +This example forces Death Mountain to stay connected the same as vanilla in both worlds: +``` +fixed_arrangements: + - arrangement: + - 0x03 0x04 0x05 0x06 0x07 + - 0x0B 0x0C 0x0D 0x0E . + world: both +``` + +#### restricted_positions + +Use this to restrict cells to a specified set of possible positions. The `world` property can be set to `light`, `dark` or `both` (default value) and determines for which worlds the restriction applies. + +This example forces the Sanctuary and Link's House screens in both worlds to get placed in corners of the grid: +``` +restricted_positions: + - cells: + - 0x13 + - 0x2C + positions: + - 0x00 + - 0x07 + - 0x38 + - 0x3F + world: both +``` + #### wrap_horizontal / wrap_vertical Set these to `true` to allow for overworld edge transitions to wrap from one side of a world to the opposite side. With `wrap_horizontal`, there can be east transitions on the eastern edge of the world map that send the player to the western edge of the world. With `wrap_vertical`, there can be south transitions on the southern edge of the world map that send the player to the northern edge of the world. +#### split_large_screens + +When set to `true`, the four quadrants of each large screen are placed on the grid independently of each other. + ### ow-crossed This must be defined by player. Each player number should be listed with the appropriate sections and each of these players MUST have `ow_crossed` enabled in the `settings` section in order for any values here to take effect. This section has four primary subsections: `force_crossed`, `force_noncrossed`, `limit_crossed`, and `undefined_chance`. There are also diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 0e66b2de..d31ef546 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -504,6 +504,7 @@ class CustomSettings(object): self.world_rep['ow-whirlpools'] = whirlpools = {} self.world_rep['ow-tileflips'] = flips = {} self.world_rep['ow-flutespots'] = flute = {} + self.world_rep['ow-grid'] = owgrid = {} for p in self.player_range: connections = edges[p] = {} connections['two-way'] = {} @@ -524,6 +525,57 @@ class CustomSettings(object): else: flute[p]['force'] = list(HexInt(id) for id in sorted(default_flute_connections)) flute[p]['forbid'] = [] + # layout grid + owgrid[p] = {} + grid = world.owgrid[p] + if grid is None: + grid = [ + [[HexInt(row * 8 + col) for col in range(8)] for row in range(8)], + [[HexInt(row * 8 + col) for col in range(8)] for row in range(8)] + ] + else: + grid = [ + [[HexInt(cell & 0xBF) for cell in row] for row in grid[0]], + [[HexInt(cell & 0xBF) for cell in row] for row in grid[1]] + ] + # Create fixed_arrangements for both worlds + owgrid[p]['fixed_arrangements'] = [ + { + 'arrangement': [' '.join(f'0x{cell:02X}' for cell in row) for row in grid[0]], + 'world': 'light' + }, + { + 'arrangement': [' '.join(f'0x{cell:02X}' for cell in row) for row in grid[1]], + 'world': 'dark' + } + ] + # Pin top left corners to position 0x00 + owgrid[p]['restricted_positions'] = [ + { + 'cells': [HexInt(grid[0][0][0])], + 'positions': [HexInt(0x00)], + 'world': 'light' + }, + { + 'cells': [HexInt(grid[1][0][0])], + 'positions': [HexInt(0x00)], + 'world': 'dark' + } + ] + # Set advanced grid options + horizontal_wrap = False + vertical_wrap = False + split_large_screens = False + if world.customizer: + grid_options = world.customizer.get_owgrid() + if grid_options and p in grid_options: + grid_options = grid_options[p] + horizontal_wrap = 'wrap_horizontal' in grid_options and grid_options['wrap_horizontal'] == True + vertical_wrap = 'wrap_vertical' in grid_options and grid_options['wrap_vertical'] == True + split_large_screens = 'split_large_screens' in grid_options and grid_options['split_large_screens'] == True + owgrid[p]['wrap_horizontal'] = horizontal_wrap + owgrid[p]['wrap_vertical'] = vertical_wrap + owgrid[p]['split_large_screens'] = split_large_screens for key, data in world.spoiler.overworlds.items(): player = data['player'] if 'player' in data else 1 connections = edges[player] @@ -531,7 +583,7 @@ class CustomSettings(object): connections[sub][data['entrance']] = data['exit'] for key, data in world.spoiler.whirlpools.items(): player = data['player'] if 'player' in data else 1 - whirlconnects = whirlconnects[player] + whirlconnects = whirlpools[player] sub = 'two-way' if data['direction'] == 'both' else 'one-way' whirlconnects[sub][data['entrance']] = data['exit'] diff --git a/source/overworld/LayoutGenerator.py b/source/overworld/LayoutGenerator.py index 8b86e516..56b28bd1 100644 --- a/source/overworld/LayoutGenerator.py +++ b/source/overworld/LayoutGenerator.py @@ -148,8 +148,7 @@ class LayoutGeneratorOptions: """ Configuration options for layout generation. """ - __slots__ = ('horizontal_wrap', 'vertical_wrap', 'split_large_screens', - 'large_screen_pool', 'distortion_chance', 'random_order', + __slots__ = ('horizontal_wrap', 'vertical_wrap', 'split_large_screens', 'distortion_chance', 'random_order', 'multi_choice', 'max_delay', 'first_ignore_bonus_points', 'penalty_full_edge_mismatch', 'penalty_partial_edge_mismatch', 'bonus_partial_edge_match', 'bonus_full_edge_match', 'bonus_crossed_group_match', 'bonus_fill_parallel', @@ -163,7 +162,6 @@ class LayoutGeneratorOptions: horizontal_wrap: bool = True, vertical_wrap: bool = True, split_large_screens = False, - large_screen_pool: bool = False, distortion_chance: float = 0.0, random_order: int = 0, multi_choice: int = 1, @@ -190,7 +188,6 @@ class LayoutGeneratorOptions: self.horizontal_wrap = horizontal_wrap self.vertical_wrap = vertical_wrap self.split_large_screens = split_large_screens - self.large_screen_pool = large_screen_pool self.distortion_chance = distortion_chance self.random_order = random_order self.multi_choice = multi_choice @@ -428,15 +425,14 @@ def create_piece_list(world: World, player: int, options: LayoutGeneratorOptions # Create 4 pieces for large screen quadrants for offset in [0x00, 0x01, 0x08, 0x09]: piece = create_piece(world, player, [[screen.id + offset]], overworld_screens) - if options.large_screen_pool: - piece.restriction = [large_id + offset for large_id in [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35]] piece_list.append(piece) else: piece = create_piece(world, player, [[screen.id]], overworld_screens) - if options.large_screen_pool: - piece.restriction = [s.id for s in overworld_screens.values() if not s.big] piece_list.append(piece) + # Apply position restrictions from Customizer + piece_list = apply_position_restrictions(world, player, piece_list, overworld_screens) + # Phase 2: Apply options via merging # Merge large screens if not split @@ -449,6 +445,9 @@ def create_piece_list(world: World, player: int, options: LayoutGeneratorOptions if world.mode[player] == 'standard': piece_list = merge_pieces(piece_list, [[0x23, 0x24], [0x2B, 0x2C]], world, player, overworld_screens) + # Apply fixed arrangement restrictions from Customizer + piece_list = apply_arrangement_restrictions(world, player, piece_list, overworld_screens) + # Trim pieces by removing empty rows/columns on edges piece_list = [trim_piece(piece) for piece in piece_list] @@ -533,6 +532,211 @@ def create_piece(world: World, player: int, grid: List[List[int]], overworld_scr return piece +def apply_position_restrictions(world: World, player: int, piece_list: List[Piece], overworld_screens: Dict[int, Screen]) -> List[Piece]: + """ + Apply position restrictions from Customizer to pieces at the end of phase 1. + + Position restrictions specify that certain cells can only be placed at certain positions. + The Customizer format is: + restricted_positions: + - cells: [0x13, 0x2C] + positions: [0x00, 0x07, 0x38, 0x3F] + world: both # or 'light' or 'dark' + + Note: At the end of phase 1, all pieces are 1x1 (one piece per cell). + + The world bit (0x40) in user input is ignored. The actual cell ID is determined by: + - The world where the restriction applies (light=0, dark=1) + - The mixed_state of the screen containing that cell (swapped screens flip the world bit) + """ + if not world.customizer: + return piece_list + + grid_options = world.customizer.get_owgrid() + if not grid_options or player not in grid_options: + return piece_list + + grid_options = grid_options[player] + restricted_positions = grid_options.get('restricted_positions', []) + if not restricted_positions: + return piece_list + + # Build a mapping from cell ID to piece for quick lookup + cell_to_piece = {piece.main.grid[0][0]: piece for piece in piece_list} + + for restriction_idx, restriction in enumerate(restricted_positions): + cells = restriction.get('cells', []) + positions = restriction.get('positions', []) + restriction_world = restriction.get('world', 'both') + + # Validate input + if not cells: + raise GenerationException(f"Invalid restriction in restricted_positions[{restriction_idx}]: No cells provided") + for cell_id in cells: + validate_cell_id(cell_id, f"restricted_positions[{restriction_idx}].cells") + if not positions: + raise GenerationException(f"Invalid restriction in restricted_positions[{restriction_idx}]: No positions provided") + for pos in positions: + validate_cell_id(pos, f"restricted_positions[{restriction_idx}].positions") + validate_world_value(restriction_world, f"restricted_positions[{restriction_idx}]") + + position_set = set(positions) + + for user_cell_id in cells: + # Ignore the world bit in user input + base_cell_id = user_cell_id & 0xBF + + # Determine which worlds this restriction applies to + worlds_to_check = [] + if restriction_world == 'light' or restriction_world == 'both': + worlds_to_check.append(0) # Light World + if restriction_world == 'dark' or restriction_world == 'both': + worlds_to_check.append(1) # Dark World + + for target_world in worlds_to_check: + # Determine the actual cell ID based on the target world and mixed state + screen_id = get_screen_id_from_cell(base_cell_id) + screen = overworld_screens.get(screen_id) + is_swapped = screen.mixed_state == "swapped" + + # Calculate the actual cell ID: + # - In Light World (target_world=0): use base_cell_id if normal, base_cell_id|0x40 if swapped + # - In Dark World (target_world=1): use base_cell_id|0x40 if normal, base_cell_id if swapped + if target_world == 0: + # Light World + actual_cell_id = (base_cell_id | 0x40) if is_swapped else base_cell_id + else: + # Dark World + actual_cell_id = base_cell_id if is_swapped else (base_cell_id | 0x40) + + piece = cell_to_piece.get(actual_cell_id) + + # Apply the position restriction + if piece.restriction is None: + piece.restriction = list(position_set) + else: + # Intersect with existing restrictions + piece.restriction = [p for p in piece.restriction if p in position_set] + + return piece_list + +def apply_arrangement_restrictions(world: World, player: int, piece_list: List[Piece], overworld_screens: Dict[int, Screen]) -> List[Piece]: + """ + Apply fixed arrangement restrictions from Customizer to pieces at the end of phase 2. + + Fixed arrangements specify the relative positioning between multiple screens. + The Customizer format is: + fixed_arrangements: + - arrangement: + - 0x03 0x04 0x05 0x06 0x07 + - 0x0B 0x0C 0x0D 0x0E . + world: both # or 'light' or 'dark' + + The '.' character is a placeholder that allows any screen to be placed there. + + The world bit (0x40) in user input is ignored. The actual cell ID is determined by: + - The world where the restriction applies (light=0, dark=1) + - The mixed_state of the screen containing that cell (swapped screens flip the world bit) + """ + if not world.customizer: + return piece_list + + grid_options = world.customizer.get_owgrid() + if not grid_options or player not in grid_options: + return piece_list + + grid_options = grid_options[player] + fixed_arrangements = grid_options.get('fixed_arrangements', []) + if not fixed_arrangements: + return piece_list + + for arrangement_idx, arrangement_config in enumerate(fixed_arrangements): + arrangement_rows = arrangement_config.get('arrangement', []) + arrangement_world = arrangement_config.get('world', 'both') + + # Validate world value + validate_world_value(arrangement_world, f"fixed_arrangements[{arrangement_idx}]") + + if not arrangement_rows: + raise GenerationException(f"Invalid arrangement in fixed_arrangements[{arrangement_idx}]: No arrangement provided") + + # Pre-validate the arrangement: check row lengths and entry validity + expected_row_length = None + for row_idx, row_str in enumerate(arrangement_rows): + parts = str(row_str).split() + if expected_row_length is None: + expected_row_length = len(parts) + elif len(parts) != expected_row_length: + raise GenerationException(f"Invalid arrangement in fixed_arrangements[{arrangement_idx}]: row {row_idx} has {len(parts)} entries but expected {expected_row_length} (all rows must have the same number of entries)") + + # Validate each entry + for part_idx, part in enumerate(parts): + part = part.strip() + if part == '.': + continue + # Try to parse as cell ID + try: + if part.startswith('0x') or part.startswith('0X'): + cell_id = int(part, 16) + else: + cell_id = int(part) + validate_cell_id(cell_id, f"fixed_arrangements[{arrangement_idx}].arrangement[{row_idx}][{part_idx}]") + except ValueError: + raise GenerationException(f"Invalid entry '{part}' in fixed_arrangements[{arrangement_idx}].arrangement[{row_idx}][{part_idx}]: must be a cell ID (0x00-0x7F) or '.'") + + # Determine which worlds this arrangement applies to + worlds_to_apply = [] + if arrangement_world == 'light' or arrangement_world == 'both': + worlds_to_apply.append(0) # Light World + if arrangement_world == 'dark' or arrangement_world == 'both': + worlds_to_apply.append(1) # Dark World + + for target_world in worlds_to_apply: + # Parse the arrangement into a 2D list of cell IDs, translating based on world and mixed state + # Each row is a string like "0x03 0x04 0x05 0x06 0x07" or contains '.' for wildcards + arrangement = [] + for row_str in arrangement_rows: + row = [] + # Split by whitespace + parts = str(row_str).split() + for part in parts: + part = part.strip() + if part == '.': + row.append(-1) # -1 represents wildcard + else: + # Parse as hex or decimal (already validated above) + if part.startswith('0x') or part.startswith('0X'): + user_cell_id = int(part, 16) + else: + user_cell_id = int(part) + + # Ignore the world bit in user input + base_cell_id = user_cell_id & 0xBF + + # Get the screen that contains this cell + screen_id = get_screen_id_from_cell(base_cell_id) + screen = overworld_screens.get(screen_id) + is_swapped = screen.mixed_state == "swapped" + + # Calculate the actual cell ID: + # - In Light World (target_world=0): use base_cell_id if normal, base_cell_id|0x40 if swapped + # - In Dark World (target_world=1): use base_cell_id|0x40 if normal, base_cell_id if swapped + if target_world == 0: + # Light World + actual_cell_id = (base_cell_id | 0x40) if is_swapped else base_cell_id + else: + # Dark World + actual_cell_id = base_cell_id if is_swapped else (base_cell_id | 0x40) + + row.append(actual_cell_id) + if row: + arrangement.append(row) + + # Merge the pieces according to the arrangement + piece_list = merge_pieces(piece_list, arrangement, world, player, overworld_screens) + + return piece_list + def get_piece_cells(piece: Piece) -> Set[int]: """Get all cell IDs contained in a piece.""" cells = set() @@ -542,6 +746,15 @@ def get_piece_cells(piece: Piece) -> Set[int]: cells.add(cell) return cells +def validate_cell_id(cell_id: int, context: str) -> None: + if not isinstance(cell_id, int) or cell_id < 0x00 or cell_id > 0x7F: + raise GenerationException(f"Invalid cell ID 0x{cell_id:02X} in {context}: must be in range 0x00-0x7F") + +def validate_world_value(world_value: str, context: str) -> None: + allowed_values = {'light', 'dark', 'both'} + if world_value not in allowed_values: + raise GenerationException(f"Invalid world value '{world_value}' in {context}: must be one of {allowed_values}") + def trim_piece(piece: Piece) -> Piece: """ Trim a piece by removing any full rows or columns on the edges that only consist of -1. @@ -1828,12 +2041,14 @@ def generate_random_grid_layout(world: World, player: int, connected_edges: List horizontal_wrap = False vertical_wrap = False + split_large_screens = False if world.customizer: grid_options = world.customizer.get_owgrid() if grid_options and player in grid_options: grid_options = grid_options[player] horizontal_wrap = 'wrap_horizontal' in grid_options and grid_options['wrap_horizontal'] == True vertical_wrap = 'wrap_vertical' in grid_options and grid_options['wrap_vertical'] == True + split_large_screens = 'split_large_screens' in grid_options and grid_options['split_large_screens'] == True first_ignore_bonus = 2 if not world.owParallel[player]: @@ -1843,8 +2058,7 @@ def generate_random_grid_layout(world: World, player: int, connected_edges: List options = LayoutGeneratorOptions( horizontal_wrap=horizontal_wrap, vertical_wrap=vertical_wrap, - split_large_screens=False, - large_screen_pool=False, + split_large_screens=split_large_screens, distortion_chance=0.0, random_order=6 if world.owParallel[player] else 12, multi_choice=1, From 40260c4fd445e5741cf4685d83607e49ffd87913 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 11 Feb 2026 10:20:39 -0700 Subject: [PATCH 19/76] fix: logic issue in PoD Arena (too stringent requirements) --- DoorShuffle.py | 2 +- RELEASENOTES.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 793e5ead..8833a28b 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -3713,7 +3713,7 @@ logical_connections = [ ('PoD Pit Room Block Path S', 'PoD Pit Room'), ('PoD Arena Landing Bonk Path', 'PoD Arena Bridge'), ('PoD Arena North Drop Down', 'PoD Arena Main'), - ('PoD Arena Bridge Drop Down', 'PoD Arena Main'), + ('PoD Arena Bridge Drop Down', 'PoD Arena Landing'), ('PoD Arena North to Landing Barrier - Orange', 'PoD Arena Landing'), ('PoD Arena Main to Ranged Crystal', 'PoD Arena Main - Ranged Crystal'), ('PoD Arena Main to Landing Barrier - Blue', 'PoD Arena Landing'), diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 55a2d0b3..86d2c0f2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,6 +1,7 @@ # Patch Notes * 1.5.5 + * Logic: Fixed an issue where PoD Bridge lead to Arena Main instead of Arena Landing Area. (Potentially unnecessarily requiring bombs or Somaria to progress) * HUD: Key counters are correct even when door shuffle is off From 0e432106d5ec121a8b35456e30bc95bf89d802f0 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 11 Feb 2026 10:49:33 -0700 Subject: [PATCH 20/76] fix: handle universal keys issue --- Rom.py | 2 +- data/base2current.bps | Bin 118471 -> 118478 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index 66436210..6d52e6c8 100644 --- a/Rom.py +++ b/Rom.py @@ -42,7 +42,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '794b7cd02f429873c3ad6dc74490279c' +RANDOMIZERBASEHASH = 'a882ed16dce1cb84f366afd69788e8f2' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index b499ded82f8231c4e574e1c36ca11b7618fe370b..6fc4741bddd675783c00d568226a21826521eb8f 100644 GIT binary patch delta 2140 zcmW+#3s4l<746%@Fv9@DFhpfXgto~7t;0`|pRmI6_Y+aX4-PsiDr90%H!7|O-M}aa z0}Q?Lppc(5tH3H^@7Vqp5L7^o!7?gpX%s8FL{fq)vSN}Y?Vft?+|Rl1)vJ2<_SMSn z*2>B~VXfKA3tY{sbkZ0LM`oMPlTg+1z#F%E^ubHUTT52rI6UCjNoPnPh}|W>4+0-) zvmJ-C{3+WU8(7AB%m|c%norf>R&bTqyG}{qb2f#X55epaveG`biz_LvDvGXcC@DU` zQ|{X&kj$Grj^Ma=K0AYQ!vpg9KEFx?USm}_WFkL4DIG{qTC^hua9yKf)T#+>+wi< z%eG(L0XO-XKNLHF9eeUlD0J}8?%e#HZAmAmCvSZsIe+%$8!K4G*8X_G-uwecE6Bfz zY(?7h`k&Mi2Ew!R}wDAfbLhfG2330VMJP_Y+8Kqk851qqIizv&w$^zEVax$(gE zz0A2mL{KnWRKqBkKXdXHw^W2p`ROt({g`sFDR0%2spv2T8d>J~9>O4AUPplkY(qCF z2=^sNs%;vH3LDy??ccQxqBHE*W@wB17lW1MF@`qfNO2wu!N#_fAfr9)_7~(o4_3{| zO)Nw?iG@temAsbd-Q%)DWiKVWX(bwEB3O4+KM&kt2kM;%@0ZKS=a&o?F5kkPv~W!q zxJwr}AuPX0UBy*qe{V4mJwjT4+5ma`0yk*OI_DpsLF~Sznrd~cY*}!zd6H6%ONg9H z=wNWiJ)#&{X~8{Gm8_y`t;U_22b?o(dYc$|*_oQm*wIsejvZyC)Z1Omif;$HaxZYI z`2*vFRzEt@F#~T0r9|;1I$+>u(aAySS_pFUyF~o+Vr6(pWBvY^zcb@?|>fJ(&%pmfJtlB^mIl}+6AR+}#dqcQZzo*-B_#)?=QJ^0! z!yU2afl5zF!I|1msirNXWok#P@~b6RjAgFu4z$w;p4xlDgX-pf4dr0$ua6d1`od0J zy>DN`84TZYl9ph&Xq#)iP`^J^!3wlx0sP)NMMWpmA5ifEX!cF(B7d5wBe<@5hwQ34 zyX__B0;9a8t4nR4R}r@7=_yCQUgks77vz&t^)9+*723QI_IO&l%dIsU4fX4^v42?X zfaLDe1%5=F$o_^W$DA_=thE6n)}db(f~&7stfr=?RkjJ!(;9{16YCWUy;$zNeb_ks z>9q6q5#xxVSu?C6EGRkv;@~nW34mNUfb155v)#s?vpDf0$$MO~yao9!0v)K4X%V=B z0e!a!O5`cq=+ce7jR7bk5SG}b?vv4#NxvF8$3`%Ff0Olv_BZK!GhieLo#@ygbfYhkrlQbK3hQM50?uuT9z$|!+rb6Hvb+4Zrx4xzNjIUY41PWf&KXP-35mFPb)5Cn(N!dTeiQ~HQ2 z0&H;3$LE;e@%Or($7$ZIY%IlBxtfOh6M;iwfK)(=o14=^XIB0`;C_EnOVI6uF5B}1Ctnw?X1*ZhK zj>DH|Xc^RemVKbGQPTXz&_(t#2IQ1gXjY~V+Qrwb7D|>wt^@**T_U(al%P!ncZKxp z8RmLNVND9?tsoj%Hi3_mqrZ|aO2#%;Y7>ep4)!x!Yqp}vO=8o9prp+(CJ%_lX$D98 zgr$1eVC#x!%V?9~#0lGqF9K=HxNZ6;v>b__*gI@MF~T(i>;**}Xj delta 2212 zcmW+#2~ZPh7Vg(MI8+i)6yrhDC=d)75xJBSL=KTrTtyM0;(@|4Iv%Uys?Z$`cer}Y zkBbnH&LA=vw1w71NkkY3x^Ao)FJ?#GILmP<#GP5H2)-SPYz4uoB1r^BuQNjBIy;bliVbPSM)k$4`=>W;SK;`h8j*Lu<)0GuxykX)~*G1U}5U*oPkCAy!J! zl{e{1Dcw-frz9DsamjWFW<+s8%NE>apLNpIcn7=ouT81c@8&Tl6PDc9l5g&^qsFG8 zp6B;p{Gd9nRW;sa^VRzP6B;*_-#zvUUHRqjswmT{{=giL^Mz6=l|&i!!nn@Qt}df6TK?R)msOnAY|Tp$4803YWH`65`yUy;DKIJBc`4+sSV8t{O3Fu-4#2(=!P z+zS;MgO#emKB8mGYoZu!@Pmtx%X|1kK0qca4}ciRMK=Rrng6vf2W>=_F^cN6D@$?f zxIRLksDA2WBYLbbdiC*}B$IMYA(4xg$YD;neV9#lh)eaEDuQ|-m$+1)%_7$gvwcdV zFtyO7x>_SNiqVD3;x=rSqbrenp^wOO%xC4`2kX!cIYiH$JbKclnW%N4B<=rdXmU6< zWqV%x!e3~tN=@gr(9K%MF)I5T4DaMpyMV~Qj1EPdzfEY6s{(vug=tc%L1>?7uz2OVTyLSBt2*kpCXBUi zNf=`e1-#rzfA*Edbf}w^&av37!k^r!c*{$xh|pZ7LM^WxPCnYDfN<}?%fzEgy6DJe z{ek%JXnP}>+(N%>r07?T)PvXL0P0tOV&Xo2So%iq9jT$M$S<~TJw@d6f*^3n8YBva z=sA9du7j6YuiWEVV^z32X0YfcCH8cnPpOv8V^tpRm~P+V9!=FuCIjsXhVLg#gF7b; zLo&HUGo+8;*Up6e3}wqxWgLir=!2mgF1*sIUz7hyh2K)QLj*K2pHyW zG?72QX(Sp=dcl#p#_`z|H3b^yHj~L?g1UCmp6^@TmDlNCTSCZ@N{^jXeLUJeANKeg z&DCO=OeX*EQ8Q$7+%e(=BV}H>1a`IwBUYgg^I;|!(Tq@-G9}jRm2K15 zK9Rk$M>V5)2ANGt7}4@jNQ8@s35CMhd-~Z91z4Qz?narq)9P$I+*@GzU=^gK^_|0s z$E$jL{@LzyMUI+D{Sq5M0>3oXu8U$hBv3!uVz#a5~^ z^LleIN(_U=@>TntDBbE0nm*~E^iNJyi=lB=_UZbyMO`YLT5ZIsR{Jb6zqA$2K*lgo zxyf>+)Je?lW<1ziz=+V)a3JvmaVR<*Zjbwixq3|>VJz%L*=TA61c3}CM!*7aLPsOu zh-2s9A|MU?QEDX2a74QzVUAn;))x&pCCaf8ZGgn6^;1cL1|q>nc4S6T$D_sfJMChV z1F6^b=~|0c0ZHVTE()&6Zw#?^@v!vYWmJC~rS8P2}Ml^Vf+fK0gheS_sKOT7tEA(V})WEktVM z%dc4k(_COF+8qy`GYXSO9+($lo$jXpKDsc{qfa*%{5Co|G(Tez}TuYOt$#rv|qv4Vb2Bo09?L5b?Cl z>Runt&rbzkSJ5ud8oh|m%z!K*M4&4hA$X!_o*Pw~hGps$%gSpH4bfZb*P{Pz1Qi6K zj7{*uIbbPH(KyQGS7pKaarlA5=<7U~29f+w9_$6zO=L~X4&(ClQ(pYiop91kv?J>5 zi)7w*7~TosAbL>f;KuN83&97Z*-;gSRogJ9j~Ir*>iQ_&j|P`$NJ7e9@SB_y)!z}2 zQ{IMQnW{S!mGs(IIlx4s&mA@A^pBdHyf3J|{9k(Ef|E<)>e5;_&@mhMv~NHqk}ePr f7#3nju`DbGn~rfJ{@M)?37mr)@FDwf`kem*T#|}r From f04ede1e217cd392f59e243420faa49b04825860 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 11 Feb 2026 12:22:44 -0700 Subject: [PATCH 21/76] doc: fix broken links --- index.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/index.md b/index.md index 8e3843ae..f8ba5794 100644 --- a/index.md +++ b/index.md @@ -56,7 +56,7 @@ Door Randomizer takes the dungeon experience in A Link to the Past to the next l 4. Configure your settings and generate a seed 5. Apply the patch to your A Link to the Past ROM (must be JP 1.0 version) -[See detailed installation instructions →](/installation) +[See detailed installation instructions →](installation.html) ## Community @@ -68,11 +68,11 @@ Join the discussion and get help: ## Learn More -- [Features Guide](/features.html) - Comprehensive feature documentation -- [Installation & Usage](/installation.html) - Setup and running the randomizer -- [Known Issues](/known-issues.html) - Current bugs and limitations -- [Roadmap](/roadmap.html) - Future development plans -- [Blog](/blog.html) - Latest updates and release notes +- [Features Guide](features.html) - Comprehensive feature documentation +- [Installation & Usage](installation.html) - Setup and running the randomizer +- [Known Issues](known-issues.html) - Current bugs and limitations +- [Roadmap](roadmap.html) - Future development plans +- [Blog](blog.html) - Latest updates and release notes ## Credits From b8884e90109fc9400fa9a035229e28cbb3d39cca Mon Sep 17 00:00:00 2001 From: Catobat <69204835+Catobat@users.noreply.github.com> Date: Sun, 8 Feb 2026 22:07:08 +0100 Subject: [PATCH 22/76] Add and update presets for Grid Layout Shuffle --- presets/world/owr_blockshuffle.yaml | 5 +- presets/world/owr_checkerboardshuffle.yaml | 140 ++++++++++++++++ .../world/owr_quadrantshuffle-diagonal.yaml | 3 + presets/world/owr_quadrantshuffle-full.yaml | 3 + presets/world/owr_quadrantshuffle-grid.yaml | 147 +++++++++++++++++ .../owr_quadrantshuffle-vanillaborders.yaml | 1 + presets/world/owr_ringshuffle-borders.yaml | 3 + presets/world/owr_ringshuffle-full.yaml | 3 + presets/world/owr_ringshuffle-grid.yaml | 147 +++++++++++++++++ presets/world/owr_ringshuffle-interiors.yaml | 3 + presets/world/owr_shuffle-dark.yaml | 20 +++ presets/world/owr_shuffle-horizontal.yaml | 3 + .../owr_shuffle-horizontalbycolumns.yaml | 3 + .../world/owr_shuffle-largescreenpool.yaml | 149 ++++++++++++++++++ presets/world/owr_shuffle-largescreens.yaml | 3 + presets/world/owr_shuffle-light.yaml | 22 +++ .../world/owr_shuffle-separatemountain.yaml | 3 + presets/world/owr_shuffle-smallscreens.yaml | 3 + presets/world/owr_shuffle-splitsimilar.yaml | 1 + .../owr_shuffle-splitsimilarterrain.yaml | 1 + presets/world/owr_shuffle-vanillaloop.yaml | 3 + .../world/owr_shuffle-vanillamountain.yaml | 16 ++ presets/world/owr_shuffle-vertical.yaml | 3 + presets/world/owr_shuffle-verticalbyrows.yaml | 3 + presets/world/owr_shuffle-wrappedgrid.yaml | 7 + presets/world/owr_vanilla.yaml | 19 +++ 26 files changed, 712 insertions(+), 2 deletions(-) create mode 100644 presets/world/owr_checkerboardshuffle.yaml create mode 100644 presets/world/owr_quadrantshuffle-grid.yaml create mode 100644 presets/world/owr_ringshuffle-grid.yaml create mode 100644 presets/world/owr_shuffle-largescreenpool.yaml create mode 100644 presets/world/owr_shuffle-vanillamountain.yaml create mode 100644 presets/world/owr_shuffle-wrappedgrid.yaml diff --git a/presets/world/owr_blockshuffle.yaml b/presets/world/owr_blockshuffle.yaml index 55f27c87..8da580cc 100644 --- a/presets/world/owr_blockshuffle.yaml +++ b/presets/world/owr_blockshuffle.yaml @@ -1,5 +1,6 @@ -meta: - players: 1 +settings: + 1: + ow_layout: wild ow-edges: 1: two-way: diff --git a/presets/world/owr_checkerboardshuffle.yaml b/presets/world/owr_checkerboardshuffle.yaml new file mode 100644 index 00000000..b173f67d --- /dev/null +++ b/presets/world/owr_checkerboardshuffle.yaml @@ -0,0 +1,140 @@ +settings: + 1: + ow_layout: grid +ow-grid: + 1: + restricted_positions: + - cells: + - 0x00 + - 0x02 + - 0x04 + - 0x06 + - 0x09 + - 0x0B + - 0x0D + - 0x0F + - 0x10 + - 0x12 + - 0x14 + - 0x16 + - 0x19 + - 0x1B + - 0x1D + - 0x1F + - 0x20 + - 0x22 + - 0x24 + - 0x26 + - 0x29 + - 0x2B + - 0x2D + - 0x2F + - 0x30 + - 0x32 + - 0x34 + - 0x36 + - 0x39 + - 0x3B + - 0x3D + - 0x3F + positions: + - 0x00 + - 0x02 + - 0x04 + - 0x06 + - 0x09 + - 0x0B + - 0x0D + - 0x0F + - 0x10 + - 0x12 + - 0x14 + - 0x16 + - 0x19 + - 0x1B + - 0x1D + - 0x1F + - 0x20 + - 0x22 + - 0x24 + - 0x26 + - 0x29 + - 0x2B + - 0x2D + - 0x2F + - 0x30 + - 0x32 + - 0x34 + - 0x36 + - 0x39 + - 0x3B + - 0x3D + - 0x3F + world: both + - cells: + - 0x01 + - 0x03 + - 0x05 + - 0x07 + - 0x08 + - 0x0A + - 0x0C + - 0x0E + - 0x11 + - 0x13 + - 0x15 + - 0x17 + - 0x18 + - 0x1A + - 0x1C + - 0x1E + - 0x21 + - 0x23 + - 0x25 + - 0x27 + - 0x28 + - 0x2A + - 0x2C + - 0x2E + - 0x31 + - 0x33 + - 0x35 + - 0x37 + - 0x38 + - 0x3A + - 0x3C + - 0x3E + positions: + - 0x01 + - 0x03 + - 0x05 + - 0x07 + - 0x08 + - 0x0A + - 0x0C + - 0x0E + - 0x11 + - 0x13 + - 0x15 + - 0x17 + - 0x18 + - 0x1A + - 0x1C + - 0x1E + - 0x21 + - 0x23 + - 0x25 + - 0x27 + - 0x28 + - 0x2A + - 0x2C + - 0x2E + - 0x31 + - 0x33 + - 0x35 + - 0x37 + - 0x38 + - 0x3A + - 0x3C + - 0x3E + world: both \ No newline at end of file diff --git a/presets/world/owr_quadrantshuffle-diagonal.yaml b/presets/world/owr_quadrantshuffle-diagonal.yaml index 010d9e3c..56209a63 100644 --- a/presets/world/owr_quadrantshuffle-diagonal.yaml +++ b/presets/world/owr_quadrantshuffle-diagonal.yaml @@ -1,3 +1,6 @@ +settings: + 1: + ow_layout: wild ow-edges: 1: groups: diff --git a/presets/world/owr_quadrantshuffle-full.yaml b/presets/world/owr_quadrantshuffle-full.yaml index 6dabad7c..f230cdca 100644 --- a/presets/world/owr_quadrantshuffle-full.yaml +++ b/presets/world/owr_quadrantshuffle-full.yaml @@ -1,3 +1,6 @@ +settings: + 1: + ow_layout: wild ow-edges: 1: groups: diff --git a/presets/world/owr_quadrantshuffle-grid.yaml b/presets/world/owr_quadrantshuffle-grid.yaml new file mode 100644 index 00000000..c655fa49 --- /dev/null +++ b/presets/world/owr_quadrantshuffle-grid.yaml @@ -0,0 +1,147 @@ +settings: + 1: + ow_layout: grid +ow-grid: + 1: + restricted_positions: + - cells: + - 0x00 + - 0x01 + - 0x02 + - 0x03 + - 0x08 + - 0x09 + - 0x0A + - 0x0B + - 0x10 + - 0x11 + - 0x12 + - 0x13 + - 0x18 + - 0x19 + - 0x1A + - 0x1B + positions: + - 0x00 + - 0x01 + - 0x02 + - 0x03 + - 0x08 + - 0x09 + - 0x0A + - 0x0B + - 0x10 + - 0x11 + - 0x12 + - 0x13 + - 0x18 + - 0x19 + - 0x1A + - 0x1B + world: both + - cells: + - 0x20 + - 0x21 + - 0x22 + - 0x23 + - 0x28 + - 0x29 + - 0x2A + - 0x2B + - 0x30 + - 0x31 + - 0x32 + - 0x33 + - 0x38 + - 0x39 + - 0x3A + - 0x3B + positions: + - 0x20 + - 0x21 + - 0x22 + - 0x23 + - 0x28 + - 0x29 + - 0x2A + - 0x2B + - 0x30 + - 0x31 + - 0x32 + - 0x33 + - 0x38 + - 0x39 + - 0x3A + - 0x3B + world: both + - cells: + - 0x04 + - 0x05 + - 0x06 + - 0x07 + - 0x0C + - 0x0D + - 0x0E + - 0x0F + - 0x14 + - 0x15 + - 0x16 + - 0x17 + - 0x1C + - 0x1D + - 0x1E + - 0x1F + positions: + - 0x04 + - 0x05 + - 0x06 + - 0x07 + - 0x0C + - 0x0D + - 0x0E + - 0x0F + - 0x14 + - 0x15 + - 0x16 + - 0x17 + - 0x1C + - 0x1D + - 0x1E + - 0x1F + world: both + - cells: + - 0x24 + - 0x25 + - 0x26 + - 0x27 + - 0x2C + - 0x2D + - 0x2E + - 0x2F + - 0x34 + - 0x35 + - 0x36 + - 0x37 + - 0x3C + - 0x3D + - 0x3E + - 0x3F + positions: + - 0x24 + - 0x25 + - 0x26 + - 0x27 + - 0x2C + - 0x2D + - 0x2E + - 0x2F + - 0x34 + - 0x35 + - 0x36 + - 0x37 + - 0x3C + - 0x3D + - 0x3E + - 0x3F + world: both + split_large_screens: true \ No newline at end of file diff --git a/presets/world/owr_quadrantshuffle-vanillaborders.yaml b/presets/world/owr_quadrantshuffle-vanillaborders.yaml index 22f9e8fb..41c6b7c8 100644 --- a/presets/world/owr_quadrantshuffle-vanillaborders.yaml +++ b/presets/world/owr_quadrantshuffle-vanillaborders.yaml @@ -1,5 +1,6 @@ settings: 1: + ow_layout: wild ow_whirlpool: false ow-edges: 1: diff --git a/presets/world/owr_ringshuffle-borders.yaml b/presets/world/owr_ringshuffle-borders.yaml index 3cbdb28f..8c96582b 100644 --- a/presets/world/owr_ringshuffle-borders.yaml +++ b/presets/world/owr_ringshuffle-borders.yaml @@ -1,3 +1,6 @@ +settings: + 1: + ow_layout: wild ow-edges: 1: two-way: diff --git a/presets/world/owr_ringshuffle-full.yaml b/presets/world/owr_ringshuffle-full.yaml index c271befe..e113aa2d 100644 --- a/presets/world/owr_ringshuffle-full.yaml +++ b/presets/world/owr_ringshuffle-full.yaml @@ -1,3 +1,6 @@ +settings: + 1: + ow_layout: wild ow-edges: 1: groups: diff --git a/presets/world/owr_ringshuffle-grid.yaml b/presets/world/owr_ringshuffle-grid.yaml new file mode 100644 index 00000000..ded19fe2 --- /dev/null +++ b/presets/world/owr_ringshuffle-grid.yaml @@ -0,0 +1,147 @@ +settings: + 1: + ow_layout: grid +ow-grid: + 1: + restricted_positions: + - cells: + - 0x00 + - 0x01 + - 0x02 + - 0x03 + - 0x04 + - 0x05 + - 0x06 + - 0x07 + - 0x08 + - 0x0F + - 0x10 + - 0x17 + - 0x18 + - 0x1F + - 0x20 + - 0x27 + - 0x28 + - 0x2F + - 0x30 + - 0x37 + - 0x38 + - 0x39 + - 0x3A + - 0x3B + - 0x3C + - 0x3D + - 0x3E + - 0x3F + positions: + - 0x00 + - 0x01 + - 0x02 + - 0x03 + - 0x04 + - 0x05 + - 0x06 + - 0x07 + - 0x08 + - 0x0F + - 0x10 + - 0x17 + - 0x18 + - 0x1F + - 0x20 + - 0x27 + - 0x28 + - 0x2F + - 0x30 + - 0x37 + - 0x38 + - 0x39 + - 0x3A + - 0x3B + - 0x3C + - 0x3D + - 0x3E + - 0x3F + world: both + - cells: + - 0x09 + - 0x0A + - 0x0B + - 0x0C + - 0x0D + - 0x0E + - 0x11 + - 0x16 + - 0x19 + - 0x1E + - 0x21 + - 0x26 + - 0x29 + - 0x2E + - 0x31 + - 0x32 + - 0x33 + - 0x34 + - 0x35 + - 0x36 + positions: + - 0x09 + - 0x0A + - 0x0B + - 0x0C + - 0x0D + - 0x0E + - 0x11 + - 0x16 + - 0x19 + - 0x1E + - 0x21 + - 0x26 + - 0x29 + - 0x2E + - 0x31 + - 0x32 + - 0x33 + - 0x34 + - 0x35 + - 0x36 + world: both + - cells: + - 0x12 + - 0x13 + - 0x14 + - 0x15 + - 0x1A + - 0x1D + - 0x22 + - 0x25 + - 0x2A + - 0x2B + - 0x2C + - 0x2D + positions: + - 0x12 + - 0x13 + - 0x14 + - 0x15 + - 0x1A + - 0x1D + - 0x22 + - 0x25 + - 0x2A + - 0x2B + - 0x2C + - 0x2D + world: both + - cells: + - 0x1B + - 0x1C + - 0x23 + - 0x24 + positions: + - 0x1B + - 0x1C + - 0x23 + - 0x24 + world: both + split_large_screens: true \ No newline at end of file diff --git a/presets/world/owr_ringshuffle-interiors.yaml b/presets/world/owr_ringshuffle-interiors.yaml index 0dcfbaa7..d63655ad 100644 --- a/presets/world/owr_ringshuffle-interiors.yaml +++ b/presets/world/owr_ringshuffle-interiors.yaml @@ -1,3 +1,6 @@ +settings: + 1: + ow_layout: wild ow-edges: 1: two-way: diff --git a/presets/world/owr_shuffle-dark.yaml b/presets/world/owr_shuffle-dark.yaml index 3b27e6d2..e6e7bf48 100644 --- a/presets/world/owr_shuffle-dark.yaml +++ b/presets/world/owr_shuffle-dark.yaml @@ -1,5 +1,6 @@ settings: 1: + ow_parallel: false ow_whirlpool: false ow-edges: 1: @@ -77,6 +78,25 @@ ow-edges: Desert Pass EC*: Dam WC* Desert Pass ES*: Dam WS* Dam EC*: South Pass WC* +ow-grid: + 1: + fixed_arrangements: + - arrangement: + - 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 + - 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F + - 0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17 + - 0x18 0x19 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F + - 0x20 0x21 0x22 0x23 0x24 0x25 0x26 0x27 + - 0x28 0x29 0x2A 0x2B 0x2C 0x2D 0x2E 0x2F + - 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37 + - 0x38 0x39 0x3A 0x3B 0x3C 0x3D 0x3E 0x3F + world: light + restricted_positions: + - cells: + - 0x00 + positions: + - 0x00 + world: light ow-whirlpools: 1: two-way: diff --git a/presets/world/owr_shuffle-horizontal.yaml b/presets/world/owr_shuffle-horizontal.yaml index 9eba3118..7fead28b 100644 --- a/presets/world/owr_shuffle-horizontal.yaml +++ b/presets/world/owr_shuffle-horizontal.yaml @@ -1,3 +1,6 @@ +settings: + 1: + ow_layout: wild ow-edges: 1: two-way: diff --git a/presets/world/owr_shuffle-horizontalbycolumns.yaml b/presets/world/owr_shuffle-horizontalbycolumns.yaml index 117b81d5..f56e5696 100644 --- a/presets/world/owr_shuffle-horizontalbycolumns.yaml +++ b/presets/world/owr_shuffle-horizontalbycolumns.yaml @@ -1,3 +1,6 @@ +settings: + 1: + ow_layout: wild ow-edges: 1: two-way: diff --git a/presets/world/owr_shuffle-largescreenpool.yaml b/presets/world/owr_shuffle-largescreenpool.yaml new file mode 100644 index 00000000..600493b8 --- /dev/null +++ b/presets/world/owr_shuffle-largescreenpool.yaml @@ -0,0 +1,149 @@ +settings: + 1: + ow_layout: grid +ow-grid: + 1: + restricted_positions: + - cells: + - 0x00 + - 0x03 + - 0x05 + - 0x18 + - 0x1B + - 0x1E + - 0x30 + - 0x35 + positions: + - 0x00 + - 0x03 + - 0x05 + - 0x18 + - 0x1B + - 0x1E + - 0x30 + - 0x35 + world: both + - cells: + - 0x01 + - 0x04 + - 0x06 + - 0x19 + - 0x1C + - 0x1F + - 0x31 + - 0x36 + positions: + - 0x01 + - 0x04 + - 0x06 + - 0x19 + - 0x1C + - 0x1F + - 0x31 + - 0x36 + world: both + - cells: + - 0x08 + - 0x0B + - 0x0D + - 0x20 + - 0x23 + - 0x26 + - 0x38 + - 0x3D + positions: + - 0x08 + - 0x0B + - 0x0D + - 0x20 + - 0x23 + - 0x26 + - 0x38 + - 0x3D + world: both + - cells: + - 0x09 + - 0x0C + - 0x0E + - 0x21 + - 0x24 + - 0x27 + - 0x39 + - 0x3E + positions: + - 0x09 + - 0x0C + - 0x0E + - 0x21 + - 0x24 + - 0x27 + - 0x39 + - 0x3E + world: both + - cells: + - 0x02 + - 0x07 + - 0x0A + - 0x0F + - 0x10 + - 0x11 + - 0x12 + - 0x13 + - 0x14 + - 0x15 + - 0x16 + - 0x17 + - 0x1A + - 0x1D + - 0x22 + - 0x25 + - 0x28 + - 0x29 + - 0x2A + - 0x2B + - 0x2C + - 0x2D + - 0x2E + - 0x2F + - 0x32 + - 0x33 + - 0x34 + - 0x37 + - 0x3A + - 0x3B + - 0x3C + - 0x3F + positions: + - 0x02 + - 0x07 + - 0x0A + - 0x0F + - 0x10 + - 0x11 + - 0x12 + - 0x13 + - 0x14 + - 0x15 + - 0x16 + - 0x17 + - 0x1A + - 0x1D + - 0x22 + - 0x25 + - 0x28 + - 0x29 + - 0x2A + - 0x2B + - 0x2C + - 0x2D + - 0x2E + - 0x2F + - 0x32 + - 0x33 + - 0x34 + - 0x37 + - 0x3A + - 0x3B + - 0x3C + - 0x3F + world: both \ No newline at end of file diff --git a/presets/world/owr_shuffle-largescreens.yaml b/presets/world/owr_shuffle-largescreens.yaml index 47798cfa..9bc37ecf 100644 --- a/presets/world/owr_shuffle-largescreens.yaml +++ b/presets/world/owr_shuffle-largescreens.yaml @@ -1,3 +1,6 @@ +settings: + 1: + ow_layout: wild settings: 1: ow_whirlpool: false diff --git a/presets/world/owr_shuffle-light.yaml b/presets/world/owr_shuffle-light.yaml index 4feaa75e..9b532c9a 100644 --- a/presets/world/owr_shuffle-light.yaml +++ b/presets/world/owr_shuffle-light.yaml @@ -1,3 +1,6 @@ +settings: + 1: + ow_parallel: false ow-edges: 1: two-way: @@ -69,6 +72,25 @@ ow-edges: Swamp Nook EC*: Swamp WC* Swamp Nook ES*: Swamp WS* Swamp EC*: Dark South Pass WC* +ow-grid: + 1: + fixed_arrangements: + - arrangement: + - 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 + - 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F + - 0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17 + - 0x18 0x19 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F + - 0x20 0x21 0x22 0x23 0x24 0x25 0x26 0x27 + - 0x28 0x29 0x2A 0x2B 0x2C 0x2D 0x2E 0x2F + - 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37 + - 0x38 0x39 0x3A 0x3B 0x3C 0x3D 0x3E 0x3F + world: dark + restricted_positions: + - cells: + - 0x00 + positions: + - 0x00 + world: dark ow-whirlpools: 1: two-way: diff --git a/presets/world/owr_shuffle-separatemountain.yaml b/presets/world/owr_shuffle-separatemountain.yaml index 02a53d9c..0f07f4e6 100644 --- a/presets/world/owr_shuffle-separatemountain.yaml +++ b/presets/world/owr_shuffle-separatemountain.yaml @@ -1,3 +1,6 @@ +settings: + 1: + ow_layout: wild ow-edges: 1: groups: diff --git a/presets/world/owr_shuffle-smallscreens.yaml b/presets/world/owr_shuffle-smallscreens.yaml index a12ad95d..ab0af00b 100644 --- a/presets/world/owr_shuffle-smallscreens.yaml +++ b/presets/world/owr_shuffle-smallscreens.yaml @@ -1,3 +1,6 @@ +settings: + 1: + ow_layout: wild ow-edges: 1: two-way: diff --git a/presets/world/owr_shuffle-splitsimilar.yaml b/presets/world/owr_shuffle-splitsimilar.yaml index bf1dde3d..825aa744 100644 --- a/presets/world/owr_shuffle-splitsimilar.yaml +++ b/presets/world/owr_shuffle-splitsimilar.yaml @@ -1,5 +1,6 @@ settings: 1: + ow_layout: wild ow_terrain: false ow-edges: 1: diff --git a/presets/world/owr_shuffle-splitsimilarterrain.yaml b/presets/world/owr_shuffle-splitsimilarterrain.yaml index 2ade18e4..0e97404a 100644 --- a/presets/world/owr_shuffle-splitsimilarterrain.yaml +++ b/presets/world/owr_shuffle-splitsimilarterrain.yaml @@ -1,5 +1,6 @@ settings: 1: + ow_layout: wild ow_terrain: true ow-edges: 1: diff --git a/presets/world/owr_shuffle-vanillaloop.yaml b/presets/world/owr_shuffle-vanillaloop.yaml index bc4223fb..7a31c031 100644 --- a/presets/world/owr_shuffle-vanillaloop.yaml +++ b/presets/world/owr_shuffle-vanillaloop.yaml @@ -1,3 +1,6 @@ +settings: + 1: + ow_layout: wild ow-edges: 1: two-way: diff --git a/presets/world/owr_shuffle-vanillamountain.yaml b/presets/world/owr_shuffle-vanillamountain.yaml new file mode 100644 index 00000000..33cf3f3c --- /dev/null +++ b/presets/world/owr_shuffle-vanillamountain.yaml @@ -0,0 +1,16 @@ +ow-edges: + 1: + two-way: + West Death Mountain EN*: East Death Mountain WN* + West Death Mountain ES*: East Death Mountain WS* + East Death Mountain EN*: Death Mountain TR Pegs WN* + West Dark Death Mountain EN*: East Dark Death Mountain WN* + West Dark Death Mountain ES*: East Dark Death Mountain WS* + East Dark Death Mountain EN*: Turtle Rock WN* +ow-grid: + 1: + fixed_arrangements: + - arrangement: + - 0x03 0x04 0x05 0x06 0x07 + - 0x0B 0x0C 0x0D 0x0E . + world: both \ No newline at end of file diff --git a/presets/world/owr_shuffle-vertical.yaml b/presets/world/owr_shuffle-vertical.yaml index d037f0c7..6994698e 100644 --- a/presets/world/owr_shuffle-vertical.yaml +++ b/presets/world/owr_shuffle-vertical.yaml @@ -1,3 +1,6 @@ +settings: + 1: + ow_layout: wild ow-edges: 1: two-way: diff --git a/presets/world/owr_shuffle-verticalbyrows.yaml b/presets/world/owr_shuffle-verticalbyrows.yaml index 25650d60..44948112 100644 --- a/presets/world/owr_shuffle-verticalbyrows.yaml +++ b/presets/world/owr_shuffle-verticalbyrows.yaml @@ -1,3 +1,6 @@ +settings: + 1: + ow_layout: wild ow-edges: 1: two-way: diff --git a/presets/world/owr_shuffle-wrappedgrid.yaml b/presets/world/owr_shuffle-wrappedgrid.yaml new file mode 100644 index 00000000..537498e3 --- /dev/null +++ b/presets/world/owr_shuffle-wrappedgrid.yaml @@ -0,0 +1,7 @@ +settings: + 1: + ow_layout: grid +ow-grid: + 1: + wrap_horizontal: true + wrap_vertical: true \ No newline at end of file diff --git a/presets/world/owr_vanilla.yaml b/presets/world/owr_vanilla.yaml index 9c86cc79..992aaa0f 100644 --- a/presets/world/owr_vanilla.yaml +++ b/presets/world/owr_vanilla.yaml @@ -144,6 +144,25 @@ ow-edges: Swamp EC*: Dark South Pass WC* South Pass ES*: Lake Hylia WS* Dark South Pass ES*: Ice Lake WS* +ow-grid: + 1: + fixed_arrangements: + - arrangement: + - 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 + - 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F + - 0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17 + - 0x18 0x19 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F + - 0x20 0x21 0x22 0x23 0x24 0x25 0x26 0x27 + - 0x28 0x29 0x2A 0x2B 0x2C 0x2D 0x2E 0x2F + - 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37 + - 0x38 0x39 0x3A 0x3B 0x3C 0x3D 0x3E 0x3F + world: both + restricted_positions: + - cells: + - 0x00 + positions: + - 0x00 + world: both ow-whirlpools: 1: two-way: From 6a59f582306d1a317323305d3d227a9859c9b992 Mon Sep 17 00:00:00 2001 From: Catobat <69204835+Catobat@users.noreply.github.com> Date: Sat, 21 Feb 2026 21:19:16 +0100 Subject: [PATCH 23/76] Consider number of separate overworld areas when picking a grid layout --- OverworldShuffle.py | 57 +++++++++++++++++++++++++---- source/overworld/LayoutGenerator.py | 44 ++++++++++++++-------- 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index b1eca427..6a9dd637 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -567,7 +567,7 @@ def link_overworld(world, player): remove_connected(forward_edge_sets, back_edge_sets) assert len(connected_edges) == len(default_connections) * 2, connected_edges - valid_layout = validate_layout(world, player) + valid_layout = world.accessibility[player] == 'none' or validate_layout(world, player) tries -= 1 assert valid_layout, 'Could not find a valid OW layout' @@ -1369,9 +1369,6 @@ def build_accessible_region_list(world, start_region, player, build_copy_world=F return explored_regions def validate_layout(world, player): - if world.accessibility[player] == 'none': - return True - entrance_connectors = { 'East Death Mountain (Bottom)': ['East Death Mountain (Top East)'], 'Kakariko Suburb Area': ['Maze Race Ledge'], @@ -1458,7 +1455,7 @@ def validate_layout(world, player): while unreachable_count != len(unreachable_regions): # find unreachable regions unreachable_regions = {} - for region_name in list(OWTileRegions.copy().keys()): + for region_name in list(OWTileRegions.keys()): if region_name not in explored_regions and region_name not in isolated_regions: region = world.get_region(region_name, player) unreachable_regions[region_name] = region @@ -1501,9 +1498,55 @@ def validate_layout(world, player): if len(unreachable_regions): return False - + return True - + +def get_separate_ow_areas(world, player): + """ + Returns a list of separated areas in the overworld layout. + It looks at the distinct connected components when only considering + OW edge and whirlpool connections (no entrances, portals, mirror, or flute). + Uses Union-Find to handle directed edges properly (treats them as undirected). + """ + parent = {} + + def find(x): + if x not in parent: + parent[x] = x + if parent[x] != x: + parent[x] = find(parent[x]) # Path compression + return parent[x] + + def union(x, y): + root_x = find(x) + root_y = find(y) + if root_x != root_y: + parent[root_y] = root_x + + all_regions = set(OWTileRegions.keys()) - set(isolated_regions) + considered_exit_spot_types = set(['OpenTerrain', 'OWTerrain', 'Ledge', 'OWEdge', 'Whirlpool']) + + # Initialize all regions in Union-Find + for region_name in all_regions: + find(region_name) + + # Build connections by examining all edges (treating directed as undirected) + for region_name in all_regions: + region = world.get_region(region_name, player) + for exit in region.exits: + if exit.spot_type in considered_exit_spot_types and exit.connected_region is not None and exit.connected_region.name in all_regions: + union(region_name, exit.connected_region.name) + + # Group regions by their root + areas = {} + for region_name in all_regions: + root = find(region_name) + if root not in areas: + areas[root] = [] + areas[root].append(region_name) + + return list(areas.values()) + test_connections = [ #('Links House ES', 'Octoballoon WS'), #('Links House NE', 'Lost Woods Pass SW') diff --git a/source/overworld/LayoutGenerator.py b/source/overworld/LayoutGenerator.py index 56b28bd1..7e6a05bc 100644 --- a/source/overworld/LayoutGenerator.py +++ b/source/overworld/LayoutGenerator.py @@ -4,7 +4,7 @@ import RaceRandom as random import random as _random from typing import List, Dict, Optional, Set, Tuple from BaseClasses import OWEdge, World, Direction, Terrain -from OverworldShuffle import connect_two_way, validate_layout +from OverworldShuffle import connect_two_way, get_separate_ow_areas, validate_layout ENABLE_KEEP_SIMILAR_SPECIAL_HANDLING = False DRAW_IMAGE = True @@ -155,7 +155,7 @@ class LayoutGeneratorOptions: 'forced_non_crossed_edges', 'forced_crossed_edges', 'check_reachability', 'crossed_chance', 'crossed_limit', 'sort_by_edge_sides', 'sort_by_max_edges_per_side', 'sort_by_piece_size', - 'min_runs', 'max_runs', 'target_runs_times_successes') + 'min_runs', 'max_runs', 'target_runs_times_successes', 'score_mult_separate_areas') def __init__( self, @@ -183,7 +183,8 @@ class LayoutGeneratorOptions: sort_by_piece_size: bool = False, min_runs: int = 100, max_runs: int = 10000, - target_runs_times_successes: int = 5000 + target_runs_times_successes: int = 5000, + score_mult_separate_areas: float = 4 ): self.horizontal_wrap = horizontal_wrap self.vertical_wrap = vertical_wrap @@ -210,6 +211,7 @@ class LayoutGeneratorOptions: self.min_runs = min_runs self.max_runs = max_runs self.target_runs_times_successes = target_runs_times_successes + self.score_mult_separate_areas = score_mult_separate_areas class LayoutGeneratorResult: """ @@ -1655,14 +1657,18 @@ def place_single_restriction_pieces( return remaining_pieces, placed_count def get_random_layout(world: World, player: int, connected_edges_cache: List[str], pieces_to_place: List[Piece], options: LayoutGeneratorOptions, prio_edges: List[str], overworld_screens: Dict[int, Screen]) -> LayoutGeneratorResult: + skip_validate_layout = world.accessibility[player] == 'none' + score_mult_separate_areas = options.score_mult_separate_areas total_score = 0 best_score = -1000000 worst_score = 1000000 best_grid_info = None + separate_areas = None # Pre-place pieces with single-element restriction lists base_grid_info = create_empty_grid_info(0.0) remaining_pieces, preplaced_count = place_single_restriction_pieces(world, player, base_grid_info, options, pieces_to_place) + logger = logging.getLogger('') successes = 0 failures = 0 @@ -1716,21 +1722,18 @@ def get_random_layout(world: World, player: int, connected_edges_cache: List[str # Successfully placed all pieces if options.check_reachability: disabled_count = connect_edges_for_screen_layout(world, player, grid_info, options, connected_edges, prio_edges, overworld_screens, False) - valid_layout = validate_layout(world, player) - # Clean up connected entrances and edges - for edge_name in connected_edges: - if edge_name not in connected_edges_cache: - entrance = world.get_entrance(edge_name, player) - entrance.connected_region.entrances.remove(entrance) - entrance.connected_region = None - edge = world.get_owedge(edge_name, player) - edge.dest = None + valid_layout = skip_validate_layout or validate_layout(world, player) if not valid_layout: + clean_up_connected_edges(world, player, connected_edges_cache, connected_edges) failures += 1 continue - logging.getLogger('').debug("Found valid layout with " + str(disabled_count)+ " disabled edges") - successes += 1 score = -disabled_count + if score_mult_separate_areas > 0: + separate_areas = len(get_separate_ow_areas(world, player)) + score -= score_mult_separate_areas * separate_areas + logger.debug("Found valid layout with " + str(disabled_count) + " disabled edges and " + str(separate_areas) + " separate areas") + clean_up_connected_edges(world, player, connected_edges_cache, connected_edges) + successes += 1 else: successes += 1 score = major_score @@ -1759,6 +1762,15 @@ def get_random_layout(world: World, player: int, connected_edges_cache: List[str failures=failures ) +def clean_up_connected_edges(world: World, player: int, connected_edges_cache: List[str], connected_edges: List[str]) -> None: + for edge_name in connected_edges: + if edge_name not in connected_edges_cache: + entrance = world.get_entrance(edge_name, player) + entrance.connected_region.entrances.remove(entrance) + entrance.connected_region = None + edge = world.get_owedge(edge_name, player) + edge.dest = None + def get_prioritized_edges(world: World, player: int) -> List[str]: prio_edges = [] if world.accessibility[player] != 'none': @@ -2080,7 +2092,8 @@ def generate_random_grid_layout(world: World, player: int, connected_edges: List sort_by_piece_size=True, min_runs=100, max_runs=10000, - target_runs_times_successes=5000 + target_runs_times_successes=5000, + score_mult_separate_areas=4 ) overworld_screens = initialize_screens(world, player) @@ -2112,6 +2125,7 @@ def generate_random_grid_layout(world: World, player: int, connected_edges: List logger.debug(f" Successes: {result.successes}") logger.debug(f" Failures: {result.failures}") logger.debug(f" Generation time: {elapsed_time:.3f}s") + logger.debug(f" Layouts per second: {(result.successes+result.failures)/elapsed_time:.3f}") if DRAW_IMAGE: logger.debug("Creating layout visualization...") From 54e4176311419053d4056f9551865ba3774817a2 Mon Sep 17 00:00:00 2001 From: Catobat <69204835+Catobat@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:06:47 +0100 Subject: [PATCH 24/76] Consider distance between starting locations when picking a grid layout --- source/overworld/LayoutGenerator.py | 49 ++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/source/overworld/LayoutGenerator.py b/source/overworld/LayoutGenerator.py index 7e6a05bc..26666a47 100644 --- a/source/overworld/LayoutGenerator.py +++ b/source/overworld/LayoutGenerator.py @@ -7,7 +7,7 @@ from BaseClasses import OWEdge, World, Direction, Terrain from OverworldShuffle import connect_two_way, get_separate_ow_areas, validate_layout ENABLE_KEEP_SIMILAR_SPECIAL_HANDLING = False -DRAW_IMAGE = True +DRAW_IMAGE = False large_screen_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35] + [0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75] @@ -155,7 +155,8 @@ class LayoutGeneratorOptions: 'forced_non_crossed_edges', 'forced_crossed_edges', 'check_reachability', 'crossed_chance', 'crossed_limit', 'sort_by_edge_sides', 'sort_by_max_edges_per_side', 'sort_by_piece_size', - 'min_runs', 'max_runs', 'target_runs_times_successes', 'score_mult_separate_areas') + 'min_runs', 'max_runs', 'target_runs_times_successes', 'score_mult_separate_areas', + 'start_loc_min_distance', 'score_mult_start_loc_distance') def __init__( self, @@ -184,7 +185,9 @@ class LayoutGeneratorOptions: min_runs: int = 100, max_runs: int = 10000, target_runs_times_successes: int = 5000, - score_mult_separate_areas: float = 4 + score_mult_separate_areas: float = 4, + start_loc_min_distance: int = 4, + score_mult_start_loc_distance: float = 3 ): self.horizontal_wrap = horizontal_wrap self.vertical_wrap = vertical_wrap @@ -212,6 +215,8 @@ class LayoutGeneratorOptions: self.max_runs = max_runs self.target_runs_times_successes = target_runs_times_successes self.score_mult_separate_areas = score_mult_separate_areas + self.start_loc_min_distance = start_loc_min_distance + self.score_mult_start_loc_distance = score_mult_start_loc_distance class LayoutGeneratorResult: """ @@ -1659,11 +1664,13 @@ def place_single_restriction_pieces( def get_random_layout(world: World, player: int, connected_edges_cache: List[str], pieces_to_place: List[Piece], options: LayoutGeneratorOptions, prio_edges: List[str], overworld_screens: Dict[int, Screen]) -> LayoutGeneratorResult: skip_validate_layout = world.accessibility[player] == 'none' score_mult_separate_areas = options.score_mult_separate_areas + apply_start_loc_penalty = options.score_mult_start_loc_distance > 0 and world.shuffle[player] == 'vanilla' and (world.is_dark_chapel_start(player) or world.doorShuffle[player] == 'vanilla' or world.intensity[player] < 3 or world.mode[player] == 'standard') total_score = 0 best_score = -1000000 worst_score = 1000000 best_grid_info = None separate_areas = None + start_loc_distance = None # Pre-place pieces with single-element restriction lists base_grid_info = create_empty_grid_info(0.0) @@ -1731,7 +1738,12 @@ def get_random_layout(world: World, player: int, connected_edges_cache: List[str if score_mult_separate_areas > 0: separate_areas = len(get_separate_ow_areas(world, player)) score -= score_mult_separate_areas * separate_areas - logger.debug("Found valid layout with " + str(disabled_count) + " disabled edges and " + str(separate_areas) + " separate areas") + if apply_start_loc_penalty: + start_loc_distance = get_start_loc_distance(world, player, grid_info.grid, options) + min_dist = options.start_loc_min_distance + if start_loc_distance < min_dist: + score -= options.score_mult_start_loc_distance * (min_dist - start_loc_distance) + logger.debug("Found valid layout with " + str(disabled_count) + " disabled edges and " + str(separate_areas) + " separate areas and distance " + str(start_loc_distance) + " between start locations") clean_up_connected_edges(world, player, connected_edges_cache, connected_edges) successes += 1 else: @@ -1771,6 +1783,31 @@ def clean_up_connected_edges(world: World, player: int, connected_edges_cache: L edge = world.get_owedge(edge_name, player) edge.dest = None +def find_cell_position(grid: List[List[List[int]]], cell_id: int) -> Optional[Tuple[int, int, int]]: + """Find the position of a cell in the grid, returning (world, row, col) or None if not found.""" + for w in range(2): + for row in range(8): + for col in range(8): + if grid[w][row][col] == cell_id: + return (w, row, col) + return None + +def get_start_loc_distance(world: World, player: int, grid: List[List[List[int]]], options: LayoutGeneratorOptions) -> float: + """Computes the starting location Manhattan distance on the grid, treating the world as a third dimension (switching world adds 1 to the distance).""" + pos_lh = find_cell_position(grid, 0x6C if world.is_bombshop_start(player) else 0x2C) + pos_sanc = find_cell_position(grid, 0x53 if world.is_dark_chapel_start(player) else 0x13) + if pos_lh is None or pos_sanc is None: + raise GenerationException("Could not find starting location cells, something went wrong with grid layout generation!") + w1, row1, col1 = pos_lh + w2, row2, col2 = pos_sanc + row_diff = abs(row1 - row2) + col_diff = abs(col1 - col2) + if options.horizontal_wrap: + col_diff = min(col_diff, 8 - col_diff) + if options.vertical_wrap: + row_diff = min(row_diff, 8 - row_diff) + return row_diff + col_diff + abs(w1 - w2) + def get_prioritized_edges(world: World, player: int) -> List[str]: prio_edges = [] if world.accessibility[player] != 'none': @@ -2093,7 +2130,9 @@ def generate_random_grid_layout(world: World, player: int, connected_edges: List min_runs=100, max_runs=10000, target_runs_times_successes=5000, - score_mult_separate_areas=4 + score_mult_separate_areas=4, + start_loc_min_distance=4, + score_mult_start_loc_distance=3 ) overworld_screens = initialize_screens(world, player) From af9b61ad7dd04ba00e724c576fd1646071faa823 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 11 Mar 2026 10:08:54 -0600 Subject: [PATCH 25/76] fix: beemizer + pottery + multiworld generation issue --- Fill.py | 9 +++++++-- Main.py | 2 +- PastReleaseNotes.md | 3 +++ RELEASENOTES.md | 5 ++--- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Fill.py b/Fill.py index 349ff83e..935dad37 100644 --- a/Fill.py +++ b/Fill.py @@ -608,8 +608,13 @@ def fast_fill_pot_for_multiworld(world, item_pool, fill_locations): flex = 256 - world.data_tables[player].pot_secret_table.multiworld_count fill_count = len(pot_fill_locations[player]) - flex if fill_count > 0: - fill_spots = random.sample(pot_fill_locations[player], fill_count) - fill_items = random.sample(pot_item_pool[player], fill_count) + first_count = min(fill_count, len(pot_item_pool[player])) + fill_items = random.sample(pot_item_pool[player], first_count) + remaining = fill_count - first_count + if remaining > 0: + other_items = [i for i in item_pool if i.player == player and i not in pot_item_pool[player]] + fill_items += random.sample(other_items, min(remaining, len(other_items))) + fill_spots = random.sample(pot_fill_locations[player], len(fill_items)) for x in fill_items: item_pool.remove(x) for x in fill_spots: diff --git a/Main.py b/Main.py index 3e3a3952..d6925fbd 100644 --- a/Main.py +++ b/Main.py @@ -38,7 +38,7 @@ from source.enemizer.DamageTables import DamageTable from source.enemizer.Enemizer import randomize_enemies from source.rom.DataTables import init_data_tables -version_number = '1.5.5' +version_number = '1.5.6' version_branch = '-u' __version__ = f'{version_number}{version_branch}' diff --git a/PastReleaseNotes.md b/PastReleaseNotes.md index e2915b60..d7984bec 100644 --- a/PastReleaseNotes.md +++ b/PastReleaseNotes.md @@ -10,6 +10,9 @@ # Patch Notes Changelog archive +* 1.5.5 + * Logic: Fixed an issue where PoD Bridge lead to Arena Main instead of Arena Landing Area. (Potentially unnecessarily requiring bombs or Somaria to progress) + * HUD: Key counters are correct even when door shuffle is off * 1.5.4 * Documentation: New AI-assisted documentation [Site](https://aerinon.github.io/ALttPDoorRandomizer) * Generation Error: Fixed Issue with Shop Code and Take Any Caves (thanks Codemann for assistance) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 86d2c0f2..0a098615 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,7 +1,6 @@ # Patch Notes -* 1.5.5 - * Logic: Fixed an issue where PoD Bridge lead to Arena Main instead of Arena Landing Area. (Potentially unnecessarily requiring bombs or Somaria to progress) - * HUD: Key counters are correct even when door shuffle is off +* 1.5.6 + * Multiworld: Fixed a generation crash when beemizer is active and pottery is enabled. Pot locations are now properly filled with same-player items when the MW limit is hit even when beemizer has replaced native pot items. From b2b775e243f41244eef11f8b8a54f3a10549d42f Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 11 Mar 2026 15:22:11 -0600 Subject: [PATCH 26/76] feat: item_pool_adjust added to customizer --- ItemList.py | 47 ++++++++++++++++++++++++-------- RELEASENOTES.md | 1 + docs/Customizer.md | 24 ++++++++++++++-- docs/customizer_example.yaml | 5 ++++ source/classes/CustomSettings.py | 5 ++++ 5 files changed, 69 insertions(+), 13 deletions(-) diff --git a/ItemList.py b/ItemList.py index 32da56cc..ecff7555 100644 --- a/ItemList.py +++ b/ItemList.py @@ -287,6 +287,42 @@ def generate_itempool(world, player): for _ in range(0, amt): pool.append('Rupees (20)') + if world.shopsanity[player] and not skip_pool_adjustments: + for shop in world.shops[player]: + if shop.region.name in shop_to_location_table: + for index, slot in enumerate(shop.inventory): + if slot: + item = slot['item'] + if shop.region.name == 'Capacity Upgrade' and world.difficulty[player] != 'normal': + pool.append('Rupees (20)') + else: + pool.append(item) + + if (world.customizer and world.customizer.get_item_pool_adjust() + and player in world.customizer.get_item_pool_adjust()): + diff = difficulties[world.difficulty[player]] + for item_name, delta in world.customizer.get_item_pool_adjust()[player].items(): + if not isinstance(delta, int): + continue + if delta > 0: + if item_name == 'Bottle (Random)': + for _ in range(delta): + pool.append(random.choice(diff.bottles)) + else: + pool.extend([item_name] * delta) + elif delta < 0: + remove_count = abs(delta) + if item_name == 'Bottle (Random)': + bottle_names = set(diff.bottles) + for _ in range(remove_count): + bottle = next((x for x in pool if x in bottle_names), None) + if bottle: + pool.remove(bottle) + else: + for _ in range(remove_count): + if item_name in pool: + pool.remove(item_name) + if world.logic[player] == 'hybridglitches' and world.pottery[player] not in ['none', 'cave']: # In HMG force swamp smalls in pots to allow getting out of swamp palace placed_items['Swamp Palace - Trench 1 Pot Key'] = 'Small Key (Swamp Palace)' @@ -329,17 +365,6 @@ def generate_itempool(world, player): world.get_location(location, player).event = True world.get_location(location, player).locked = True - if world.shopsanity[player] and not skip_pool_adjustments: - for shop in world.shops[player]: - if shop.region.name in shop_to_location_table: - for index, slot in enumerate(shop.inventory): - if slot: - item = slot['item'] - if shop.region.name == 'Capacity Upgrade' and world.difficulty[player] != 'normal': - pool.append('Rupees (20)') - else: - pool.append(item) - items = ItemFactory(pool, player) if world.shopsanity[player]: for potion in ['Green Potion', 'Blue Potion', 'Red Potion']: diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0a098615..af6ac06b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,5 +2,6 @@ * 1.5.6 * Multiworld: Fixed a generation crash when beemizer is active and pottery is enabled. Pot locations are now properly filled with same-player items when the MW limit is hit even when beemizer has replaced native pot items. + * Customizer: Added `item_pool_adjust` section to apply additive/subtractive deltas to the base item pool rather than replacing it entirely. diff --git a/docs/Customizer.md b/docs/Customizer.md index 2c76da96..b8b7af28 100644 --- a/docs/Customizer.md +++ b/docs/Customizer.md @@ -64,10 +64,30 @@ Then each player can have the entire item pool defined. The name of item should `Bottle (Random)` is supported to randomize bottle contents according to those allowed by difficulty. Pendants and crystals are supported here. -##### Caveat - +**Note**: `item_pool` is a **full replacement** of the base item pool. Every item the player will receive must be listed. If you only want to tweak the default pool, use `item_pool_adjust` instead. + +##### Caveat + Dungeon items amount can be increased (but not decreased as the minimum of each dungeon item is either pre-determined or calculated by door rando) if the type of dungeon item is not shuffled then it is attempted to be placed in the dungeon. Extra item beyond dungeon capacity not be confined to the dungeon. +### item_pool_adjust + +This must be defined by player. Each player number should be listed with the appropriate adjustments. + +Unlike `item_pool`, this section **adjusts** the base item pool generated by the randomizer settings rather than replacing it. Use positive values to add items and negative values to remove items. + +```yaml +item_pool_adjust: + 1: + Bottle (Random): 2 # add 2 extra random bottles + Rupees (300): 1 # add 1 extra rupee pack + Boss Heart Container: -3 # remove 3 heart containers from the base pool +``` + +`Bottle (Random)` follows the same bottle randomization rules as in `item_pool`. When removing bottles with `Bottle (Random): -N`, any bottle (regardless of contents) will be removed. + +Removals that exceed the number of that item currently in the pool are silently ignored. Item pool adjustments are applied after beemizer but before the standard-mode Link's Uncle weapon selection, so weapons added here are eligible for that placement. + ### placements This must be defined by player. Each player number should be listed with the appropriate placement list. diff --git a/docs/customizer_example.yaml b/docs/customizer_example.yaml index 82f3fb7b..17c6f90c 100644 --- a/docs/customizer_example.yaml +++ b/docs/customizer_example.yaml @@ -60,6 +60,11 @@ item_pool: Green Potion: 1 Blue Potion: 1 Red Potion: 1 +item_pool_adjust: + 1: + Bottle (Random): 2 # add 2 extra random bottles on top of the base pool + Magic Upgrade (1/2): 1 # add 1 extra half-magic + Boss Heart Container: -3 # remove 3 heart containers from the base pool placements: 1: Palace of Darkness - Big Chest: Hammer diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index a9ef34d9..80b25d44 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -202,6 +202,11 @@ class CustomSettings(object): return self.file_source['item_pool'] return None + def get_item_pool_adjust(self): + if 'item_pool_adjust' in self.file_source: + return self.file_source['item_pool_adjust'] + return None + def get_placements(self): if 'placements' in self.file_source: return self.file_source['placements'] From a4d7d632355ee453b7251a3bd85b065cf3617a9a Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 11 Mar 2026 15:29:09 -0600 Subject: [PATCH 27/76] fix: disable pikit drops due to vanilla steal failure bug --- RELEASENOTES.md | 1 + source/dungeon/EnemyList.py | 2 +- source/enemizer/SpriteSheets.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index af6ac06b..2aa01541 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,5 +3,6 @@ * 1.5.6 * Multiworld: Fixed a generation crash when beemizer is active and pottery is enabled. Pot locations are now properly filled with same-player items when the MW limit is hit even when beemizer has replaced native pot items. * Customizer: Added `item_pool_adjust` section to apply additive/subtractive deltas to the base item pool rather than replacing it entirely. + * Enemy Drops: Pikit are no longer eligible for dropped items due to a vanilla bug where a failed steal can overwrite the assigned drop. diff --git a/source/dungeon/EnemyList.py b/source/dungeon/EnemyList.py index 2ce45a26..896c17b1 100644 --- a/source/dungeon/EnemyList.py +++ b/source/dungeon/EnemyList.py @@ -449,7 +449,7 @@ def init_enemy_stats(): EnemySprite.Stalfos: EnemyStats(EnemySprite.Stalfos, False, True, 6, health=4, dmg=1), EnemySprite.GreenZirro: EnemyStats(EnemySprite.GreenZirro, False, False, 1, health=4, dmg=5, dmask=0x80), EnemySprite.BlueZirro: EnemyStats(EnemySprite.BlueZirro, False, False, 7, health=8, dmg=3, dmask=0x80), - EnemySprite.Pikit: EnemyStats(EnemySprite.Pikit, False, True, 2, health=12, dmg=5), + EnemySprite.Pikit: EnemyStats(EnemySprite.Pikit, False, False, 2, health=12, dmg=5), EnemySprite.OldMan: EnemyStats(EnemySprite.OldMan, True, dmg=0), EnemySprite.PipeDown: EnemyStats(EnemySprite.PipeDown, True, dmg=0), diff --git a/source/enemizer/SpriteSheets.py b/source/enemizer/SpriteSheets.py index 7de739f3..e2abaa7d 100644 --- a/source/enemizer/SpriteSheets.py +++ b/source/enemizer/SpriteSheets.py @@ -329,7 +329,7 @@ def init_sprite_requirements(): SpriteRequirement(EnemySprite.Stalfos).sub_group(0, 0x1f), SpriteRequirement(EnemySprite.GreenZirro).no_drop().sub_group(3, 0x1b).exclude(NoFlyingRooms), SpriteRequirement(EnemySprite.BlueZirro).no_drop().sub_group(3, 0x1b).exclude(NoFlyingRooms), - SpriteRequirement(EnemySprite.Pikit).sub_group(3, 0x1b), + SpriteRequirement(EnemySprite.Pikit).no_drop().sub_group(3, 0x1b), SpriteRequirement(EnemySprite.CrystalMaiden).affix(), SpriteRequirement(EnemySprite.OldMan).affix().sub_group(2, 0x1c), SpriteRequirement(EnemySprite.PipeDown).affix(), From 4da73127a224ec86e762c1f838d8d9c3f9735845 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 12 Mar 2026 12:59:58 -0600 Subject: [PATCH 28/76] fix: reworked spawn refills for standard + enemy drop --- RELEASENOTES.md | 7 ++++--- Rom.py | 30 +++++++++++++++--------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2aa01541..a43e0725 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,8 +1,9 @@ # Patch Notes * 1.5.6 - * Multiworld: Fixed a generation crash when beemizer is active and pottery is enabled. Pot locations are now properly filled with same-player items when the MW limit is hit even when beemizer has replaced native pot items. - * Customizer: Added `item_pool_adjust` section to apply additive/subtractive deltas to the base item pool rather than replacing it entirely. * Enemy Drops: Pikit are no longer eligible for dropped items due to a vanilla bug where a failed steal can overwrite the assigned drop. - + * Standard Mode: Reworked spawn refills to be more generous for enemy drop modes. + * Customizer: Added `item_pool_adjust` section to apply additive/subtractive deltas to the base item pool rather than replacing it entirely. + * Multiworld: Fixed a generation crash when beemizer is active and pottery is enabled. Pot locations are now properly filled with same-player items when the MW limit is hit even when beemizer has replaced native pot items. + diff --git a/Rom.py b/Rom.py index 6d52e6c8..4a88b1f5 100644 --- a/Rom.py +++ b/Rom.py @@ -1348,38 +1348,38 @@ def patch_rom(world, rom, player, team, is_mystery=False): rom.write_bytes(0x180188, [0, 0, 0]) # Zelda respawn refills (magic, bombs, arrows) rom.write_bytes(0x18018B, [0, 0, 0]) # Mantle respawn refills (magic, bombs, arrows) bow_max, bomb_max, magic_max = 0, 0, 0 - bow_small, magic_small = 0, 0 + bow_small, bomb_small, magic_small = 10, 3, 0x20 if world.mode[player] == 'standard': if uncle_location.item is not None and uncle_location.item.name in ['Bow', 'Progressive Bow']: rom.write_byte(0x18004E, 1) # Escape Fill (arrows) write_int16(rom, 0x180183, 300) # Escape fill rupee bow rom.write_bytes(0x180185, [0, 0, 70]) # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [0, 0, 10]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [0, 0, 10]) # Mantle respawn refills (magic, bombs, arrows) - bow_max, bow_small = 70, 10 + rom.write_bytes(0x180188, [0, 0, 70]) # Zelda respawn refills (magic, bombs, arrows) + rom.write_bytes(0x18018B, [0, 0, 70]) # Mantle respawn refills (magic, bombs, arrows) + bow_max = 70 elif uncle_location.item is not None and uncle_location.item.name in ['Bomb Upgrade (+10)' if world.bombbag[player] else 'Bombs (10)']: rom.write_byte(0x18004E, 2) # Escape Fill (bombs) rom.write_bytes(0x180185, [0, 50, 0]) # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [0, 3, 0]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [0, 3, 0]) # Mantle respawn refills (magic, bombs, arrows) + rom.write_bytes(0x180188, [0, 50, 0]) # Zelda respawn refills (magic, bombs, arrows) + rom.write_bytes(0x18018B, [0, 50, 0]) # Mantle respawn refills (magic, bombs, arrows) bomb_max = 50 elif uncle_location.item is not None and uncle_location.item.name in ['Cane of Somaria', 'Cane of Byrna', 'Fire Rod']: rom.write_byte(0x18004E, 4) # Escape Fill (magic) rom.write_bytes(0x180185, [0x80, 0, 0]) # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [0x20, 0, 0]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [0x20, 0, 0]) # Mantle respawn refills (magic, bombs, arrows) - magic_max, magic_small = 0x80, 0x20 + rom.write_bytes(0x180188, [0x80, 0, 0]) # Zelda respawn refills (magic, bombs, arrows) + rom.write_bytes(0x18018B, [0x80, 0, 0]) # Mantle respawn refills (magic, bombs, arrows) + magic_max = 0x80 if world.doorShuffle[player] not in ['vanilla', 'basic']: # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180185, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)]) + rom.write_bytes(0x180185, [max(magic_small, magic_max), max(bomb_small, bomb_max), max(bow_small, bow_max)]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)]) + rom.write_bytes(0x180188, [max(magic_small, magic_max), max(bomb_small, bomb_max), max(bow_small, bow_max)]) # Mantle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)]) + rom.write_bytes(0x18018B, [max(magic_small, magic_max), max(bomb_small, bomb_max), max(bow_small, bow_max)]) elif world.doorShuffle[player] == 'basic': # just in case a bomb is needed to get to a chest - rom.write_bytes(0x180185, [max(0x00, magic_max), max(3, bomb_max), max(0, bow_max)]) - rom.write_bytes(0x180188, [magic_small, 3, bow_small]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [magic_small, 3, bow_small]) # Mantle respawn refills (magic, bombs, arrows) + rom.write_bytes(0x180185, [max(magic_small, magic_max), max(bomb_small, bomb_max), max(bow_small, bow_max)]) + rom.write_bytes(0x180188, [magic_small, max(bomb_small, bomb_max), bow_small]) # Zelda respawn refills (magic, bombs, arrows) + rom.write_bytes(0x18018B, [magic_small, max(bomb_small, bomb_max), bow_small]) # Mantle respawn refills (magic, bombs, arrows) # patch swamp: Need to enable permanent drain of water as dam or swamp were moved rom.write_byte(0x18003D, 0x01 if world.swamp_patch_required[player] else 0x00) From b45ebdc2ab33a66c1ae139ea3055539038e4eee2 Mon Sep 17 00:00:00 2001 From: theclearmouse <105736589+theclearmouse@users.noreply.github.com> Date: Mon, 26 Jan 2026 18:54:01 -0500 Subject: [PATCH 29/76] add crosskeys winners --- Rom.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Rom.py b/Rom.py index b5cce11a..2fd16ffa 100644 --- a/Rom.py +++ b/Rom.py @@ -2244,6 +2244,9 @@ def write_strings(rom, world, player, team): " Crosskeys\n" " Tournament\n" " Winners\n{HARP}\n" + " ~~~2025~~~\n humbugh\n\n" + " ~~~2024~~~\n Gammachuu\n\n" + " ~~~2023~~~\n WallKicks\n\n" " ~~~2022~~~\n Schulzer\n\n" " ~~~2021~~~\n Goomba\n\n" " ~~~2020~~~\n Linlinlin\n\n" From 3547aef480784c6df3a1d658e22db408d4f03435 Mon Sep 17 00:00:00 2001 From: Esme Date: Sat, 2 Nov 2024 19:27:06 +0000 Subject: [PATCH 30/76] Account for failure to determine locale. --- source/classes/BabelFish.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/classes/BabelFish.py b/source/classes/BabelFish.py index 40b6463f..58d26670 100644 --- a/source/classes/BabelFish.py +++ b/source/classes/BabelFish.py @@ -5,6 +5,8 @@ import os class BabelFish(): def __init__(self,subpath=["resources","app","meta"],lang=None): localization_string = locale.getdefaultlocale()[0] #get set localization + if localization_string is None: + localization_string = "en" self.locale = localization_string[:2] if lang is None else lang #let caller override localization self.langs = ["en"] #start with English if(not self.locale == "en"): #add localization From 0dbae8e21ec46b12ac46b81a88c3f65dbacb9e37 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 15 Mar 2026 16:32:54 -0500 Subject: [PATCH 31/76] Fixed bug/oversight with vanilla_fill and prize_shuffle --- Fill.py | 17 +++++++++++++++-- source/item/FillUtil.py | 3 --- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Fill.py b/Fill.py index 83d7a737..04ab18ee 100644 --- a/Fill.py +++ b/Fill.py @@ -111,7 +111,7 @@ def fill_dungeons_restrictive(world, shuffled_locations): for attempt in range(15): try: for player in range(1, world.players + 1): - if world.prizeshuffle[player] == 'nearby': + if world.prizeshuffle[player] == 'nearby' and world.algorithm != 'vanilla_fill': dungeon_pool = [] for dungeon in world.dungeons: from Dungeons import dungeon_table @@ -143,6 +143,14 @@ def fill_dungeons_restrictive(world, shuffled_locations): else: raise FillError(f'Unable to place dungeon prizes: {", ".join(list(map(lambda d: d.name, prizes)))}') + if world.algorithm == 'vanilla_fill': + for prize in prizes_copy: + if prize.is_near_dungeon_item(world): + if prize.location and prize.location.parent_region.dungeon: + dungeon = prize.location.parent_region.dungeon + dungeon.prize = prize + prize.dungeon_object = dungeon + random.shuffle(shuffled_locations) fill(all_state_base, others, shuffled_locations) @@ -234,7 +242,8 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl test_state.sweep_for_events() if location.can_fill(test_state, item_to_place, perform_access_check): if valid_key_placement(item_to_place, location, key_pool, test_state, world): - if (item_to_place.prize and world.prizeshuffle[item_to_place.player] == 'none') \ + if (item_to_place.prize and (world.prizeshuffle[item_to_place.player] == 'none' \ + 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: @@ -330,6 +339,10 @@ def track_dungeon_items(item, location, world): if item.prize: location.parent_region.dungeon.prize = item item.dungeon_object = location.parent_region.dungeon + elif world.algorithm == 'vanilla_fill' and item.prize and location.parent_region.dungeon: + dungeon = location.parent_region.dungeon + dungeon.prize = item + item.dungeon_object = dungeon def is_dungeon_item(item, world): diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 25bebbcd..6f1016fa 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -120,9 +120,6 @@ def create_item_pool_config(world): LocationGroup('bkgt').locs(mode_grouping['GT Trash'])] for loc_name in mode_grouping['Big Chests'] + mode_grouping['Heart Containers']: config.reserved_locations[player].add(loc_name) - if world.prizeshuffle[player] != 'none': - for loc_name in mode_grouping['Prizes']: - config.reserved_locations[player].add(loc_name) elif world.algorithm == 'major_only': config.location_groups = [ LocationGroup('MajorItems'), From 3ce66a4952f98f62ef02e901d7de8b06317b37ed Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 15 Mar 2026 16:46:07 -0500 Subject: [PATCH 32/76] Changed big bomb shop vendor text to be follower agnostic --- Rom.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Rom.py b/Rom.py index 2fd16ffa..ac28fbc7 100644 --- a/Rom.py +++ b/Rom.py @@ -2579,16 +2579,19 @@ def write_strings(rom, world, player, team): loc.item = i return loc (crystal5, crystal6, greenpendant) = tuple([x[0] if x else missing_prize() for x in [crystal5, crystal6, greenpendant]]) + bigbomb_follower = 'Big Bomb?\n' + if world.shuffle_followers[player]: + bigbomb_follower = '' if world.prizeshuffle[player] in ['none', 'dungeon']: (crystal5, crystal6, greenpendant) = tuple([x.parent_region.dungeon.name for x in [crystal5, crystal6, greenpendant]]) - tt['bomb_shop'] = 'Big Bomb?\nMy supply is blocked until you clear %s and %s.' % (crystal5, crystal6) + tt['bomb_shop'] = f'{bigbomb_follower}My supply is blocked until you clear %s and %s.' % (crystal5, crystal6) tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant elif world.prizeshuffle[player] == 'nearby': (crystal5, crystal6, greenpendant) = tuple([x.item.dungeon_object.name for x in [crystal5, crystal6, greenpendant]]) - tt['bomb_shop'] = 'Big Bomb?\nThe crystals can be found near %s and %s.' % (crystal5, crystal6) + tt['bomb_shop'] = f'{bigbomb_follower}The crystals can be found near %s and %s.' % (crystal5, crystal6) tt['sahasrahla_bring_courage'] = 'I lost my family heirloom near %s' % greenpendant else: - tt['bomb_shop'] = 'Big Bomb?\nThe crystals can be found %s and %s.' % (crystal5.hint_text, crystal6.hint_text) + tt['bomb_shop'] = f'{bigbomb_follower}The crystals can be found %s and %s.' % (crystal5.hint_text, crystal6.hint_text) tt['sahasrahla_bring_courage'] = 'My family heirloom can be found %s' % greenpendant.hint_text tt['sign_ganons_tower'] = ('You need %d crystal to enter.' if world.crystals_needed_for_gt[player] == 1 else 'You need %d crystals to enter.') % world.crystals_needed_for_gt[player] From a3adb1242225cf53356b9d1b88b202063075d84c Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 15 Mar 2026 17:01:50 -0500 Subject: [PATCH 33/76] Various baserom fixes - Fix buffer sword slash when dashing into water - Fixed bug with MSU-1 GT2 track not falling back to GT track - Fix for Kiki unfollowing after certain entrance transition conditions --- Rom.py | 2 +- data/base2current.bps | Bin 139245 -> 139250 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index ac28fbc7..bb4ce2ff 100644 --- a/Rom.py +++ b/Rom.py @@ -43,7 +43,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '69578b6039ebd80d4213823127b29194' +RANDOMIZERBASEHASH = '767da6cf86c6e7163ae59b2cd8a7305d' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index fce5484c234236fa1b7abf99a88d797868d56b26..bb2486998bbbd0c9bc7e0e28890a7e61e9a9aaf4 100644 GIT binary patch delta 239 zcmVRgfvk-prL$Qic4+}kv(Sx+K@Rb2rA_b|$pY{br4Y#vlVQ&+2F~h* z2C3eYlh1ttQj-zTA^|^>G|=t|DtRgfvk-ppR-vcc4+}lv(Sx+K?vDvrA_b?$pVvS&nyPW>V*cW*pr#h zeE~+37tkUBSCcx>?g>L8fLtxgQ=2xkG11*f0V9_W8UYsp7q=%G0b+9o1OS1TcW}3l zjR8Fl0SK4Pn*k~Udvmw>n*oXh1#U>#n_stxpaGjN4XR5a-;)yqxqpB}f4G+~&;cz0 TR<~Wy0gX=xew+D{|Emq7Fy&et From a663ab892d67e3b1b286bbdec519be60eda7904a Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 15 Mar 2026 17:08:18 -0500 Subject: [PATCH 34/76] Version bump 0.7.0.3 --- CHANGELOG.md | 12 ++++++++++++ OverworldShuffle.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fe8b5e9..e30bdb7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +# 0.7.0.3 +- Further updates and new yamls for Grid OW Shuffle +- Further customizer options for Grid OW Shuffle like defining screens that should stay together +- Fix buffer sword slash when dashing into water +- Fixed bug with MSU-1 GT2 track not falling back to GT track +- Fix for Kiki unfollowing after certain entrance transition conditions +- Fixed bug/oversight with vanilla_fill and prize_shuffle +- Changed big bomb shop vendor text to be follower agnostic +- Updated crosskeys winners (thanks clearmouse) +- Fixed bug with potential locale determination (thanks Esme) +- \~Merged in DR v1.5.6~ + # 0.7.0.2 - Fixed money error for bombbag/take-anys - Fixed palette issue for map check/flute menu diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 6a9dd637..f3079f52 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -9,7 +9,7 @@ from OWEdges import OWTileRegions, OWEdgeGroups, OWEdgeGroupsTerrain, OWExitType from OverworldGlitchRules import create_owg_connections from Utils import bidict -version_number = '0.7.0.2' +version_number = '0.7.0.3' # branch indicator is intentionally different across branches version_branch = '-u' From f421bba9f33c2e3800d925c305dc986c9bf7a392 Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Tue, 14 Apr 2026 10:20:53 -0500 Subject: [PATCH 35/76] Fix issue with make_custom_pool --- ItemList.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ItemList.py b/ItemList.py index cf76538f..db86e178 100644 --- a/ItemList.py +++ b/ItemList.py @@ -1507,7 +1507,7 @@ def make_customizer_pool(world, player): elif timer == 'ohko': clock_mode = 'ohko' - if goal in ['sanctuary']: + if world.goal[player] in ['sanctuary']: place_item('Sanctuary', 'Triforce') if world.custom_goals[player]['pedgoal'] and 'requirements' in world.custom_goals[player]['pedgoal'] and world.custom_goals[player]['pedgoal']['requirements'][0]['condition'] == 0x00: place_item('Master Sword Pedestal', 'Nothing') From 3cb1e9d77b1ca005ccfc770ab8c6a72b4f1c6e36 Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Tue, 14 Apr 2026 10:20:53 -0500 Subject: [PATCH 36/76] Fix issue with make_custom_pool --- ItemList.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ItemList.py b/ItemList.py index d89e724b..7ccead35 100644 --- a/ItemList.py +++ b/ItemList.py @@ -1533,7 +1533,7 @@ def make_customizer_pool(world, player): elif timer == 'ohko': clock_mode = 'ohko' - if goal in ['sanctuary']: + if world.goal[player] in ['sanctuary']: place_item('Sanctuary', 'Triforce') if world.custom_goals[player]['pedgoal'] and 'requirements' in world.custom_goals[player]['pedgoal'] and world.custom_goals[player]['pedgoal']['requirements'][0]['condition'] == 0x00: place_item('Master Sword Pedestal', 'Nothing') From d64a58f636601a49f7c2e03c37ebfbfa12a4b6cc Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Sun, 3 May 2026 17:05:33 -0500 Subject: [PATCH 37/76] extra_keys setting for crossed door rando --- BaseClasses.py | 4 ++++ CLI.py | 5 +++-- DoorShuffle.py | 12 ++++++++++-- Main.py | 3 +++ Rom.py | 8 +++++--- data/base2current.bps | Bin 156429 -> 156474 bytes resources/app/cli/args.json | 3 +++ resources/app/cli/lang/en.json | 1 + 8 files changed, 29 insertions(+), 7 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 55bcd826..034bcd60 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -202,6 +202,7 @@ class World(object): set_player_attr('damage_challenge', 'normal') set_player_attr('shuffle_damage_table', 'vanilla') set_player_attr('crystal_book', False) + set_player_attr('extra_keys', 0) set_player_attr('collection_rate', False) set_player_attr('colorizepots', True) set_player_attr('pot_pool', {}) @@ -1978,6 +1979,7 @@ class Dungeon(object): self.prize = None self.big_key = big_key self.small_keys = small_keys + self.extra_small_keys = 0 self.dungeon_items = dungeon_items self.bosses = dict() self.player = player @@ -3174,6 +3176,7 @@ class Spoiler(object): 'damage_challenge': self.world.damage_challenge, 'shuffle_damage_table': self.world.shuffle_damage_table, 'crystal_book': self.world.crystal_book, + 'extra_keys': self.world.extra_keys, 'triforcegoal': self.world.treasure_hunt_count, 'triforcepool': self.world.treasure_hunt_total, 'race': self.world.settings.world_rep['meta']['race'], @@ -3453,6 +3456,7 @@ class Spoiler(object): outfile.write('Damage Challenge:'.ljust(line_width) + '%s\n' % self.metadata['damage_challenge'][player]) outfile.write('Damage Table Randomization:'.ljust(line_width) + '%s\n' % self.metadata['shuffle_damage_table'][player]) outfile.write('Crystal Book:'.ljust(line_width) + '%s\n' % yn(self.metadata['crystal_book'][player])) + outfile.write('Extra Keys:'.ljust(line_width) + '%d%%\n' % self.metadata['extra_keys'][player]) outfile.write('Hints:'.ljust(line_width) + '%s\n' % yn(self.metadata['hints'][player])) outfile.write('Race:'.ljust(line_width) + '%s\n' % yn(self.world.settings.world_rep['meta']['race'])) diff --git a/CLI.py b/CLI.py index 8a1bcc70..6b9f416d 100644 --- a/CLI.py +++ b/CLI.py @@ -163,8 +163,8 @@ def parse_cli(argv, no_defaults=False): 'shuffletavern', 'skullwoods', 'linked_drops', 'pseudoboots', 'mirrorscroll', 'dark_rooms', 'damage_challenge', 'shuffle_damage_table', - 'crystal_book', 'retro', 'accessibility', 'hints', - 'beemizer', 'experimental', 'dungeon_counters', + 'crystal_book', 'extra_keys', 'retro', 'accessibility', + 'hints', 'beemizer', 'experimental', 'dungeon_counters', 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', 'ow_palettes', 'uw_palettes', 'sprite', 'triforce_gfx', 'disablemusic', @@ -254,6 +254,7 @@ def parse_settings(): "damage_challenge": "normal", "shuffle_damage_table": "vanilla", "crystal_book": False, + "extra_keys": 0, "shuffleenemies": "none", "shufflebosses": "none", diff --git a/DoorShuffle.py b/DoorShuffle.py index ab9bb32e..c84d2a3c 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -3,6 +3,7 @@ import time from collections import defaultdict, deque from enum import Flag, unique from itertools import chain +from math import ceil from typing import DefaultDict, Dict, List import RaceRandom as random @@ -1599,7 +1600,11 @@ def assign_cross_keys(dungeon_builders, world, player): if actual_chest_keys == 0: dungeon.small_keys = [] else: - dungeon.small_keys = [ItemFactory(dungeon_keys[name], player)] * actual_chest_keys + extra_keys = ceil(actual_chest_keys * world.extra_keys[player] / 100) + logger.debug(f'Adding {extra_keys} extra small keys to {name}') + dungeon.extra_small_keys = extra_keys + created_keys = actual_chest_keys + extra_keys + dungeon.small_keys = [ItemFactory(dungeon_keys[name], player)] * created_keys logger.info(f'{world.fish.translate("cli", "cli", "keydoor.shuffle.time.crossed")}: {time.process_time()-start}') @@ -2171,7 +2176,10 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, all_ if actual_chest_keys == 0: dungeon.small_keys = [] else: - dungeon.small_keys = [ItemFactory(dungeon_keys[dungeon_name], player) for _ in range(actual_chest_keys)] + extra_keys = ceil(actual_chest_keys * world.extra_keys[player] / 100) + dungeon.extra_small_keys = extra_keys + created_keys = actual_chest_keys + extra_keys + dungeon.small_keys = [ItemFactory(dungeon_keys[dungeon_name], player) for _ in range(created_keys)] for name, small_list in small_map.items(): used_doors.update(flatten_pair_list(small_list)) diff --git a/Main.py b/Main.py index ba3f7016..c7fabc3e 100644 --- a/Main.py +++ b/Main.py @@ -564,6 +564,7 @@ def init_world(args, fish): world.damage_challenge = args.damage_challenge.copy() world.shuffle_damage_table = args.shuffle_damage_table.copy() world.crystal_book = args.crystal_book.copy() + world.extra_keys = {player: int(args.extra_keys[player]) for player in range(1, world.players + 1)} world.overworld_map = args.overworld_map.copy() world.take_any = args.take_any.copy() world.restrict_boss_items = args.restrict_boss_items.copy() @@ -868,6 +869,7 @@ def copy_world(world): ret.damage_challenge = world.damage_challenge.copy() ret.shuffle_damage_table = world.shuffle_damage_table.copy() ret.crystal_book = world.crystal_book.copy() + ret.extra_keys = world.extra_keys.copy() ret.overworld_map = world.overworld_map.copy() ret.take_any = world.take_any.copy() ret.boss_shuffle = world.boss_shuffle.copy() @@ -1100,6 +1102,7 @@ def copy_world_premature(world, player, create_flute_exits=True): ret.damage_challenge = world.damage_challenge.copy() ret.shuffle_damage_table = world.shuffle_damage_table.copy() ret.crystal_book = world.crystal_book.copy() + ret.extra_keys = world.extra_keys.copy() ret.overworld_map = world.overworld_map.copy() ret.take_any = world.take_any.copy() ret.boss_shuffle = world.boss_shuffle.copy() diff --git a/Rom.py b/Rom.py index 00542973..3f536a07 100644 --- a/Rom.py +++ b/Rom.py @@ -85,7 +85,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc from Versions import DRVersion, GKVersion, ORVersion JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '8945e9fdcefc02eb3ff3ad2a8892a180' +RANDOMIZERBASEHASH = '8a6d769751e2676e8d9da48871cb7634' class JsonRom(object): @@ -818,11 +818,13 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): rom.write_byte(0x138002, 2) for name, layout in world.key_layout[player].items(): offset = compass_data[name][4]//2 + dungeon = world.get_dungeon(name, player) if world.keyshuffle[player] == 'universal': rom.write_byte(0x187010+offset, layout.max_chests + layout.max_drops) else: - rom.write_byte(0x13f020+offset, layout.max_chests + layout.max_drops) # not currently used - rom.write_byte(0x187010+offset, layout.max_chests) + rom.write_byte(0x13f020+offset, layout.max_chests + layout.max_drops + dungeon.extra_small_keys) # not currently used + rom.write_byte(0x187010+offset, layout.max_chests + dungeon.extra_small_keys) + rom.write_byte(0x187000+offset, dungeon.extra_small_keys) builder = world.dungeon_layouts[player][name] bk_status = 1 if builder.bk_required else 0 bk_status = 2 if builder.bk_provided else bk_status diff --git a/data/base2current.bps b/data/base2current.bps index 02dcd32fb7d01fbec2d8deb41e1a833c079ca6bf..a43acf16a839eaa8c9ccfe5a64850707065b6f04 100644 GIT binary patch delta 867 zcmW-de`phT7{>4YCYhI{O;e09Ri{^*m_+L+6567^tuA_sI6}ohPy${dl zfrs~rE$Z(s>g7us?Pq;QZt+n~yKXuq?!3*HG+iL@wDwK<*R<%0CwHzE#QY85|5A-hpe_#w=hf;j2ZlU$=q+J2I7 zF<{&mlcMe1Oj7)mIV7aIt@rQe{8!W`F*Davty*; zy#(8kIeYq2Onr*^Lh=z)91a+|uVy-Ms`l5n#2*KXOLu6Qf4zS>!Yq;E1%7YeGONiL z`j^YQ9u>_;&w1D6mEckvV@{E4aoDz{lej*t4nHdXLBu?>RjA3!z5MQRe(M1FCJqi- zCx}&l7QI5&;^2Xoh=+sbu9GjStbc}M&HS~8{@NgA1C?kf*HH}Kxs!`SNb=bSD-xyZpYT26Q#lRF%=xJFZ=q^^ngrc|@^7vG8_w6wxq z2v?e=j*rqQNry!@Qm>Bch&BNSj3^@;nVV_3lbI)j32-*dr{yzQ(+MjMe`U!!Mo26H zO>NsotT=g|3FiK>+54pJ687bM>&V25Skv=GtH z8u+)k$)lS{WUAb*1c`nETK1q!Ro2W>!#RNAKo{lvEuF6qv9FS+hlG!*%0a2BifS|2 zcU<7#C4&=iW`l8jx0WSFj+a4g)&jAy>Sp8#xWi122kiFrj^FssjG7X}T>l)T4}ap}P8}x80@kT!f-4 iPm0skJGHs<#nnZ2E~8Z@`_S1<^q-N31OLA8xbHv6V{7aH delta 899 zcmW-eZERCz6vzAAc5v(H*11y3ByhVI)(&bmgtFm+CBZSfG9u#x4if7KhEkQ$B_^7s zdv0GAE`7OOFWuAI>beceUF*xeTCwOtN+w&GiOKz7V!{WbB={1(+?o+hoy@CW&N;tx z&i|j2oUd2ZkrlOYMX5Ud1~b9c8P$b>9t@ziFz9UlfZZ6dDmT4afW{=VJQ z>4T7!55I+SM(!q+Bldn14t-V+^YkIeymF$#G!IrN;vOnZGM60{Qkfn+URmA!prk!D z(Y+z8c^`HXKPJ&)6zpi)iA?WT-49CNAudn+gXHq6o$2r~ub)JfC}_ifMO@XH>9eDq zG}sT+Xov>JeWoK4>AX&pTIK`FY2Jw2Lw9NnuI+73gDci5HxyERdWPIa`zXE1NxNW; z4}C#{=T&ES+?_?g(4fWi63g+@2DdybY56}GyN|;8HF`2A8u_8|6wB8@&WuUZFF%XA zVxYfPObNBbdnuup2%$g>v`>Um!f2*rL=S`abeSV>=^?v`@aC3vnfUPStSzIG;Wu5z zhq~oaA|TUWx{uDw>;G#=JdU@)OgLyz^ML{msoeU9rLl=dxY#5IYVSfO{#r8(bwSqX z%~=*?8xI)?7`<+E>Fp$?&OXCzxk$%PZAH}Vs_o%B;ej1HkRFDI8y> z@)|#R8ugq>&MCwbd{yEkBN`k|G7Ts&1x7ay`8!l3x>Wpo3M@Bb7l+YO7Q6_?i%+v4 z?8nAmKw&f10xlETUIo~$1L$@) zb`*1~i%+|;=;pe>3c UE0&I9V_R|d^SJ};4^Ni<1=65`UjP6A diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 425fcb18..ae48628f 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -665,6 +665,9 @@ "action": "store_true", "type": "bool" }, + "extra_keys": { + "type": "int" + }, "calc_playthrough": { "action": "store_false", "type": "bool" diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index c63ef213..fdfd4a4b 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -425,6 +425,7 @@ "AlwaysInLogic: Dark rooms are always considered to be in logic, even if the player cannot see" ], "crystal_book": [ " Book can be used indoors to flip the state of colored pegs (default: %(default)s)"], + "extra_keys": [ " Percentage of extra small keys to create for each dungeon when door shuffle is enabled (default: %(default)s)"], "bombbag": ["Start with 0 bomb capacity. Two capacity upgrades (+10) are added to the pool (default: %(default)s)" ], "any_enemy_logic": [ "How to handle potential traversal between dungeon in Crossed door shuffle", From b3afa576779619fcf0c6155456830beb5012b8de Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Mon, 4 May 2026 00:14:28 -0500 Subject: [PATCH 38/76] Show level of current supertile loot in hud --- Rom.py | 2 +- data/base2current.bps | Bin 156474 -> 157416 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index 3f536a07..01b29498 100644 --- a/Rom.py +++ b/Rom.py @@ -85,7 +85,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc from Versions import DRVersion, GKVersion, ORVersion JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '8a6d769751e2676e8d9da48871cb7634' +RANDOMIZERBASEHASH = 'a9cbc48908d21ad0d354199251efbae8' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index a43acf16a839eaa8c9ccfe5a64850707065b6f04..230a64537f16b9d3dedad0440615ccfba7eb243d 100644 GIT binary patch delta 9134 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 delta 8198 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 DTuRQM From 4de84df7a8393531879efbae9768151ead295048 Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Mon, 4 May 2026 00:38:48 -0500 Subject: [PATCH 39/76] Update baserom --- Rom.py | 2 +- data/base2current.bps | Bin 157416 -> 157417 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index 01b29498..e52b2aec 100644 --- a/Rom.py +++ b/Rom.py @@ -85,7 +85,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc from Versions import DRVersion, GKVersion, ORVersion JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'a9cbc48908d21ad0d354199251efbae8' +RANDOMIZERBASEHASH = 'c4ba2f29976344e33ca1b5901b3073bb' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 230a64537f16b9d3dedad0440615ccfba7eb243d..8459f435790c999b316a45c82ce53efff61c6941 100644 GIT binary patch delta 61 zcmV-D0K)(1%?atv39yI*1M}JpgNp-)ivt0-ivt1*S_!u=0j^7jHwB2do?QY10Rt#o TowuW20zC)_6Rp_ehHf!y;8+%j delta 60 zcmV-C0K@<3%?aqu39yI*1N7PpgNp-)ivt0-ivt1*S_!HyYn@kzPY1WRo?QY10RkP5 Sx1?SIJqQT1wUgDdX0aIGh! Date: Mon, 4 May 2026 08:53:42 -0500 Subject: [PATCH 40/76] loothud option --- BaseClasses.py | 3 +++ CLI.py | 52 +++++++++++++++++++----------------- Main.py | 3 +++ Rom.py | 14 +++++++++- data/base2current.bps | Bin 157417 -> 157443 bytes resources/app/cli/args.json | 8 ++++++ 6 files changed, 54 insertions(+), 26 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 034bcd60..fe0dce2a 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -164,6 +164,7 @@ class World(object): set_player_attr('bigkeyshuffle', 'none') set_player_attr('prizeshuffle', 'none') set_player_attr('showloot', 'never') + set_player_attr('loothud', 'never') set_player_attr('showmap', 'map') set_player_attr('restrict_boss_items', 'none') set_player_attr('bombbag', False) @@ -3156,6 +3157,7 @@ class Spoiler(object): 'bigkeyshuffle': self.world.bigkeyshuffle, 'prizeshuffle': self.world.prizeshuffle, 'showloot': self.world.showloot, + 'loothud': self.world.loothud, 'showmap': self.world.showmap, 'boss_shuffle': self.world.boss_shuffle, 'enemy_shuffle': self.world.enemy_shuffle, @@ -3430,6 +3432,7 @@ class Spoiler(object): outfile.write('Big Key Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['bigkeyshuffle'][player]) outfile.write('Prize Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['prizeshuffle'][player]) outfile.write('Show Value of Checks:'.ljust(line_width) + '%s\n' % self.metadata['showloot'][player]) + outfile.write('Show Value of Checks on HUD:'.ljust(line_width) + '%s\n' % self.metadata['loothud'][player]) outfile.write('Show Map:'.ljust(line_width) + '%s\n' % self.metadata['showmap'][player]) outfile.write('Key Logic Algorithm:'.ljust(line_width) + '%s\n' % self.metadata['key_logic'][player]) outfile.write('\n') diff --git a/CLI.py b/CLI.py index 6b9f416d..8711f649 100644 --- a/CLI.py +++ b/CLI.py @@ -153,31 +153,32 @@ def parse_cli(argv, no_defaults=False): 'crystals_ganon', 'crystals_gt', 'bosses_ganon', 'bosshunt_include_agas', 'ganon_item', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', - 'bigkeyshuffle', 'prizeshuffle', 'showloot', 'showmap', - 'startinventory', 'usestartinventory', 'bombbag', - 'shuffleganon', 'overworld_map', 'restrict_boss_items', - 'triforce_max_difference', 'triforce_pool_min', - 'triforce_pool_max', 'triforce_goal_min', - 'triforce_goal_max', 'triforce_min_difference', - 'triforce_goal', 'triforce_pool', 'shufflelinks', - 'shuffletavern', 'skullwoods', 'linked_drops', - 'pseudoboots', 'mirrorscroll', 'dark_rooms', - 'damage_challenge', 'shuffle_damage_table', - 'crystal_book', 'extra_keys', 'retro', 'accessibility', - 'hints', 'beemizer', 'experimental', 'dungeon_counters', - 'shufflebosses', 'shuffleenemies', 'enemy_health', - 'enemy_damage', 'shufflepots', 'ow_palettes', - 'uw_palettes', 'sprite', 'triforce_gfx', 'disablemusic', - 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', - 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', - 'keydropshuffle', 'mixed_travel', - 'standardize_palettes', 'code', 'reduce_flashing', - 'shuffle_sfx', 'shuffle_sfxinstruments', - 'shuffle_songinstruments', 'msu_resume', - 'collection_rate', 'colorizepots', 'decoupledoors', - 'door_type_mode', 'bonk_drops', 'trap_door_mode', - 'key_logic_algorithm', 'door_self_loops', - 'any_enemy_logic', 'aga_randomness', 'money_balance']: + 'bigkeyshuffle', 'prizeshuffle', 'showloot', 'loothud', + 'showmap', 'startinventory', 'usestartinventory', + 'bombbag', 'shuffleganon', 'overworld_map', + 'restrict_boss_items', 'triforce_max_difference', + 'triforce_pool_min', 'triforce_pool_max', + 'triforce_goal_min', 'triforce_goal_max', + 'triforce_min_difference', 'triforce_goal', + 'triforce_pool', 'shufflelinks', 'shuffletavern', + 'skullwoods', 'linked_drops', 'pseudoboots', + 'mirrorscroll', 'dark_rooms', 'damage_challenge', + 'shuffle_damage_table', 'crystal_book', 'extra_keys', + 'retro', 'accessibility', 'hints', 'beemizer', + 'experimental', 'dungeon_counters', 'shufflebosses', + 'shuffleenemies', 'enemy_health', 'enemy_damage', + 'shufflepots', 'ow_palettes', 'uw_palettes', 'sprite', + 'triforce_gfx', 'disablemusic', 'quickswap', 'fastmenu', + 'heartcolor', 'heartbeep', 'remote_items', 'shopsanity', + 'dropshuffle', 'pottery', 'keydropshuffle', + 'mixed_travel', 'standardize_palettes', 'code', + 'reduce_flashing', 'shuffle_sfx', + 'shuffle_sfxinstruments', 'shuffle_songinstruments', + 'msu_resume', 'collection_rate', 'colorizepots', + 'decoupledoors', 'door_type_mode', 'bonk_drops', + 'trap_door_mode', 'key_logic_algorithm', + 'door_self_loops', 'any_enemy_logic', 'aga_randomness', + 'money_balance']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -274,6 +275,7 @@ def parse_settings(): "bigkeyshuffle": "none", "prizeshuffle": "none", "showloot": "never", + "loothud": "never", "showmap": "map", "keysanity": False, "door_shuffle": "vanilla", diff --git a/Main.py b/Main.py index c7fabc3e..f2c19eff 100644 --- a/Main.py +++ b/Main.py @@ -516,6 +516,7 @@ def init_world(args, fish): world.bigkeyshuffle = args.bigkeyshuffle.copy() world.prizeshuffle = args.prizeshuffle.copy() world.showloot = args.showloot.copy() + world.loothud = args.loothud.copy() world.showmap = args.showmap.copy() world.bombbag = args.bombbag.copy() world.flute_mode = args.flute_mode.copy() @@ -837,6 +838,7 @@ def copy_world(world): ret.bigkeyshuffle = world.bigkeyshuffle.copy() ret.prizeshuffle = world.prizeshuffle.copy() ret.showloot = world.showloot.copy() + ret.loothud = world.loothud.copy() ret.showmap = world.showmap.copy() ret.bombbag = world.bombbag.copy() ret.flute_mode = world.flute_mode.copy() @@ -1070,6 +1072,7 @@ def copy_world_premature(world, player, create_flute_exits=True): ret.bigkeyshuffle = world.bigkeyshuffle.copy() ret.prizeshuffle = world.prizeshuffle.copy() ret.showloot = world.showloot.copy() + ret.loothud = world.loothud.copy() ret.showmap = world.showmap.copy() ret.bombbag = world.bombbag.copy() ret.flute_mode = world.flute_mode.copy() diff --git a/Rom.py b/Rom.py index e52b2aec..39c09788 100644 --- a/Rom.py +++ b/Rom.py @@ -85,7 +85,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc from Versions import DRVersion, GKVersion, ORVersion JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'c4ba2f29976344e33ca1b5901b3073bb' +RANDOMIZERBASEHASH = '5fe97f04afd1880f281ec2c27cfabc17' class JsonRom(object): @@ -1516,6 +1516,18 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): rom.write_bytes(0x1CFF08, [0x02, 0x00, 0x00, 0x00]) rom.write_byte(0x1CFF11, 0x00) + if world.loothud[player] == 'never': + rom.write_byte(0x1CFF12, 0x00) + elif world.showloot[player] == 'presence': + rom.write_byte(0x1CFF12, 0x01) + rom.write_bytes(0x1CFF0E, [0x01, 0x01]) + elif world.showloot[player] == 'value': + rom.write_byte(0x1CFF12, 0x01) + rom.write_bytes(0x1CFF0E, [0xFF, 0xFF]) + elif world.showloot[player] == 'dungeon_value': + rom.write_byte(0x1CFF12, 0x01) + rom.write_bytes(0x1CFF0E, [0xFF, 0x01]) + if world.showmap[player] == 'visited': rom.write_bytes(0x1CFF00, [0x01, 0x00, 0x00, 0x05]) elif world.showmap[player] == 'map': diff --git a/data/base2current.bps b/data/base2current.bps index 8459f435790c999b316a45c82ce53efff61c6941..710f9438eed6cabb2496e9192ec554efda465fc4 100644 GIT binary patch delta 1274 zcmW-ee^8Tk9LJyMv(aD-n5ULFlet5UU*7zY1q>Vn3SH(De_+80IZ;GX>ooOp`#!^` z9FxX;qHoI8u3~O`7@LN*zNz;pg-V?kjyTVA8i@t1&yLywoi}x8x*$}Wfl6!owIgbi!an#=&H+8?%aI={ggpf_p&LMPdto9&+H3` z5;N$XXt=AKewm-qcj`dbj6ND&a2~=#MUcnV;zvc$&3=Pj#W0oKjlIP%C!^V)6K8nn zVaO&wF5HW+6@zJVX&()J67w;Bv$!4&@(jtsOAKIU7vdfRoO$kEkPOA+G36lRymToV zMfX2t7-?SU|IjqZ#MC&un$G#Jr&UQ0f&?Y|N2$}}4@)4Fvok=DT3SU_Db-VzOHBbn z_f0fl-O?l2(o}#5T)aqr$2&@4_T1udOqs%{s~N_9Hf|v$USs8T&p_OQnu{V@;_$Y+ z{-tpTK3xhW)^)wrjN17+Sjl?9$hme<^QzG9dV{i_GDx|B@;GIq(C+yhtPR?kj;qI# zLmO=UX^1mAZTv(-cZH3&ip~QrGwtJqD&%USri~76q2wuhDBq<#Mrjw?Ip6msh-1Rm z8f0hO6}_(@ZnAL8C9+mBRk-O2P(DSw?@(q^-lNnANt`Q}jz29pxP0%e-o0)8D%8ce zZH#rI`x%bAi{D0$JjC6{`N%N^aev^+s6%EY5Anf7IVQ}qxwg{!k>KFB$u3wBhr;;r z5;(6l1rjG!c^IXS7z2Xh{g3E%4T-ROMdR4WJ$%CmnaV23_TI8}h$rJ%UIvHRVT{XQ zv0~Uq_;e|Pe<}mx@>V}lzdRI=ZL}LB**O`a^tYdg-$kVnKhdwasuZR91D1&)Mt?t% z@GP2V*G4iDeR7fTj1>11B;>ofbiw9I5yOq;Py+1F*j)jo><#>D1_X6R3D8>Sy)pE^X5c?WOjP&c~9z)ymNv0Vx4C{3eiVSvz`o+BTRBA zlJH;P?Uj(rn(@&}$jRH}J?9yUGciNK6HH9+9B`(ccER!4Qj?%Rb!7S=bY@DM@t@9R z9JeL>pc2Xya*n~a@WSOVNtUERp*jg0Dt)8nL59-WKCGR8lF|{+BlAtVz{nw94`qsu7*{t z(y~j3j#_d$KWoHi)<8K-UyIqba5Ft+!37~TTZVWONYgfj$JRm(3z;>zsuo^@Cs*M> zEnI{7&A5F%Xkc1%*t;H-vPr355z)Q50P)71&I284X)Ye9gI2Z*>+9i#nbnzBcv1AK zG*{eZsB4mS>x1(1&QAXDUti9B^j1OGRSzS{3UmI|!^L?gaB~yvN&ay->67t7?Unxk Dj;J;m delta 1231 zcmW-ge@s(X6vy8?Ef#8l65JfRC?M!SaNw_L3s@Kx@D~+yx`iTBLYOFFTQ<>U?R&4j z#ll#5yvfydTa%62_O()H&0cOcea;LO7ZsOf0>i{OvmsLh^T#%}+1>7+^ZA}{&bjv_ z=U&H^ggciLP8?FHAB^gmTZt{os zuJ-qJWq$0(+u5#6pC9M3Lfta4si=dFb4(Skb5XgyA zJoHhxGJvYv(U=h|sTs$K=~t*v(VIpqGr&_A(3&D>q@PB8MR59stNnN|8lBV*P_E0D z$H$p_pHY;O68_(`3{aDLlvPFg0yh&Ym23T&i4WW&Dis|mhH%c75N7ns3KF%7iA1$h z6~g4c=?0X!_9(J7=51{G!ga_mJyyHEEhYxC&Av3KCg51 zSbTT~7fEDTlZ)f08{Fj_zf*D@6dFlgqgcrZO++-2#%981!al;?gvSW?h#jo|yJCi= zWP2^+q&(%pR~Y_rz?I4f5^cwSiuJ4zB4U~}Oc4Gz8C)xa#eQx1M2LY#jrn$8Jw`2}aG04TGFq!HZJL<BO=L|r1nz9c4?y6wf_40%nN;T@v8?G_=`HhgGipzI|cge_Xfh_%o znJmX{iUmt7)S&$qXjxFW3&!erF5R(Ax61 zGMSHTIo+QxLf5xK88mD~b6zR=#MMK<-1YMM-+SN8SnL^E}52@Yt&GI)dgI1MtU&fLrKcC@Eue%O*=@l&+ ztAo9C5?W`47w09XjqsA>tC=(6DP<0)*mvJAJMZe|dmewe=uyiOIcSAz@tVA~VvlW= Th#XDO9-n%A_~-Xp3tRpJsF@wT diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index ae48628f..d62965e6 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -486,6 +486,14 @@ "always" ] }, + "loothud": { + "choices": [ + "never", + "presence", + "value", + "dungeon_value" + ] + }, "showmap": { "choices": [ "visited", From 4d63168e9ac0b44d7a79f1ee694db2dfc4eedf9d Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Mon, 4 May 2026 17:54:09 -0500 Subject: [PATCH 41/76] Bugfix update baserom --- Rom.py | 2 +- data/base2current.bps | Bin 157443 -> 157460 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index 39c09788..b0cdb22a 100644 --- a/Rom.py +++ b/Rom.py @@ -85,7 +85,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc from Versions import DRVersion, GKVersion, ORVersion JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '5fe97f04afd1880f281ec2c27cfabc17' +RANDOMIZERBASEHASH = 'c5e123bcd2723114e1ecc8b42d0c5efa' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 710f9438eed6cabb2496e9192ec554efda465fc4..6c4f49c018f6950a6983d0a7664b7d7a666e4a43 100644 GIT binary patch delta 1950 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 Date: Mon, 11 May 2026 22:49:15 -0500 Subject: [PATCH 42/76] Update baserom, add hashes to meta --- BaseClasses.py | 1 + Rom.py | 29 +++++++++++++++-------------- data/base2current.bps | Bin 157460 -> 157463 bytes 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index fe0dce2a..8f4df302 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -3184,6 +3184,7 @@ class Spoiler(object): 'race': self.world.settings.world_rep['meta']['race'], 'user_notes': self.world.settings.world_rep['meta']['user_notes'], 'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)}, + 'hashes': [{p: self.hashes[p, t] for p in range(1, self.world.players + 1)} for t in range(self.world.teams)], 'seed': self.world.seed } diff --git a/Rom.py b/Rom.py index b0cdb22a..06903808 100644 --- a/Rom.py +++ b/Rom.py @@ -85,7 +85,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc from Versions import DRVersion, GKVersion, ORVersion JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'c5e123bcd2723114e1ecc8b42d0c5efa' +RANDOMIZERBASEHASH = 'beae4c06c4841030709639215e2b03c3' class JsonRom(object): @@ -1503,19 +1503,6 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): loot_source |= 0x04 rom.write_byte(0x1CFF10, loot_source) - if world.showloot[player] == 'never': - rom.write_bytes(0x1CFF08, [0x00, 0x00, 0x00, 0x00]) - rom.write_byte(0x1CFF11, 0x00) - elif world.showloot[player] == 'presence': - rom.write_bytes(0x1CFF08, [0x01, 0x00, 0x00, 0x00]) - rom.write_byte(0x1CFF11, 0x00) - elif world.showloot[player] == 'compass': - rom.write_bytes(0x1CFF08, [0x01, 0x00, 0x02, 0x00]) - rom.write_byte(0x1CFF11, 0x01) - elif world.showloot[player] == 'always': - rom.write_bytes(0x1CFF08, [0x02, 0x00, 0x00, 0x00]) - rom.write_byte(0x1CFF11, 0x00) - if world.loothud[player] == 'never': rom.write_byte(0x1CFF12, 0x00) elif world.showloot[player] == 'presence': @@ -1528,6 +1515,20 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): rom.write_byte(0x1CFF12, 0x01) rom.write_bytes(0x1CFF0E, [0xFF, 0x01]) + if world.showloot[player] == 'never': + rom.write_bytes(0x1CFF08, [0x00, 0x00, 0x00, 0x00]) + rom.write_byte(0x1CFF11, 0x00) + rom.write_byte(0x1CFF12, 0x00) # turn off hud icon too just to be safe + elif world.showloot[player] == 'presence': + rom.write_bytes(0x1CFF08, [0x01, 0x00, 0x00, 0x00]) + rom.write_byte(0x1CFF11, 0x00) + elif world.showloot[player] == 'compass': + rom.write_bytes(0x1CFF08, [0x01, 0x00, 0x02, 0x00]) + rom.write_byte(0x1CFF11, 0x01) + elif world.showloot[player] == 'always': + rom.write_bytes(0x1CFF08, [0x02, 0x00, 0x00, 0x00]) + rom.write_byte(0x1CFF11, 0x00) + if world.showmap[player] == 'visited': rom.write_bytes(0x1CFF00, [0x01, 0x00, 0x00, 0x05]) elif world.showmap[player] == 'map': diff --git a/data/base2current.bps b/data/base2current.bps index 6c4f49c018f6950a6983d0a7664b7d7a666e4a43..c8a8949ce58305efbe596e81f84129ff35ad6d82 100644 GIT binary patch delta 1450 zcmWlZdrVVT9LMiDEtJxNAmG4ftum}ssVpLof}kKe9FK{UImuAEO;}()CTdiAZs8gW zvT}D}XRgzlL7`lpj=Ao}$8}893KGbc4NMHqY=()BDmpWCcBlXRKHu{@-*0lx`6Z`e zNREf(JtrhmOV^&f-jkAA37G5+m#tX6Xp=*DaK)@X;^;0*a5UGcT@Imuu2cWw5VSPA zqlh!;Yd~!8lzzwsI-Qq&L30CPu@&3_glVV2)vrM?_DzImm(+CVLi?50%+S$uk0&NB z)pYP8)!#Z(H0eKgl+kuxsBY9^S0ap6?Cusc9%_aeR8?W$B*@PHxm}nkQnQR;@HN1* zkiw}x3CLE#Bp}sqxHkzdRvd042?jP(!T~&<1oK0(J4wSKQfKh~q(aosP(lpepoi-D z&sypWx7%TONf_l%_Ip~r!JA&8D#O)!_&QjR+x76$oR?jK%j0qT_fwWzwc8Aa%0tym$k?Jp&kt!7{@Xiz%2A<=CDKIiB%jx!) zQ$d?3%67$5DZJI1nsL|O=cz;`iNh+v+FHZDBJI=orxcj1{iuBt6=W%gMxlgDrP)11 ze9YCb`v}Vjy9hra>>=EXwQJ!*qlLFrvj(zexs8bZkO+<#b`v%dHWOmP7Q$0p4ec06 zMl|Kzg3OdHy?r~PmvHx49vFp4?hm$uFp8KT5NZe?5=L`AG`pOnS8%now)KAd;i_IG zYNTu?$~a?PMn}^x@KID7jp+Zd7S)C#+Ji$-ox~^r4pgyPA|zZbTT1AIr`N)f^+g@t zXII+E420YcPS$XmjBc~HY0cR=J@T@hJeG~3j5XmODES*Vt_22777geQP16OU>9fpj z$Sg>)EDdsC99z?1oNU}7FoEJ-JememHy>*gg0fv6cdVZd3~Y0z)<2WUO=4IMla+{Y=qOW4yHgbiJQ`4O3Gtw zqGjBI4t7|wIi_J+-C;dWH*q=j++kGN<%=OPPM^&Pd70k z#^6O6aPb?@TJGuUH`x7M(jIc+E+I0|9dB2-r1i1*~sP&c_txbbS2aEkKlzU$Tf zcz*_54nDyKri5Kdg@s58@}5OqxFA ze3AiGejvmdk_`g^ScTO&FcXyF(i~U~a`03R)I^x&-NBsv!TDjiJxXcK+JTaI~)aS?jW)X5Cm2Q+TV9ipnTb4#l@#r}B2 z^bg^b;`^=30!QfU(J=62zAS4?Fzg)51t44 z&dEGT18@MBZGmw>iJP}T1IWYje3%CI;H-SOS{Y_|iv4-#(S)b?edo!1_%={1tWcw` zDmC>@K#l*}O00=E`W^UZfF|}jr&uaMOfD4ToeQ?Xd;k*gz5-Yb;_$Bp@Xo@#k}Ndl zp#b#pl`B1fIwY->=0PMiNl!?v@p$B2SOU|(bLzK~4=Z@y4+3wKtwc=fUQ2adkhlgv z+5yYJZv0*$T)&_sVvOPWR_(koTN-K%HkSUq?wX~Z`TU>nqn@pCUMPfla=*dxiYW2h H^2YxG`_*~d delta 1435 zcmWlZYfw{16vua$5MBX6(Bh*tL{NhfQ+$lbLl6NK3Zhsa11*;50IgOBDV^N zNxa~yXya5u;PP~sHtwigrDBaB12cXQN7{~Uos6iYLT$y4>E_G%o%7%GpE-MWX32n0 z?SN0`2@jd7b0n+lq(`*}baaJ!Em}4wwTV8vS*beI)Ui9Jsi8)7r-^RmYE(l_RKuBa z^8{U76fs1vOu7hfUw{Ebz7k!Ud%RT}(yORX3we0uFNfpC z@m5g+z1GS6QQw&ld3D>hiaIs1uSC&)J3FY_1t$4^B_{G%WSTwLLMQW8CZ6i#I&=vE zlIR(O&82({Rz4tIu_(;1w3#`0)Bxyy@@p*02~KZgLX!l(BPFqUxR(c1LsH|AYWAx} zW8QWP3N4xjeA#)Y)or}zPL#!@A`Z=iiKHbC-I)HSo!VV4r*AJX+`sQ|;1}NmAjOih zcr@MbZ9B!@%}@N-O7X-Tk2-?OS}6{cK4sBYDrQk8m5?0?Xd0X#UnQW3`D<-XUmp;- z4gm8_R~cPzO-%XC(&H+_MJorD^sKeY^p45r$xjI=UZZPS2LcUyke==m5;@aemJ|wA zru~e?jO~nHFm^KTA(~a_1N}+SP+`)s`joI1n|86p$QlY6Pct?!)-pCSo)N0JroMR0 z0ox{A3CwF+wqs5yJT-|>PgTMnrdGyq*8Gfd9^-Sy1%fAMie&Ccp_~ zC~S6m8xGqTk@*$iPo5E@wLl3*;wr~BEY%A z;%Ao`qs8^PmZP|X4IHY!)lSa=?~W07-a`(gph&onm{X8?;eP8ai`@mBaf{@@sWtQ% z{I8o(XM|KDXs?}{*^7)bqyyxYA&uh>+Qw6m(i;ZaLNZYRgi&N}7D|J~K z#=ALVU-^|sYTz--_WtlJ=KIb1*S5kjKGacvw7Vvve^7Bbp{H^b14AI@yh%jyu%%Z8enZKw!6xN2Lqo&8Wjp;xJBhEbSL+-In$36z!+XD-?eKO>*yq2%`qW(@P9Xq`7} n*qnsxg7n2NSKl!h`O~j|od0*Stve58`*=ql^#8|jH?{D8vxamA From b6f73ffda4a63a15e8bf00cba975fa1aafa1b148 Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Mon, 11 May 2026 23:02:32 -0500 Subject: [PATCH 43/76] Update hashes in spoiler --- BaseClasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index 8f4df302..9f065220 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -3184,7 +3184,7 @@ class Spoiler(object): 'race': self.world.settings.world_rep['meta']['race'], 'user_notes': self.world.settings.world_rep['meta']['user_notes'], 'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)}, - 'hashes': [{p: self.hashes[p, t] for p in range(1, self.world.players + 1)} for t in range(self.world.teams)], + 'hash': {p: self.hashes[p, 0] for p in range(1, self.world.players + 1)}, # TODO: make this work for multiple teams 'seed': self.world.seed } From 97aceb3f43ce07d2cb3985e52754873583f3a5fc Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Mon, 11 May 2026 23:17:47 -0500 Subject: [PATCH 44/76] Update hash alphabet strings for clarity --- Rom.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Rom.py b/Rom.py index 06903808..67e9e0bc 100644 --- a/Rom.py +++ b/Rom.py @@ -3541,7 +3541,10 @@ Prizes = ['Green Pendant', ] hash_alphabet = [ - "Bow", "Boomerang", "Hookshot", "Bomb", "Mushroom", "Powder", "Rod", "Pendant", "Bombos", "Ether", "Quake", - "Lamp", "Hammer", "Shovel", "Ocarina", "Bug Net", "Book", "Bottle", "Potion", "Cane", "Cape", "Mirror", "Boots", - "Gloves", "Flippers", "Pearl", "Shield", "Tunic", "Heart", "Map", "Compass", "Key" + "Bow", "Boomerang", "Hookshot", "Bomb", "Mushroom", "Powder", + "Ice Rod", "Green Pendant", "Bombos", "Ether", "Quake", "Lamp", + "Hammer", "Shovel", "Ocarina", "Bug Net", "Book", "Bottle", + "Green Potion", "Somaria", "Cape", "Mirror", "Boots", "Gloves", + "Flippers", "Pearl", "Shield", "Green Tunic", "Heart", "Map", + "Compass", "Key", ] From e86312f145db7014ca29ca31f26352dd7369b784 Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Mon, 11 May 2026 23:24:03 -0500 Subject: [PATCH 45/76] Fix broken --- BaseClasses.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index 9f065220..6adf3c3d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -3184,9 +3184,10 @@ class Spoiler(object): 'race': self.world.settings.world_rep['meta']['race'], 'user_notes': self.world.settings.world_rep['meta']['user_notes'], 'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)}, - 'hash': {p: self.hashes[p, 0] for p in range(1, self.world.players + 1)}, # TODO: make this work for multiple teams 'seed': self.world.seed } + if self.hashes: + self.metadata['hashes'] = {p: self.hashes[p, 0] for p in range(1, self.world.players + 1)} # TODO: make this work for multiple teams for p in range(1, self.world.players + 1): from ItemList import set_default_triforce From d2a5bb56d8dcc89064e3ef356925d0a9bc6a2584 Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Mon, 11 May 2026 23:26:07 -0500 Subject: [PATCH 46/76] One more fix for the road... --- BaseClasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index 6adf3c3d..de3ad469 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -3187,7 +3187,7 @@ class Spoiler(object): 'seed': self.world.seed } if self.hashes: - self.metadata['hashes'] = {p: self.hashes[p, 0] for p in range(1, self.world.players + 1)} # TODO: make this work for multiple teams + self.metadata['hash'] = {p: self.hashes[p, 0] for p in range(1, self.world.players + 1)} # TODO: make this work for multiple teams for p in range(1, self.world.players + 1): from ItemList import set_default_triforce From cdfdc1521962cacc9512504da3da45752de4c661 Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Wed, 13 May 2026 00:59:03 -0500 Subject: [PATCH 47/76] Baserom update and bugfix --- Rom.py | 8 ++++---- data/base2current.bps | Bin 157463 -> 157482 bytes 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Rom.py b/Rom.py index 67e9e0bc..67a14a46 100644 --- a/Rom.py +++ b/Rom.py @@ -85,7 +85,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc from Versions import DRVersion, GKVersion, ORVersion JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'beae4c06c4841030709639215e2b03c3' +RANDOMIZERBASEHASH = '40c072b56aef71fb7754c1153b877594' class JsonRom(object): @@ -1505,13 +1505,13 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): if world.loothud[player] == 'never': rom.write_byte(0x1CFF12, 0x00) - elif world.showloot[player] == 'presence': + elif world.loothud[player] == 'presence': rom.write_byte(0x1CFF12, 0x01) rom.write_bytes(0x1CFF0E, [0x01, 0x01]) - elif world.showloot[player] == 'value': + elif world.loothud[player] == 'value': rom.write_byte(0x1CFF12, 0x01) rom.write_bytes(0x1CFF0E, [0xFF, 0xFF]) - elif world.showloot[player] == 'dungeon_value': + elif world.loothud[player] == 'dungeon_value': rom.write_byte(0x1CFF12, 0x01) rom.write_bytes(0x1CFF0E, [0xFF, 0x01]) diff --git a/data/base2current.bps b/data/base2current.bps index c8a8949ce58305efbe596e81f84129ff35ad6d82..a56c6d8fadd65739e0bb86a38a38049f6fab8870 100644 GIT binary patch delta 3316 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 delta 3245 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 Date: Thu, 14 May 2026 00:14:54 -0500 Subject: [PATCH 48/76] Start of attempt for vanilla item placements --- BaseClasses.py | 4 + CLI.py | 1 + Fill.py | 5 +- ItemList.py | 24 ++-- Main.py | 6 +- vanilla_placements.yaml | 239 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 269 insertions(+), 10 deletions(-) create mode 100644 vanilla_placements.yaml diff --git a/BaseClasses.py b/BaseClasses.py index de3ad469..2fb9b3ea 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2895,6 +2895,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..62163b28 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)) 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/Main.py b/Main.py index f2c19eff..b1aaa1c4 100644 --- a/Main.py +++ b/Main.py @@ -335,7 +335,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) @@ -572,7 +572,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'] @@ -1288,6 +1288,8 @@ def create_playthrough(world): for location in sphere: if world.goal[location.player] == 'completionist': continue # every location for that player is required + if location.item.type == "SmallKey": + continue # we remove the item at location and check if game is still beatable logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player) old_item = location.item diff --git a/vanilla_placements.yaml b/vanilla_placements.yaml new file mode 100644 index 00000000..f3bba118 --- /dev/null +++ b/vanilla_placements.yaml @@ -0,0 +1,239 @@ +item_pool_adjust: + 1: + "Bottle (Random)": -4 + "Bottle": 4 + "Lamp": 1 + "Bombs (10)": -1 + "Bombs (3)": 1 +placements: + 1: + "Master Sword Pedestal": Master Sword + "Mushroom": Mushroom + "Ether Tablet": Ether + "Spectacle Rock": Piece of Heart + "Old Man": Magic Mirror + "Floating Island": Piece of Heart + "King Zora": Flippers + "Zora's Ledge": Piece of Heart + "Bottle Merchant": Bottle + "Maze Race": Piece of Heart + "Flute Spot": Ocarina + "Hobo": Bottle + "Desert Ledge": Piece of Heart + "Bombos Tablet": Bombos + "Lake Hylia Island": Piece of Heart + "Purple Chest": Bottle + "Sunken Treasure": Piece of Heart + "Bumper Cave Ledge": Piece of Heart + "Catfish": Quake + "Pyramid": Piece of Heart + "Digging Game": Piece of Heart + "Stumpy": Shovel + "Lost Woods Hideout": Piece of Heart + "Lumberjack Tree": Piece of Heart + "Spectacle Rock Cave": Piece of Heart + "Spiral Cave": Rupees (20) + "Mimic Cave": Piece of Heart + "Paradox Cave Lower - Far Left": Rupees (20) + "Paradox Cave Lower - Left": Rupees (20) + "Paradox Cave Lower - Right": Rupees (20) + "Paradox Cave Lower - Far Right": Rupees (20) + "Paradox Cave Lower - Middle": Rupees (20) + "Paradox Cave Upper - Left": Bombs (3) + "Paradox Cave Upper - Right": Arrows (10) + "Waterfall Fairy - Left": Red Boomerang + "Waterfall Fairy - Right": Red Shield + "Bonk Rock Cave": Piece of Heart + "Graveyard Cave": Piece of Heart + "King's Tomb": Cape + "Potion Shop": Magic Powder + "Kakariko Well - Left": Rupees (20) + "Kakariko Well - Middle": Rupees (20) + "Kakariko Well - Right": Rupees (20) + "Kakariko Well - Bottom": Bombs (3) + "Kakariko Well - Top": Piece of Heart + "Blind's Hideout - Left": Rupees (20) + "Blind's Hideout - Right": Rupees (20) + "Blind's Hideout - Far Left": Rupees (20) + "Blind's Hideout - Far Right": Rupees (20) + "Blind's Hideout - Top": Piece of Heart + "Chicken House": Arrows (10) + "Sick Kid": Bug Catching Net + "Kakariko Tavern": Bottle + "Link's Uncle": Fighter Sword + "Secret Passage": Blue Shield + "Sahasrahla's Hut - Left": Rupees (50) + "Sahasrahla's Hut - Middle": Bombs (3) + "Sahasrahla's Hut - Right": Rupees (50) + "Sahasrahla": Pegasus Boots + "Blacksmith": Tempered Sword + "Magic Bat": Magic Upgrade (1/2) + "Library": Book of Mudora + "Link's House": Lamp + "Checkerboard Cave": Piece of Heart + "Aginah's Cave": Piece of Heart + "Cave 45": Piece of Heart + "Mini Moldorm Cave - Far Left": Bombs (3) + "Mini Moldorm Cave - Left": Rupees (20) + "Mini Moldorm Cave - Right": Rupees (20) + "Mini Moldorm Cave - Far Right": Arrows (10) + "Mini Moldorm Cave - Generous Guy": Rupees (300) + "Ice Rod Cave": Ice Rod + "Floodgate Chest": Bombs (3) + "Spike Cave": Cane of Byrna + "Hookshot Cave - Bottom Right": Rupees (50) + "Hookshot Cave - Top Right": Rupees (50) + "Hookshot Cave - Top Left": Rupees (50) + "Hookshot Cave - Bottom Left": Rupees (50) + "Superbunny Cave - Top": Bombs (3) + "Superbunny Cave - Bottom": Rupees (50) + "Chest Game": Piece of Heart + "C-Shaped House": Rupees (300) + "Brewery": Rupees (300) + "Pyramid Fairy - Left": Golden Sword + "Pyramid Fairy - Right": Silver Arrows + "Peg Cave": Piece of Heart + "Mire Shed - Left": Piece of Heart + "Mire Shed - Right": Arrows (10) + "Hype Cave - Top": Rupees (20) + "Hype Cave - Middle Right": Rupees (20) + "Hype Cave - Middle Left": Rupees (20) + "Hype Cave - Bottom": Rupees (20) + "Hype Cave - Generous Guy": Rupees (300) + "Hyrule Castle - Map Chest": Map (Escape) + "Hyrule Castle - Boomerang Chest": Blue Boomerang + "Hyrule Castle - Zelda's Chest": Lamp + "Sewers - Dark Cross": Small Key (Escape) + "Sewers - Secret Room - Left": Bombs (3) + "Sewers - Secret Room - Middle": Rupees (300) + "Sewers - Secret Room - Right": Arrows (10) + "Sanctuary": Sanctuary Heart Container + "Eastern Palace - Cannonball Chest": Rupees (100) + "Eastern Palace - Map Chest": Map (Eastern Palace) + "Eastern Palace - Compass Chest": Compass (Eastern Palace) + "Eastern Palace - Big Chest": Bow + "Eastern Palace - Big Key Chest": Big Key (Eastern Palace) + "Eastern Palace - Boss": Boss Heart Container + "Eastern Palace - Prize": Green Pendant + "Desert Palace - Compass Chest": Compass (Desert Palace) + "Desert Palace - Big Key Chest": Big Key (Desert Palace) + "Desert Palace - Map Chest": Map (Desert Palace) + "Desert Palace - Torch": Small Key (Desert Palace) + "Desert Palace - Big Chest": Power Glove + "Desert Palace - Boss": Boss Heart Container + "Desert Palace - Prize": Blue Pendant + "Tower of Hera - Map Chest": Map (Tower of Hera) + "Tower of Hera - Basement Cage": Small Key (Tower of Hera) + "Tower of Hera - Big Key Chest": Big Key (Tower of Hera) + "Tower of Hera - Compass Chest": Compass (Tower of Hera) + "Tower of Hera - Big Chest": Moon Pearl + "Tower of Hera - Boss": Boss Heart Container + "Tower of Hera - Prize": Red Pendant + "Castle Tower - Room 03": Small Key (Agahnims Tower) + "Castle Tower - Dark Maze": Small Key (Agahnims Tower) + "Palace of Darkness - Shooter Room": Small Key (Palace of Darkness) + "Palace of Darkness - The Arena - Bridge": Small Key (Palace of Darkness) + "Palace of Darkness - The Arena - Ledge": Small Key (Palace of Darkness) + "Palace of Darkness - Map Chest": Map (Palace of Darkness) + "Palace of Darkness - Stalfos Basement": Small Key (Palace of Darkness) + "Palace of Darkness - Big Key Chest": Big Key (Palace of Darkness) + "Palace of Darkness - Dark Maze - Top": Rupees (20) + "Palace of Darkness - Dark Maze - Bottom": Rupees (5) + "Palace of Darkness - Big Chest": Hammer + "Palace of Darkness - Compass Chest": Compass (Palace of Darkness) + "Palace of Darkness - Dark Basement - Left": Single Arrow + "Palace of Darkness - Dark Basement - Right": Small Key (Palace of Darkness) + "Palace of Darkness - Harmless Hellway": Small Key (Palace of Darkness) + "Palace of Darkness - Boss": Boss Heart Container + "Palace of Darkness - Prize": Crystal 3 + "Thieves' Town - Map Chest": Map (Thieves Town) + "Thieves' Town - Ambush Chest": Rupees (20) + "Thieves' Town - Compass Chest": Compass (Thieves Town) + "Thieves' Town - Big Key Chest": Big Key (Thieves Town) + "Thieves' Town - Boss": Boss Heart Container + "Thieves' Town - Prize": Crystal 4 + "Thieves' Town - Attic": Bombs (3) + "Thieves' Town - Blind's Cell": Small Key (Thieves Town) + "Thieves' Town - Big Chest": Titans Mitts + "Skull Woods - Map Chest": Map (Skull Woods) + "Skull Woods - Big Chest": Fire Rod + "Skull Woods - Pinball Room": Small Key (Skull Woods) + "Skull Woods - Pot Prison": Small Key (Skull Woods) + "Skull Woods - Compass Chest": Compass (Skull Woods) + "Skull Woods - Big Key Chest": Big Key (Skull Woods) + "Skull Woods - Bridge Room": Small Key (Skull Woods) + "Skull Woods - Boss": Boss Heart Container + "Skull Woods - Prize": Crystal 7 + "Swamp Palace - Entrance": Small Key (Swamp Palace) + "Swamp Palace - Map Chest": Map (Swamp Palace) + "Swamp Palace - Big Chest": Hookshot + "Swamp Palace - Compass Chest": Compass (Swamp Palace) + "Swamp Palace - Big Key Chest": Big Key (Swamp Palace) + "Swamp Palace - West Chest": Rupees (20) + "Swamp Palace - Flooded Room - Left": Rupees (20) + "Swamp Palace - Flooded Room - Right": Rupees (20) + "Swamp Palace - Waterfall Room": Rupees (20) + "Swamp Palace - Boss": Boss Heart Container + "Swamp Palace - Prize": Crystal 1 + "Ice Palace - Compass Chest": Compass (Ice Palace) + "Ice Palace - Big Key Chest": Big Key (Ice Palace) + "Ice Palace - Spike Room": Small Key (Ice Palace) + "Ice Palace - Map Chest": Map (Ice Palace) + "Ice Palace - Freezor Chest": Bombs (3) + "Ice Palace - Iced T Room": Small Key (Ice Palace) + "Ice Palace - Big Chest": Blue Mail + "Ice Palace - Boss": Boss Heart Container + "Ice Palace - Prize": Crystal 5 + "Misery Mire - Main Lobby": Small Key (Misery Mire) + "Misery Mire - Big Chest": Cane of Somaria + "Misery Mire - Map Chest": Map (Misery Mire) + "Misery Mire - Spike Chest": Small Key (Misery Mire) + "Misery Mire - Bridge Chest": Small Key (Misery Mire) + "Misery Mire - Compass Chest": Compass (Misery Mire) + "Misery Mire - Big Key Chest": Big Key (Misery Mire) + "Misery Mire - Boss": Boss Heart Container + "Misery Mire - Prize": Crystal 6 + "Turtle Rock - Compass Chest": Compass (Turtle Rock) + "Turtle Rock - Roller Room - Left": Map (Turtle Rock) + "Turtle Rock - Roller Room - Right": Small Key (Turtle Rock) + "Turtle Rock - Chain Chomps": Small Key (Turtle Rock) + "Turtle Rock - Big Key Chest": Big Key (Turtle Rock) + "Turtle Rock - Big Chest": Mirror Shield + "Turtle Rock - Crystaroller Room": Small Key (Turtle Rock) + "Turtle Rock - Eye Bridge - Bottom Left": Small Key (Turtle Rock) + "Turtle Rock - Eye Bridge - Bottom Right": Rupees (20) + "Turtle Rock - Eye Bridge - Top Left": Rupees (5) + "Turtle Rock - Eye Bridge - Top Right": Rupee (1) + "Turtle Rock - Boss": Boss Heart Container + "Turtle Rock - Prize": Crystal 2 + "Ganons Tower - Bob's Torch": Small Key (Ganons Tower) + "Ganons Tower - Hope Room - Left": Arrows (10) + "Ganons Tower - Hope Room - Right": Bombs (3) + "Ganons Tower - Big Chest": Red Mail + "Ganons Tower - Bob's Chest": Arrows (10) + "Ganons Tower - Tile Room": Small Key (Ganons Tower) + "Ganons Tower - Compass Room - Top Left": Compass (Ganons Tower) + "Ganons Tower - Compass Room - Top Right": Rupee (1) + "Ganons Tower - Compass Room - Bottom Left": Rupees (20) + "Ganons Tower - Compass Room - Bottom Right": Arrows (10) + "Ganons Tower - Map Chest": Map (Ganons Tower) + "Ganons Tower - Firesnake Room": Small Key (Ganons Tower) + "Ganons Tower - DMs Room - Top Left": Bombs (3) + "Ganons Tower - DMs Room - Top Right": Arrows (10) + "Ganons Tower - DMs Room - Bottom Left": Bombs (3) + "Ganons Tower - DMs Room - Bottom Right": Rupees (20) + "Ganons Tower - Randomizer Room - Top Left": Arrows (10) + "Ganons Tower - Randomizer Room - Top Right": Arrows (10) + "Ganons Tower - Randomizer Room - Bottom Left": Bombs (3) + "Ganons Tower - Randomizer Room - Bottom Right": Bombs (3) + "Ganons Tower - Big Key Room - Left": Arrows (10) + "Ganons Tower - Big Key Room - Right": Bombs (3) + "Ganons Tower - Big Key Chest": Big Key (Ganons Tower) + "Ganons Tower - Mini Helmasaur Room - Left": Bombs (3) + "Ganons Tower - Mini Helmasaur Room - Right": Bombs (3) + "Ganons Tower - Pre-Moldorm Chest": Small Key (Ganons Tower) + "Ganons Tower - Validation Chest": Rupees (5) +medallions: + 1: + "Misery Mire": Ether + "Turtle Rock": Quake From d530b68e86aa2a9c0099efcc6af2074deef70a05 Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Thu, 14 May 2026 10:31:03 -0500 Subject: [PATCH 49/76] Fix crystal numbers --- vanilla_placements.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vanilla_placements.yaml b/vanilla_placements.yaml index f3bba118..9045a08f 100644 --- a/vanilla_placements.yaml +++ b/vanilla_placements.yaml @@ -145,7 +145,7 @@ placements: "Palace of Darkness - Dark Basement - Right": Small Key (Palace of Darkness) "Palace of Darkness - Harmless Hellway": Small Key (Palace of Darkness) "Palace of Darkness - Boss": Boss Heart Container - "Palace of Darkness - Prize": Crystal 3 + "Palace of Darkness - Prize": Crystal 1 "Thieves' Town - Map Chest": Map (Thieves Town) "Thieves' Town - Ambush Chest": Rupees (20) "Thieves' Town - Compass Chest": Compass (Thieves Town) @@ -163,7 +163,7 @@ placements: "Skull Woods - Big Key Chest": Big Key (Skull Woods) "Skull Woods - Bridge Room": Small Key (Skull Woods) "Skull Woods - Boss": Boss Heart Container - "Skull Woods - Prize": Crystal 7 + "Skull Woods - Prize": Crystal 3 "Swamp Palace - Entrance": Small Key (Swamp Palace) "Swamp Palace - Map Chest": Map (Swamp Palace) "Swamp Palace - Big Chest": Hookshot @@ -174,7 +174,7 @@ placements: "Swamp Palace - Flooded Room - Right": Rupees (20) "Swamp Palace - Waterfall Room": Rupees (20) "Swamp Palace - Boss": Boss Heart Container - "Swamp Palace - Prize": Crystal 1 + "Swamp Palace - Prize": Crystal 2 "Ice Palace - Compass Chest": Compass (Ice Palace) "Ice Palace - Big Key Chest": Big Key (Ice Palace) "Ice Palace - Spike Room": Small Key (Ice Palace) @@ -205,7 +205,7 @@ placements: "Turtle Rock - Eye Bridge - Top Left": Rupees (5) "Turtle Rock - Eye Bridge - Top Right": Rupee (1) "Turtle Rock - Boss": Boss Heart Container - "Turtle Rock - Prize": Crystal 2 + "Turtle Rock - Prize": Crystal 7 "Ganons Tower - Bob's Torch": Small Key (Ganons Tower) "Ganons Tower - Hope Room - Left": Arrows (10) "Ganons Tower - Hope Room - Right": Bombs (3) From ec89afaa5e3ce9d62e5c5c69c316fe52b6b0f6b9 Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Thu, 14 May 2026 20:05:39 -0500 Subject: [PATCH 50/76] Fix up junk items in vanilla_placements --- vanilla_placements.yaml | 62 +++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/vanilla_placements.yaml b/vanilla_placements.yaml index 9045a08f..b6cb2154 100644 --- a/vanilla_placements.yaml +++ b/vanilla_placements.yaml @@ -2,9 +2,13 @@ item_pool_adjust: 1: "Bottle (Random)": -4 "Bottle": 4 - "Lamp": 1 + "Bombs (10)": -1 "Bombs (3)": 1 + + "Rupees (5)": -1 + "Arrows (10)": -1 + "Rupees (20)": 2 placements: 1: "Master Sword Pedestal": Master Sword @@ -32,7 +36,7 @@ placements: "Lost Woods Hideout": Piece of Heart "Lumberjack Tree": Piece of Heart "Spectacle Rock Cave": Piece of Heart - "Spiral Cave": Rupees (20) + "Spiral Cave": Rupees (50) "Mimic Cave": Piece of Heart "Paradox Cave Lower - Far Left": Rupees (20) "Paradox Cave Lower - Left": Rupees (20) @@ -57,11 +61,12 @@ placements: "Blind's Hideout - Far Left": Rupees (20) "Blind's Hideout - Far Right": Rupees (20) "Blind's Hideout - Top": Piece of Heart - "Chicken House": Arrows (10) + "Chicken House": Arrows (10) # TODO: VERIFY "Sick Kid": Bug Catching Net "Kakariko Tavern": Bottle "Link's Uncle": Fighter Sword "Secret Passage": Blue Shield +# "Secret Passage": Lamp "Sahasrahla's Hut - Left": Rupees (50) "Sahasrahla's Hut - Middle": Bombs (3) "Sahasrahla's Hut - Right": Rupees (50) @@ -69,7 +74,8 @@ placements: "Blacksmith": Tempered Sword "Magic Bat": Magic Upgrade (1/2) "Library": Book of Mudora - "Link's House": Lamp + "Link's House": Rupees (5) +# "Link's House": Lamp "Checkerboard Cave": Piece of Heart "Aginah's Cave": Piece of Heart "Cave 45": Piece of Heart @@ -86,7 +92,7 @@ placements: "Hookshot Cave - Top Left": Rupees (50) "Hookshot Cave - Bottom Left": Rupees (50) "Superbunny Cave - Top": Bombs (3) - "Superbunny Cave - Bottom": Rupees (50) + "Superbunny Cave - Bottom": Rupees (20) "Chest Game": Piece of Heart "C-Shaped House": Rupees (300) "Brewery": Rupees (300) @@ -94,7 +100,7 @@ placements: "Pyramid Fairy - Right": Silver Arrows "Peg Cave": Piece of Heart "Mire Shed - Left": Piece of Heart - "Mire Shed - Right": Arrows (10) + "Mire Shed - Right": Rupees (20) "Hype Cave - Top": Rupees (20) "Hype Cave - Middle Right": Rupees (20) "Hype Cave - Middle Left": Rupees (20) @@ -137,33 +143,17 @@ placements: "Palace of Darkness - Map Chest": Map (Palace of Darkness) "Palace of Darkness - Stalfos Basement": Small Key (Palace of Darkness) "Palace of Darkness - Big Key Chest": Big Key (Palace of Darkness) - "Palace of Darkness - Dark Maze - Top": Rupees (20) + "Palace of Darkness - Dark Maze - Top": Bombs (3) "Palace of Darkness - Dark Maze - Bottom": Rupees (5) +# "Palace of Darkness - Dark Maze - Bottom": Small Key (Palace of Darkness) "Palace of Darkness - Big Chest": Hammer "Palace of Darkness - Compass Chest": Compass (Palace of Darkness) "Palace of Darkness - Dark Basement - Left": Single Arrow "Palace of Darkness - Dark Basement - Right": Small Key (Palace of Darkness) "Palace of Darkness - Harmless Hellway": Small Key (Palace of Darkness) +# "Palace of Darkness - Harmless Hellway": Rupees (5) "Palace of Darkness - Boss": Boss Heart Container "Palace of Darkness - Prize": Crystal 1 - "Thieves' Town - Map Chest": Map (Thieves Town) - "Thieves' Town - Ambush Chest": Rupees (20) - "Thieves' Town - Compass Chest": Compass (Thieves Town) - "Thieves' Town - Big Key Chest": Big Key (Thieves Town) - "Thieves' Town - Boss": Boss Heart Container - "Thieves' Town - Prize": Crystal 4 - "Thieves' Town - Attic": Bombs (3) - "Thieves' Town - Blind's Cell": Small Key (Thieves Town) - "Thieves' Town - Big Chest": Titans Mitts - "Skull Woods - Map Chest": Map (Skull Woods) - "Skull Woods - Big Chest": Fire Rod - "Skull Woods - Pinball Room": Small Key (Skull Woods) - "Skull Woods - Pot Prison": Small Key (Skull Woods) - "Skull Woods - Compass Chest": Compass (Skull Woods) - "Skull Woods - Big Key Chest": Big Key (Skull Woods) - "Skull Woods - Bridge Room": Small Key (Skull Woods) - "Skull Woods - Boss": Boss Heart Container - "Skull Woods - Prize": Crystal 3 "Swamp Palace - Entrance": Small Key (Swamp Palace) "Swamp Palace - Map Chest": Map (Swamp Palace) "Swamp Palace - Big Chest": Hookshot @@ -175,6 +165,24 @@ placements: "Swamp Palace - Waterfall Room": Rupees (20) "Swamp Palace - Boss": Boss Heart Container "Swamp Palace - Prize": Crystal 2 + "Skull Woods - Map Chest": Map (Skull Woods) + "Skull Woods - Big Chest": Fire Rod + "Skull Woods - Pinball Room": Small Key (Skull Woods) + "Skull Woods - Pot Prison": Small Key (Skull Woods) + "Skull Woods - Compass Chest": Compass (Skull Woods) + "Skull Woods - Big Key Chest": Big Key (Skull Woods) + "Skull Woods - Bridge Room": Small Key (Skull Woods) + "Skull Woods - Boss": Boss Heart Container + "Skull Woods - Prize": Crystal 3 + "Thieves' Town - Map Chest": Map (Thieves Town) + "Thieves' Town - Ambush Chest": Rupees (20) + "Thieves' Town - Compass Chest": Compass (Thieves Town) + "Thieves' Town - Big Key Chest": Big Key (Thieves Town) + "Thieves' Town - Attic": Bombs (3) + "Thieves' Town - Blind's Cell": Small Key (Thieves Town) + "Thieves' Town - Big Chest": Titans Mitts + "Thieves' Town - Boss": Boss Heart Container + "Thieves' Town - Prize": Crystal 4 "Ice Palace - Compass Chest": Compass (Ice Palace) "Ice Palace - Big Key Chest": Big Key (Ice Palace) "Ice Palace - Spike Room": Small Key (Ice Palace) @@ -220,7 +228,7 @@ placements: "Ganons Tower - Firesnake Room": Small Key (Ganons Tower) "Ganons Tower - DMs Room - Top Left": Bombs (3) "Ganons Tower - DMs Room - Top Right": Arrows (10) - "Ganons Tower - DMs Room - Bottom Left": Bombs (3) + "Ganons Tower - DMs Room - Bottom Left": Rupees (20) "Ganons Tower - DMs Room - Bottom Right": Rupees (20) "Ganons Tower - Randomizer Room - Top Left": Arrows (10) "Ganons Tower - Randomizer Room - Top Right": Arrows (10) @@ -232,7 +240,7 @@ placements: "Ganons Tower - Mini Helmasaur Room - Left": Bombs (3) "Ganons Tower - Mini Helmasaur Room - Right": Bombs (3) "Ganons Tower - Pre-Moldorm Chest": Small Key (Ganons Tower) - "Ganons Tower - Validation Chest": Rupees (5) + "Ganons Tower - Validation Chest": Rupees (20) medallions: 1: "Misery Mire": Ether From a946c2b3b03d8d6e474907125408ce384959ccd6 Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Thu, 14 May 2026 21:23:10 -0500 Subject: [PATCH 51/76] Allow applying patches to rom from CLI --- CLI.py | 2 +- Main.py | 2 ++ Rom.py | 43 +++++++++++++++++++++++++++++ patches/no_dungeon_item_popups.ips | Bin 0 -> 14 bytes patches/pod_key_swap.ips | Bin 0 -> 20 bytes patches/pod_key_swap.ips.bak | Bin 0 -> 22 bytes resources/app/cli/args.json | 4 +++ 7 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 patches/no_dungeon_item_popups.ips create mode 100644 patches/pod_key_swap.ips create mode 100644 patches/pod_key_swap.ips.bak diff --git a/CLI.py b/CLI.py index 62163b28..6d5a0292 100644 --- a/CLI.py +++ b/CLI.py @@ -179,7 +179,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/Main.py b/Main.py index b1aaa1c4..e0f0f73d 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, @@ -364,6 +365,7 @@ 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) + 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], diff --git a/Rom.py b/Rom.py index 67a14a46..8b3873b2 100644 --- a/Rom.py +++ b/Rom.py @@ -2088,6 +2088,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/no_dungeon_item_popups.ips b/patches/no_dungeon_item_popups.ips new file mode 100644 index 0000000000000000000000000000000000000000..b9865236e3027a5964a1f22243d63f1dbe65937c GIT binary patch literal 14 VcmWG=3~~05V9a7*WN`I&0{|A<0?z;d literal 0 HcmV?d00001 diff --git a/patches/pod_key_swap.ips b/patches/pod_key_swap.ips new file mode 100644 index 0000000000000000000000000000000000000000..ee72cd700188a4cbf47b806535c1d237a7c8338f GIT binary patch literal 20 bcmWG=3~}~gc;&{xXv**^f`M_FtG^onI)Me6 literal 0 HcmV?d00001 diff --git a/patches/pod_key_swap.ips.bak b/patches/pod_key_swap.ips.bak new file mode 100644 index 0000000000000000000000000000000000000000..abe2e3adaa07f56e3cd3a325283d6fa16da6aaf8 GIT binary patch literal 22 dcmWG=3~}~gc;&{xz`$tA@G63Vaha>X8vsAU1)2Z= literal 0 HcmV?d00001 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", From 821c0ee46eac7bed31680a36c0e3df1628fae3de Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Thu, 14 May 2026 21:36:27 -0500 Subject: [PATCH 52/76] Remove accidentally .bak file --- patches/pod_key_swap.ips.bak | Bin 22 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 patches/pod_key_swap.ips.bak diff --git a/patches/pod_key_swap.ips.bak b/patches/pod_key_swap.ips.bak deleted file mode 100644 index abe2e3adaa07f56e3cd3a325283d6fa16da6aaf8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22 dcmWG=3~}~gc;&{xz`$tA@G63Vaha>X8vsAU1)2Z= From 8ae1aefcf322c10fb774f1e6409513ce4f3c0152 Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Thu, 14 May 2026 22:09:49 -0500 Subject: [PATCH 53/76] Add prize pack info to vanilla_placements --- vanilla_placements.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/vanilla_placements.yaml b/vanilla_placements.yaml index b6cb2154..f48d7231 100644 --- a/vanilla_placements.yaml +++ b/vanilla_placements.yaml @@ -241,6 +241,22 @@ placements: "Ganons Tower - Mini Helmasaur Room - Right": Bombs (3) "Ganons Tower - Pre-Moldorm Chest": Small Key (Ganons Tower) "Ganons Tower - Validation Chest": Rupees (20) +drops: + 1: + Pack 1: ["Small Heart", "Small Heart", "Small Heart", "Small Heart", "Rupee (1)", "Small Heart", "Small Heart", "Rupee (1)"] + Pack 2: ["Rupees (5)", "Rupee (1)", "Rupees (5)", "Rupees (20)", "Rupees (5)", "Rupee (1)", "Rupees (5)", "Rupees (5)"] + Pack 3: ["Big Magic", "Small Magic", "Small Magic", "Rupees (5)", "Big Magic", "Small Magic", "Small Heart", "Small Magic"] + Pack 4: ["Single Bomb", "Single Bomb", "Single Bomb", "Bombs (4)", "Single Bomb", "Single Bomb", "Bombs (8)", "Single Bomb"] + Pack 5: ["Arrows (5)", "Small Heart", "Arrows (5)", "Arrows (10)", "Arrows (5)", "Small Heart", "Arrows (5)", "Arrows (10)"] + Pack 6: ["Small Magic", "Rupee (1)", "Small Heart", "Arrows (5)", "Small Magic", "Single Bomb", "Rupee (1)", "Small Heart"] + Pack 7: ["Small Heart", "Fairy", "Big Magic", "Rupees (20)", "Bombs (8)", "Small Heart", "Rupees (20)", "Arrows (10)"] + Tree Pull Tier 1: "Rupee (1)" + Tree Pull Tier 2: "Rupees (5)" + Tree Pull Tier 3: "Rupees (20)" + Crab Normal: "Rupee (1)" + Crab Special: "Rupees (20)" + Stun Prize: "Rupee (1)" + Fish: "Rupees (20)" medallions: 1: "Misery Mire": Ether From 84270544bfac12ed4730707cc75b3d62e3cc63e6 Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Thu, 14 May 2026 22:28:41 -0500 Subject: [PATCH 54/76] vanilla with universal keys --- CLI.py | 9 +- vanilla_placements_universal.yaml | 264 ++++++++++++++++++++++++++++++ 2 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 vanilla_placements_universal.yaml diff --git a/CLI.py b/CLI.py index 6d5a0292..889ca73d 100644 --- a/CLI.py +++ b/CLI.py @@ -107,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 diff --git a/vanilla_placements_universal.yaml b/vanilla_placements_universal.yaml new file mode 100644 index 00000000..8de39293 --- /dev/null +++ b/vanilla_placements_universal.yaml @@ -0,0 +1,264 @@ +item_pool_adjust: + 1: + "Bottle (Random)": -4 + "Bottle": 4 + + "Bombs (10)": -1 + "Bombs (3)": 1 + + "Rupees (5)": -1 + "Arrows (10)": -1 + "Rupees (20)": -8 + + "Small Key (Universal)": 5 +placements: + 1: + "Master Sword Pedestal": Master Sword + "Mushroom": Mushroom + "Ether Tablet": Ether + "Spectacle Rock": Piece of Heart + "Old Man": Magic Mirror + "Floating Island": Piece of Heart + "King Zora": Flippers + "Zora's Ledge": Piece of Heart + "Bottle Merchant": Bottle + "Maze Race": Piece of Heart + "Flute Spot": Ocarina + "Hobo": Bottle + "Desert Ledge": Piece of Heart + "Bombos Tablet": Bombos + "Lake Hylia Island": Piece of Heart + "Purple Chest": Bottle + "Sunken Treasure": Piece of Heart + "Bumper Cave Ledge": Piece of Heart + "Catfish": Quake + "Pyramid": Piece of Heart + "Digging Game": Piece of Heart + "Stumpy": Shovel + "Lost Woods Hideout": Piece of Heart + "Lumberjack Tree": Piece of Heart + "Spectacle Rock Cave": Piece of Heart + "Spiral Cave": Rupees (50) + "Mimic Cave": Piece of Heart + "Paradox Cave Lower - Far Left": Rupees (20) + "Paradox Cave Lower - Left": Rupees (20) + "Paradox Cave Lower - Right": Rupees (20) + "Paradox Cave Lower - Far Right": Rupees (20) + "Paradox Cave Lower - Middle": Rupees (20) + "Paradox Cave Upper - Left": Bombs (3) + "Paradox Cave Upper - Right": Arrows (10) + "Waterfall Fairy - Left": Red Boomerang + "Waterfall Fairy - Right": Red Shield + "Bonk Rock Cave": Piece of Heart + "Graveyard Cave": Piece of Heart + "King's Tomb": Cape + "Potion Shop": Magic Powder + "Kakariko Well - Left": Rupees (20) + "Kakariko Well - Middle": Rupees (20) + "Kakariko Well - Right": Rupees (20) + "Kakariko Well - Bottom": Bombs (3) + "Kakariko Well - Top": Piece of Heart + "Blind's Hideout - Left": Rupees (20) + "Blind's Hideout - Right": Rupees (20) + "Blind's Hideout - Far Left": Rupees (20) + "Blind's Hideout - Far Right": Rupees (20) + "Blind's Hideout - Top": Piece of Heart + "Chicken House": Arrows (10) # TODO: VERIFY + "Sick Kid": Bug Catching Net + "Kakariko Tavern": Bottle + "Link's Uncle": Fighter Sword + "Secret Passage": Blue Shield +# "Secret Passage": Lamp + "Sahasrahla's Hut - Left": Rupees (50) + "Sahasrahla's Hut - Middle": Bombs (3) + "Sahasrahla's Hut - Right": Rupees (50) + "Sahasrahla": Pegasus Boots + "Blacksmith": Tempered Sword + "Magic Bat": Magic Upgrade (1/2) + "Library": Book of Mudora + "Link's House": Rupees (5) +# "Link's House": Lamp + "Checkerboard Cave": Piece of Heart + "Aginah's Cave": Piece of Heart + "Cave 45": Piece of Heart + "Mini Moldorm Cave - Far Left": Bombs (3) + "Mini Moldorm Cave - Left": Rupees (20) + "Mini Moldorm Cave - Right": Rupees (20) + "Mini Moldorm Cave - Far Right": Arrows (10) + "Mini Moldorm Cave - Generous Guy": Rupees (300) + "Ice Rod Cave": Ice Rod + "Floodgate Chest": Bombs (3) + "Spike Cave": Cane of Byrna + "Hookshot Cave - Bottom Right": Rupees (50) + "Hookshot Cave - Top Right": Rupees (50) + "Hookshot Cave - Top Left": Rupees (50) + "Hookshot Cave - Bottom Left": Rupees (50) + "Superbunny Cave - Top": Bombs (3) + "Superbunny Cave - Bottom": Rupees (20) + "Chest Game": Piece of Heart + "C-Shaped House": Rupees (300) + "Brewery": Rupees (300) + "Pyramid Fairy - Left": Golden Sword + "Pyramid Fairy - Right": Silver Arrows + "Peg Cave": Piece of Heart + "Mire Shed - Left": Piece of Heart + "Mire Shed - Right": Rupees (20) + "Hype Cave - Top": Rupees (20) + "Hype Cave - Middle Right": Rupees (20) + "Hype Cave - Middle Left": Rupees (20) + "Hype Cave - Bottom": Rupees (20) + "Hype Cave - Generous Guy": Rupees (300) + "Hyrule Castle - Map Chest": Map (Escape) + "Hyrule Castle - Boomerang Chest": Blue Boomerang + "Hyrule Castle - Zelda's Chest": Lamp + "Sewers - Dark Cross": Small Key (Universal) + "Sewers - Secret Room - Left": Bombs (3) + "Sewers - Secret Room - Middle": Rupees (300) + "Sewers - Secret Room - Right": Arrows (10) + "Sanctuary": Sanctuary Heart Container + "Eastern Palace - Cannonball Chest": Rupees (100) + "Eastern Palace - Map Chest": Map (Eastern Palace) + "Eastern Palace - Compass Chest": Compass (Eastern Palace) + "Eastern Palace - Big Chest": Bow + "Eastern Palace - Big Key Chest": Big Key (Eastern Palace) + "Eastern Palace - Boss": Boss Heart Container + "Eastern Palace - Prize": Green Pendant + "Desert Palace - Compass Chest": Compass (Desert Palace) + "Desert Palace - Big Key Chest": Big Key (Desert Palace) + "Desert Palace - Map Chest": Map (Desert Palace) + "Desert Palace - Torch": Small Key (Universal) + "Desert Palace - Big Chest": Power Glove + "Desert Palace - Boss": Boss Heart Container + "Desert Palace - Prize": Blue Pendant + "Tower of Hera - Map Chest": Map (Tower of Hera) + "Tower of Hera - Basement Cage": Small Key (Universal) + "Tower of Hera - Big Key Chest": Big Key (Tower of Hera) + "Tower of Hera - Compass Chest": Compass (Tower of Hera) + "Tower of Hera - Big Chest": Moon Pearl + "Tower of Hera - Boss": Boss Heart Container + "Tower of Hera - Prize": Red Pendant + "Castle Tower - Room 03": Small Key (Universal) + "Castle Tower - Dark Maze": Small Key (Universal) + "Palace of Darkness - Shooter Room": Small Key (Universal) + "Palace of Darkness - The Arena - Bridge": Small Key (Universal) + "Palace of Darkness - The Arena - Ledge": Small Key (Universal) + "Palace of Darkness - Map Chest": Map (Palace of Darkness) + "Palace of Darkness - Stalfos Basement": Small Key (Universal) + "Palace of Darkness - Big Key Chest": Big Key (Palace of Darkness) + "Palace of Darkness - Dark Maze - Top": Bombs (3) + "Palace of Darkness - Dark Maze - Bottom": Small Key (Universal) + "Palace of Darkness - Big Chest": Hammer + "Palace of Darkness - Compass Chest": Compass (Palace of Darkness) + "Palace of Darkness - Dark Basement - Left": Single Arrow + "Palace of Darkness - Dark Basement - Right": Small Key (Universal) + "Palace of Darkness - Harmless Hellway": Rupees (5) + "Palace of Darkness - Boss": Boss Heart Container + "Palace of Darkness - Prize": Crystal 1 + "Swamp Palace - Entrance": Small Key (Universal) + "Swamp Palace - Map Chest": Map (Swamp Palace) + "Swamp Palace - Big Chest": Hookshot + "Swamp Palace - Compass Chest": Compass (Swamp Palace) + "Swamp Palace - Big Key Chest": Big Key (Swamp Palace) + "Swamp Palace - West Chest": Rupees (20) + "Swamp Palace - Flooded Room - Left": Rupees (20) + "Swamp Palace - Flooded Room - Right": Rupees (20) + "Swamp Palace - Waterfall Room": Rupees (20) + "Swamp Palace - Boss": Boss Heart Container + "Swamp Palace - Prize": Crystal 2 + "Skull Woods - Map Chest": Map (Skull Woods) + "Skull Woods - Big Chest": Fire Rod + "Skull Woods - Pinball Room": Small Key (Universal) + "Skull Woods - Pot Prison": Small Key (Universal) + "Skull Woods - Compass Chest": Compass (Skull Woods) + "Skull Woods - Big Key Chest": Big Key (Skull Woods) + "Skull Woods - Bridge Room": Small Key (Universal) + "Skull Woods - Boss": Boss Heart Container + "Skull Woods - Prize": Crystal 3 + "Thieves' Town - Map Chest": Map (Thieves Town) + "Thieves' Town - Ambush Chest": Rupees (20) + "Thieves' Town - Compass Chest": Compass (Thieves Town) + "Thieves' Town - Big Key Chest": Big Key (Thieves Town) + "Thieves' Town - Attic": Bombs (3) + "Thieves' Town - Blind's Cell": Small Key (Universal) + "Thieves' Town - Big Chest": Titans Mitts + "Thieves' Town - Boss": Boss Heart Container + "Thieves' Town - Prize": Crystal 4 + "Ice Palace - Compass Chest": Compass (Ice Palace) + "Ice Palace - Big Key Chest": Big Key (Ice Palace) + "Ice Palace - Spike Room": Small Key (Universal) + "Ice Palace - Map Chest": Map (Ice Palace) + "Ice Palace - Freezor Chest": Bombs (3) + "Ice Palace - Iced T Room": Small Key (Universal) + "Ice Palace - Big Chest": Blue Mail + "Ice Palace - Boss": Boss Heart Container + "Ice Palace - Prize": Crystal 5 + "Misery Mire - Main Lobby": Small Key (Universal) + "Misery Mire - Big Chest": Cane of Somaria + "Misery Mire - Map Chest": Map (Misery Mire) + "Misery Mire - Spike Chest": Small Key (Universal) + "Misery Mire - Bridge Chest": Small Key (Universal) + "Misery Mire - Compass Chest": Compass (Misery Mire) + "Misery Mire - Big Key Chest": Big Key (Misery Mire) + "Misery Mire - Boss": Boss Heart Container + "Misery Mire - Prize": Crystal 6 + "Turtle Rock - Compass Chest": Compass (Turtle Rock) + "Turtle Rock - Roller Room - Left": Map (Turtle Rock) + "Turtle Rock - Roller Room - Right": Small Key (Universal) + "Turtle Rock - Chain Chomps": Small Key (Universal) + "Turtle Rock - Big Key Chest": Big Key (Turtle Rock) + "Turtle Rock - Big Chest": Mirror Shield + "Turtle Rock - Crystaroller Room": Small Key (Universal) + "Turtle Rock - Eye Bridge - Bottom Left": Small Key (Universal) + "Turtle Rock - Eye Bridge - Bottom Right": Rupees (20) + "Turtle Rock - Eye Bridge - Top Left": Rupees (5) + "Turtle Rock - Eye Bridge - Top Right": Rupee (1) + "Turtle Rock - Boss": Boss Heart Container + "Turtle Rock - Prize": Crystal 7 + "Ganons Tower - Bob's Torch": Small Key (Universal) + "Ganons Tower - Hope Room - Left": Arrows (10) + "Ganons Tower - Hope Room - Right": Bombs (3) + "Ganons Tower - Big Chest": Red Mail + "Ganons Tower - Bob's Chest": Arrows (10) + "Ganons Tower - Tile Room": Small Key (Universal) + "Ganons Tower - Compass Room - Top Left": Compass (Ganons Tower) + "Ganons Tower - Compass Room - Top Right": Rupee (1) + "Ganons Tower - Compass Room - Bottom Left": Rupees (20) + "Ganons Tower - Compass Room - Bottom Right": Arrows (10) + "Ganons Tower - Map Chest": Map (Ganons Tower) + "Ganons Tower - Firesnake Room": Small Key (Universal) + "Ganons Tower - DMs Room - Top Left": Bombs (3) + "Ganons Tower - DMs Room - Top Right": Arrows (10) + "Ganons Tower - DMs Room - Bottom Left": Rupees (20) + "Ganons Tower - DMs Room - Bottom Right": Rupees (20) + "Ganons Tower - Randomizer Room - Top Left": Arrows (10) + "Ganons Tower - Randomizer Room - Top Right": Arrows (10) + "Ganons Tower - Randomizer Room - Bottom Left": Bombs (3) + "Ganons Tower - Randomizer Room - Bottom Right": Bombs (3) + "Ganons Tower - Big Key Room - Left": Arrows (10) + "Ganons Tower - Big Key Room - Right": Bombs (3) + "Ganons Tower - Big Key Chest": Big Key (Ganons Tower) + "Ganons Tower - Mini Helmasaur Room - Left": Bombs (3) + "Ganons Tower - Mini Helmasaur Room - Right": Bombs (3) + "Ganons Tower - Pre-Moldorm Chest": Small Key (Universal) + "Ganons Tower - Validation Chest": Rupees (20) +drops: + 1: + Pack 1: ["Small Heart", "Small Heart", "Small Heart", "Small Heart", "Rupee (1)", "Small Heart", "Small Heart", "Rupee (1)"] + Pack 2: ["Rupees (5)", "Rupee (1)", "Rupees (5)", "Rupees (20)", "Rupees (5)", "Rupee (1)", "Rupees (5)", "Rupees (5)"] + Pack 3: ["Big Magic", "Small Magic", "Small Magic", "Rupees (5)", "Big Magic", "Small Magic", "Small Heart", "Small Magic"] + Pack 4: ["Single Bomb", "Single Bomb", "Single Bomb", "Bombs (4)", "Single Bomb", "Single Bomb", "Bombs (8)", "Single Bomb"] + Pack 5: ["Arrows (5)", "Small Heart", "Arrows (5)", "Arrows (10)", "Arrows (5)", "Small Heart", "Arrows (5)", "Arrows (10)"] + Pack 6: ["Small Magic", "Rupee (1)", "Small Heart", "Arrows (5)", "Small Magic", "Single Bomb", "Rupee (1)", "Small Heart"] + Pack 7: ["Small Heart", "Fairy", "Big Magic", "Rupees (20)", "Bombs (8)", "Small Heart", "Rupees (20)", "Arrows (10)"] + Tree Pull Tier 1: "Rupee (1)" + Tree Pull Tier 2: "Rupees (5)" + Tree Pull Tier 3: "Rupees (20)" + Crab Normal: "Rupee (1)" + Crab Special: "Rupees (20)" + Stun Prize: "Rupee (1)" + Fish: "Rupees (20)" +medallions: + 1: + "Misery Mire": Ether + "Turtle Rock": Quake + From 1ae9da59bb105fe2939117f57cf85bf85a0a4a4b Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Thu, 14 May 2026 23:52:59 -0500 Subject: [PATCH 55/76] Fix Sword and Shield item --- BaseClasses.py | 5 ++++- Items.py | 2 +- Rom.py | 19 ++++++++++--------- vanilla_placements.yaml | 17 +++++++++-------- vanilla_placements_universal.yaml | 17 +++++++++-------- 5 files changed, 33 insertions(+), 27 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 2fb9b3ea..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 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/Rom.py b/Rom.py index 8b3873b2..7f1e5f07 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]) rom.write_byte(0x18004E, 0) # Escape Fill (nothing) write_int16(rom, 0x180183, 300) # Escape fill rupee bow diff --git a/vanilla_placements.yaml b/vanilla_placements.yaml index f48d7231..d0bd73c2 100644 --- a/vanilla_placements.yaml +++ b/vanilla_placements.yaml @@ -3,10 +3,13 @@ item_pool_adjust: "Bottle (Random)": -4 "Bottle": 4 + "Fighter Sword": -1 + "Blue Shield": -1 + "Sword and Shield": 1 + "Bombs (10)": -1 "Bombs (3)": 1 - "Rupees (5)": -1 "Arrows (10)": -1 "Rupees (20)": 2 placements: @@ -61,12 +64,11 @@ placements: "Blind's Hideout - Far Left": Rupees (20) "Blind's Hideout - Far Right": Rupees (20) "Blind's Hideout - Top": Piece of Heart - "Chicken House": Arrows (10) # TODO: VERIFY + "Chicken House": Arrows (10) # Blue Boomerang that turns into 10 arrows "Sick Kid": Bug Catching Net "Kakariko Tavern": Bottle - "Link's Uncle": Fighter Sword - "Secret Passage": Blue Shield -# "Secret Passage": Lamp + "Link's Uncle": Sword and Shield + "Secret Passage": Rupees (5) # Lamp that turns into blue rupee "Sahasrahla's Hut - Left": Rupees (50) "Sahasrahla's Hut - Middle": Bombs (3) "Sahasrahla's Hut - Right": Rupees (50) @@ -74,8 +76,7 @@ placements: "Blacksmith": Tempered Sword "Magic Bat": Magic Upgrade (1/2) "Library": Book of Mudora - "Link's House": Rupees (5) -# "Link's House": Lamp + "Link's House": Rupees (5) # Lamp that turns into blue rupee "Checkerboard Cave": Piece of Heart "Aginah's Cave": Piece of Heart "Cave 45": Piece of Heart @@ -95,7 +96,7 @@ placements: "Superbunny Cave - Bottom": Rupees (20) "Chest Game": Piece of Heart "C-Shaped House": Rupees (300) - "Brewery": Rupees (300) + "Brewery": Rupees (300) # Red Boomerang that turns into 300 rupees "Pyramid Fairy - Left": Golden Sword "Pyramid Fairy - Right": Silver Arrows "Peg Cave": Piece of Heart diff --git a/vanilla_placements_universal.yaml b/vanilla_placements_universal.yaml index 8de39293..96141e0d 100644 --- a/vanilla_placements_universal.yaml +++ b/vanilla_placements_universal.yaml @@ -3,10 +3,13 @@ item_pool_adjust: "Bottle (Random)": -4 "Bottle": 4 + "Fighter Sword": -1 + "Blue Shield": -1 + "Sword and Shield": 1 + "Bombs (10)": -1 "Bombs (3)": 1 - "Rupees (5)": -1 "Arrows (10)": -1 "Rupees (20)": -8 @@ -63,12 +66,11 @@ placements: "Blind's Hideout - Far Left": Rupees (20) "Blind's Hideout - Far Right": Rupees (20) "Blind's Hideout - Top": Piece of Heart - "Chicken House": Arrows (10) # TODO: VERIFY + "Chicken House": Arrows (10) # Blue Boomerang that turns into 10 arrows "Sick Kid": Bug Catching Net "Kakariko Tavern": Bottle - "Link's Uncle": Fighter Sword - "Secret Passage": Blue Shield -# "Secret Passage": Lamp + "Link's Uncle": Sword and Shield + "Secret Passage": Rupees (5) # Lamp that turns into blue rupee "Sahasrahla's Hut - Left": Rupees (50) "Sahasrahla's Hut - Middle": Bombs (3) "Sahasrahla's Hut - Right": Rupees (50) @@ -76,8 +78,7 @@ placements: "Blacksmith": Tempered Sword "Magic Bat": Magic Upgrade (1/2) "Library": Book of Mudora - "Link's House": Rupees (5) -# "Link's House": Lamp + "Link's House": Rupees (5) # Lamp that turns into blue rupee "Checkerboard Cave": Piece of Heart "Aginah's Cave": Piece of Heart "Cave 45": Piece of Heart @@ -97,7 +98,7 @@ placements: "Superbunny Cave - Bottom": Rupees (20) "Chest Game": Piece of Heart "C-Shaped House": Rupees (300) - "Brewery": Rupees (300) + "Brewery": Rupees (300) # Red Boomerang that turns into 300 rupees "Pyramid Fairy - Left": Golden Sword "Pyramid Fairy - Right": Silver Arrows "Peg Cave": Piece of Heart From 151765d8d266303e9a253e7923c2294c99ae2b46 Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Fri, 15 May 2026 21:53:28 -0500 Subject: [PATCH 56/76] Fix broken patches arg --- Main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Main.py b/Main.py index e0f0f73d..036c366a 100644 --- a/Main.py +++ b/Main.py @@ -365,7 +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) - apply_rom_patches(rom, map(lambda arg: arg.strip(), args.patches[player].split(","))) + 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], From ab2e8f78185143962e97629d5ee4ecee45100538 Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Sat, 16 May 2026 21:29:42 -0500 Subject: [PATCH 57/76] Update baserom and enemy_deny.yaml --- Rom.py | 2 +- data/base2current.bps | Bin 157482 -> 157506 bytes source/enemizer/enemy_deny.yaml | 15 +++++++++------ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Rom.py b/Rom.py index 67a14a46..bfa90f7b 100644 --- a/Rom.py +++ b/Rom.py @@ -85,7 +85,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc from Versions import DRVersion, GKVersion, ORVersion JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '40c072b56aef71fb7754c1153b877594' +RANDOMIZERBASEHASH = '2eff237fff0085c5fdd80542e07778f2' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index a56c6d8fadd65739e0bb86a38a38049f6fab8870..720a0548f3626d9cb379f550a901e90397fd7830 100644 GIT binary patch delta 1244 zcmWmBZA?>F7zglspLSSoYbn9`hMQPLgfXl{jY28qRS_o$)grMOI_3*RH#g&SX7pb0 zE&^h?UgW62s`A!e3S}X#9(B9gtO^u(w$}WNe~sBeTYdF?!tRkD}|_>2XxhMGIQ< z*{)}XZ|4RT%32h=-1oeTPVA(Oh{=GfD{puV3j&&sEejt1_)jqS^9_$AQ-S`us+Y{t z$Gmm$_L+-TG@b$JwAh{r9rQcYnF&U9k2^U`@z6uztLTqRu*97jBmyx~AEF2Co0m?CcEf{2Hu2YnJ80m{_tWX-DLORqGa6g^DRZjL| zR^@(1QY{+H0&kqhgIS&2LMkI)B2_70LtmKSeOixr6C@^`@P(LYN>@Zt&b#3{e6rtC z@U+(-u44~pOeWy_{Y}C zJ5fe9e0cblyh8CjktT8OWRyT$i7A%&c-K=-m?QR-?jn-iu zX(>5F!mMa^az$djkncO53nri!qLF+snUL>lRB|#btt6 z+_sCA97Tl;4!>i?9RY?DMX}B8Zoh#%l=&@gNtrjW4;Al(8<3*#K^d?#U5pya zVI#dC-71G_NXkM97BJI=XrBc(XlkopBS!L-WxPfkeAg{dMl;7x=-DfE$*tX1^llgA zg3gL^cf)h7J^g{GUZr69QjoO=eCw@HPD6|ZHCDiW(3GQ@3iut8YS5!S5D(fKUvLjF zirB)NSaRCZSl)chai&El??(z79HC98jrJKO@v0xBvhE delta 1230 zcmWmBdrVVT90%}oe=U!eT8a>-WlqO@qzp-QY8(`3Sy6Okie>nS7zA`v!?H)5^VObP z>0*o6UaoMWjCD+qaw(wJc|9}BH6&Hg%%FcPVz$K?hY?)0DE=X-zoot%5l z8NQ{FZfVXPQ>p*$O5Pqht~#OOP$WC0NS$)^%*dK zdUFDyp~iPZ-7Pu4hv;U$CFgO7=JCR|ePW}<$E}I{W4SfT$@2AN&fU(*WL+mG=#9AZ zf%)L{fJ(iW?42b)?&KDJ!!07|1@PTl!LT_is_R&jb@%+AvDi<+u%w`9?{w``t?{nM zHgKh>%|`ANz%uR#Nh*X6E=IZv!IE~O&lE=<=tk5>VuetZSwFxUAq9O)z9_80Q4gYa zvfcs>+-7po0ymat4AIeed_o&V{-3*IG5q{{gcJwKSzCjJ4N){?QBI!kO8@|d%kdzeO|vBK@rec{Ag>3Wbh zM0bY8?311B*>)tFr?(yc<90@vT~`nxmv8n?ois9XO>cNwdsE%cH!AM7B;?W4Ib^O4 zOe$_ijFj8pBbW@4^EOzKIvJuKy)qRdFKkd!(b`81AB@H)>b=&n1*UXKc|)SojhOPX zkDAxt(@M(HVdwNHGCxmrJRHvV&L2xp%$X$WLCOM&UA;BPzazSZeEC2r6a$CIWjhpe z$>eW4y!m?l0F@N`ARSXx1ll~hh8e@=`K!GT%(0kaC(Fwqf6>$+U8tW})sr@=4lI+_ zZR zzC})14OnY9k;c3EK%%rJ%1J^tz@{1Arx^JSm`^1od&Y^r5=t|5$+)?_Z`jOEYJEjp za#Q+Okorpa0h}NkHv;Ckd8ESuE4h{AjsqItkcF(O0xM@E2dZF&uB2g-XeD`3!6ebh zKUTp;PFwzE4nFNP1rHeG*pwV)$ma7Saw}ZTcE<84WQKRlq)uYgM)ep>Dml+ zP`i;NZGm6s9oqdq9*-jql9DyLIETJcJJpqd)o%4Mbw}OqOIty!$}nH1QgCTL_EeTQ diff --git a/source/enemizer/enemy_deny.yaml b/source/enemizer/enemy_deny.yaml index 56aca46d..6d4d2ac1 100644 --- a/source/enemizer/enemy_deny.yaml +++ b/source/enemizer/enemy_deny.yaml @@ -84,7 +84,10 @@ UwGeneralDeny: - [ 0x0039, 4, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "FirebarCW", "FirebarCCW" ] ] #"Skull Woods - Play Pen - Spike Trap 1" - [0x0039, 5, ["RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "Bumper", "AntiFairyCircle"]] #"Skull Woods - Play Pen - Hardhat Beetle" - [ 0x0039, 6, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "FirebarCW", "FirebarCCW" ] ] #"Skull Woods - Play Pen - Spike Trap 2" - - [0x003a, 1, ["RollerVerticalUp", "RollerVerticalDown"]] + - [ 0x003a, 0, ["RollerVerticalUp", "RollerVerticalDown"]] + - [ 0x003a, 1, ["RollerVerticalUp", "RollerVerticalDown"]] + - [ 0x003a, 3, ["RollerHorizontalLeft", "RollerHorizontalRight"]] + - [ 0x003a, 4, ["RollerHorizontalLeft", "RollerHorizontalRight"]] - [ 0x003b, 1, [ "Bumper", "AntiFairyCircle" ]] - [ 0x003b, 4, ["RollerVerticalUp", "RollerVerticalDown"]] - [ 0x003c, 0, ["BigSpike"]] @@ -339,11 +342,11 @@ UwGeneralDeny: - [ 0x00c2, 5, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots - [ 0x00c5, 6, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Turtle Rock - Catwalk - Mini Helmasaur" - [ 0x00c5, 7, [ "Statue" ] ] #"Turtle Rock - Catwalk - Laser Eye (Left) 4" - - [0x00c6, 2, ["Bumper", "AntiFairyCircle"]] - - [0x00c6, 3, ["Bumper", "AntiFairyCircle"]] - - [0x00c6, 4, ["Bumper", "AntiFairyCircle"]] - - [0x00c6, 5, ["Bumper", "AntiFairyCircle"]] - - [0x00c6, 6, ["Bumper", "AntiFairyCircle"]] + - [ 0x00c6, 2, ["Bumper", "AntiFairyCircle", "BigSpike" ]] + - [ 0x00c6, 3, ["Bumper", "AntiFairyCircle", "BigSpike" ]] + - [ 0x00c6, 4, ["Bumper", "AntiFairyCircle", "BigSpike" ]] + - [ 0x00c6, 5, ["Bumper", "AntiFairyCircle", "BigSpike" ]] + - [ 0x00c6, 6, ["Bumper", "AntiFairyCircle", "BigSpike" ]] - [ 0x00cb, 0, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots - [ 0x00cb, 3, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Thieves' Town - Grand Room NW - Zol 1" - [ 0x00cb, 5, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Thieves' Town - Grand Room NW - Zol 2" From fb57e9fd34937096a72eb84477299d232afacd56 Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Sat, 16 May 2026 22:18:51 -0500 Subject: [PATCH 58/76] Fix some enemy denials being overwritten --- source/rom/DataTables.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/source/rom/DataTables.py b/source/rom/DataTables.py index 8523cf77..ace6a6ce 100644 --- a/source/rom/DataTables.py +++ b/source/rom/DataTables.py @@ -58,17 +58,17 @@ class DataTables: self.bush_sprite_table = {} # enemizer conditions - self.uw_enemy_denials = {} - self.ow_enemy_denials = {} - self.uw_enemy_drop_denials = {} + self.uw_enemy_denials = defaultdict(set) + self.ow_enemy_denials = defaultdict(set) + self.uw_enemy_drop_denials = defaultdict(set) self.sheet_choices = [] denial_data = load_cached_yaml(['source', 'enemizer', 'enemy_deny.yaml']) for denial in denial_data['UwGeneralDeny']: - self.uw_enemy_denials[denial[0], denial[1]] = {sprite_translation[x] for x in denial[2]} + self.uw_enemy_denials[denial[0], denial[1]] |= {sprite_translation[x] for x in denial[2]} for denial in denial_data['OwGeneralDeny']: - self.ow_enemy_denials[denial[0], denial[1]] = {sprite_translation[x] for x in denial[2]} + self.ow_enemy_denials[denial[0], denial[1]] |= {sprite_translation[x] for x in denial[2]} for denial in denial_data['UwEnemyDrop']: - self.uw_enemy_drop_denials[denial[0], denial[1]] = {sprite_translation[x] for x in denial[2]} + self.uw_enemy_drop_denials[denial[0], denial[1]] |= {sprite_translation[x] for x in denial[2]} weights = load_cached_yaml(['source', 'enemizer', 'enemy_weight.yaml']) self.uw_weights = {sprite_translation[k]: v for k, v in weights['UW'].items()} self.ow_weights = {sprite_translation[k]: v for k, v in weights['OW'].items()} From c39a31b299ae4f94a581a53c1a8e32d538bc2087 Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Sun, 17 May 2026 15:08:46 -0500 Subject: [PATCH 59/76] Add all_starting patch --- patches/all_starting.ips | Bin 0 -> 96 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 patches/all_starting.ips diff --git a/patches/all_starting.ips b/patches/all_starting.ips new file mode 100644 index 0000000000000000000000000000000000000000..d3333b8e533092b1ca416c687fbea7e5ee920409 GIT binary patch literal 96 zcmWG=3~~05Fm_m literal 0 HcmV?d00001 From 320e518519120a60d15a726394bd4837446c2cfe Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Tue, 19 May 2026 20:30:30 -0500 Subject: [PATCH 60/76] Add no_escape_assist patch --- patches/no_escape_assist.ips | Bin 0 -> 15 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 patches/no_escape_assist.ips diff --git a/patches/no_escape_assist.ips b/patches/no_escape_assist.ips new file mode 100644 index 0000000000000000000000000000000000000000..ae1ff592f56055b17429a3946cdab58448103662 GIT binary patch literal 15 WcmWG=3~~05VDM#NVqkFfcLM+y Date: Tue, 19 May 2026 20:33:23 -0500 Subject: [PATCH 61/76] Add RainDeathRefill spots to no_escape_assist --- patches/no_escape_assist.ips | Bin 15 -> 23 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/patches/no_escape_assist.ips b/patches/no_escape_assist.ips index ae1ff592f56055b17429a3946cdab58448103662..2dfd97c5017dfa62e90bd0921d5ee9b5143dab57 100644 GIT binary patch literal 23 ecmWG=3~~05VDM#NVqlP9Y-M0z;AC+1cLM+{kONr& literal 15 WcmWG=3~~05VDM#NVqkFfcLM+y Date: Tue, 19 May 2026 22:45:15 -0500 Subject: [PATCH 62/76] Update standard escape ammo refills to be more consistent --- Rom.py | 54 ++++++++++++++++-------------------------- data/base2current.bps | Bin 157506 -> 157478 bytes 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/Rom.py b/Rom.py index b504da9d..4196942a 100644 --- a/Rom.py +++ b/Rom.py @@ -85,7 +85,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc from Versions import DRVersion, GKVersion, ORVersion JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '2eff237fff0085c5fdd80542e07778f2' +RANDOMIZERBASEHASH = 'd5b351a79bab079408bdf19b0deaa655' class JsonRom(object): @@ -1789,44 +1789,30 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): rom.write_bytes(0x6D2FB, [0x00, 0x00, 0xf7, 0xff, 0x02, 0x0E]) rom.write_bytes(0x6D313, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E]) - rom.write_byte(0x18004E, 0) # Escape Fill (nothing) - write_int16(rom, 0x180183, 300) # Escape fill rupee bow - rom.write_bytes(0x180185, [0, 0, 0]) # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [0, 0, 0]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [0, 0, 0]) # Mantle respawn refills (magic, bombs, arrows) - bow_max, bomb_max, magic_max = 0, 0, 0 - bow_small, bomb_small, magic_small = 10, 3, 0x20 + write_int16(rom, 0x180183, 0) # Escape fill rupee bow + # Uncle / Zelda / Mantle respawn refills (magic, bombs, arrows) + rom.write_bytes(0x180185, [0] * 9) + if world.mode[player] == 'standard': + if world.doorShuffle[player] not in ['vanilla']: + # If door shuffle, give player small bit of all three ammos on respawn during escape + # Uncle / Zelda / Mantle respawn refills (magic, bombs, arrows) + rom.write_bytes(0x180185, [0x20, 3, 10] * 3) + + # Always fully fill ammo of starting weapon on respawn during escape if uncle_location.item is not None and uncle_location.item.name in ['Bow', 'Progressive Bow']: - rom.write_byte(0x18004E, 1) # Escape Fill (arrows) write_int16(rom, 0x180183, 300) # Escape fill rupee bow - rom.write_bytes(0x180185, [0, 0, 70]) # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [0, 0, 70]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [0, 0, 70]) # Mantle respawn refills (magic, bombs, arrows) - bow_max = 70 + rom.write_byte(0x180187, 70) # Uncle respawn refill arrows + rom.write_byte(0x18018A, 70) # Zelda respawn refill arrows + rom.write_byte(0x18018D, 70) # Mantle respawn refill arrowss elif uncle_location.item is not None and uncle_location.item.name in ['Bomb Upgrade (+10)' if world.bombbag[player] else 'Bombs (10)']: - rom.write_byte(0x18004E, 2) # Escape Fill (bombs) - rom.write_bytes(0x180185, [0, 50, 0]) # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [0, 50, 0]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [0, 50, 0]) # Mantle respawn refills (magic, bombs, arrows) - bomb_max = 50 + rom.write_byte(0x180186, 50) # Uncle respawn refill bombs + rom.write_byte(0x180189, 50) # Zelda respawn refill bombs + rom.write_byte(0x18018C, 50) # Mantle respawn refill bombs elif uncle_location.item is not None and uncle_location.item.name in ['Cane of Somaria', 'Cane of Byrna', 'Fire Rod']: - rom.write_byte(0x18004E, 4) # Escape Fill (magic) - rom.write_bytes(0x180185, [0x80, 0, 0]) # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [0x80, 0, 0]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [0x80, 0, 0]) # Mantle respawn refills (magic, bombs, arrows) - magic_max = 0x80 - if world.doorShuffle[player] not in ['vanilla', 'basic']: - # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180185, [max(magic_small, magic_max), max(bomb_small, bomb_max), max(bow_small, bow_max)]) - # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [max(magic_small, magic_max), max(bomb_small, bomb_max), max(bow_small, bow_max)]) - # Mantle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [max(magic_small, magic_max), max(bomb_small, bomb_max), max(bow_small, bow_max)]) - elif world.doorShuffle[player] == 'basic': # just in case a bomb is needed to get to a chest - rom.write_bytes(0x180185, [max(magic_small, magic_max), max(bomb_small, bomb_max), max(bow_small, bow_max)]) - rom.write_bytes(0x180188, [magic_small, max(bomb_small, bomb_max), bow_small]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [magic_small, max(bomb_small, bomb_max), bow_small]) # Mantle respawn refills (magic, bombs, arrows) + rom.write_byte(0x180185, 0x80) # Uncle respawn refill magic + rom.write_byte(0x180188, 0x80) # Zelda respawn refill magic + rom.write_byte(0x18018B, 0x80) # Mantle respawn refill magic # patch swamp: Need to enable permanent drain of water as dam or swamp were moved rom.write_byte(0x18003D, 0x01 if world.swamp_patch_required[player] else 0x00) diff --git a/data/base2current.bps b/data/base2current.bps index 720a0548f3626d9cb379f550a901e90397fd7830..6d61d379716b80ab026900b8b8ecdf80578d71b7 100644 GIT binary patch delta 17702 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 z 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 delta 17863 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 Cy< From cd5bc9a2061694513d327484d2cf07971a00116e Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Tue, 19 May 2026 22:45:15 -0500 Subject: [PATCH 63/76] Update standard escape ammo refills to be more consistent --- Rom.py | 54 ++++++++++++++++-------------------------- data/base2current.bps | Bin 157506 -> 157478 bytes 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/Rom.py b/Rom.py index bfa90f7b..dbec28d0 100644 --- a/Rom.py +++ b/Rom.py @@ -85,7 +85,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc from Versions import DRVersion, GKVersion, ORVersion JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '2eff237fff0085c5fdd80542e07778f2' +RANDOMIZERBASEHASH = 'd5b351a79bab079408bdf19b0deaa655' class JsonRom(object): @@ -1788,44 +1788,30 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): rom.write_bytes(0x6D2FB, [0x00, 0x00, 0xf7, 0xff, 0x02, 0x0E]) rom.write_bytes(0x6D313, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E]) - rom.write_byte(0x18004E, 0) # Escape Fill (nothing) - write_int16(rom, 0x180183, 300) # Escape fill rupee bow - rom.write_bytes(0x180185, [0, 0, 0]) # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [0, 0, 0]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [0, 0, 0]) # Mantle respawn refills (magic, bombs, arrows) - bow_max, bomb_max, magic_max = 0, 0, 0 - bow_small, bomb_small, magic_small = 10, 3, 0x20 + write_int16(rom, 0x180183, 0) # Escape fill rupee bow + # Uncle / Zelda / Mantle respawn refills (magic, bombs, arrows) + rom.write_bytes(0x180185, [0] * 9) + if world.mode[player] == 'standard': + if world.doorShuffle[player] not in ['vanilla']: + # If door shuffle, give player small bit of all three ammos on respawn during escape + # Uncle / Zelda / Mantle respawn refills (magic, bombs, arrows) + rom.write_bytes(0x180185, [0x20, 3, 10] * 3) + + # Always fully fill ammo of starting weapon on respawn during escape if uncle_location.item is not None and uncle_location.item.name in ['Bow', 'Progressive Bow']: - rom.write_byte(0x18004E, 1) # Escape Fill (arrows) write_int16(rom, 0x180183, 300) # Escape fill rupee bow - rom.write_bytes(0x180185, [0, 0, 70]) # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [0, 0, 70]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [0, 0, 70]) # Mantle respawn refills (magic, bombs, arrows) - bow_max = 70 + rom.write_byte(0x180187, 70) # Uncle respawn refill arrows + rom.write_byte(0x18018A, 70) # Zelda respawn refill arrows + rom.write_byte(0x18018D, 70) # Mantle respawn refill arrowss elif uncle_location.item is not None and uncle_location.item.name in ['Bomb Upgrade (+10)' if world.bombbag[player] else 'Bombs (10)']: - rom.write_byte(0x18004E, 2) # Escape Fill (bombs) - rom.write_bytes(0x180185, [0, 50, 0]) # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [0, 50, 0]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [0, 50, 0]) # Mantle respawn refills (magic, bombs, arrows) - bomb_max = 50 + rom.write_byte(0x180186, 50) # Uncle respawn refill bombs + rom.write_byte(0x180189, 50) # Zelda respawn refill bombs + rom.write_byte(0x18018C, 50) # Mantle respawn refill bombs elif uncle_location.item is not None and uncle_location.item.name in ['Cane of Somaria', 'Cane of Byrna', 'Fire Rod']: - rom.write_byte(0x18004E, 4) # Escape Fill (magic) - rom.write_bytes(0x180185, [0x80, 0, 0]) # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [0x80, 0, 0]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [0x80, 0, 0]) # Mantle respawn refills (magic, bombs, arrows) - magic_max = 0x80 - if world.doorShuffle[player] not in ['vanilla', 'basic']: - # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180185, [max(magic_small, magic_max), max(bomb_small, bomb_max), max(bow_small, bow_max)]) - # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [max(magic_small, magic_max), max(bomb_small, bomb_max), max(bow_small, bow_max)]) - # Mantle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [max(magic_small, magic_max), max(bomb_small, bomb_max), max(bow_small, bow_max)]) - elif world.doorShuffle[player] == 'basic': # just in case a bomb is needed to get to a chest - rom.write_bytes(0x180185, [max(magic_small, magic_max), max(bomb_small, bomb_max), max(bow_small, bow_max)]) - rom.write_bytes(0x180188, [magic_small, max(bomb_small, bomb_max), bow_small]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [magic_small, max(bomb_small, bomb_max), bow_small]) # Mantle respawn refills (magic, bombs, arrows) + rom.write_byte(0x180185, 0x80) # Uncle respawn refill magic + rom.write_byte(0x180188, 0x80) # Zelda respawn refill magic + rom.write_byte(0x18018B, 0x80) # Mantle respawn refill magic # patch swamp: Need to enable permanent drain of water as dam or swamp were moved rom.write_byte(0x18003D, 0x01 if world.swamp_patch_required[player] else 0x00) diff --git a/data/base2current.bps b/data/base2current.bps index 720a0548f3626d9cb379f550a901e90397fd7830..6d61d379716b80ab026900b8b8ecdf80578d71b7 100644 GIT binary patch delta 17702 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 z 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 delta 17863 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 Cy< From 33cd2d076ce0c7fa1fa0a63200a58f1d9dcd74fe Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Sun, 24 May 2026 08:56:06 -0500 Subject: [PATCH 64/76] Change intro text for mystery --- Rom.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Rom.py b/Rom.py index dbec28d0..8ace2559 100644 --- a/Rom.py +++ b/Rom.py @@ -1931,7 +1931,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): write_enemizer_tweaks(rom, world, player) randomize_damage_table(rom, world, player) - write_strings(rom, world, player, team) + write_strings(rom, world, player, team, is_mystery) # write initial sram rom.write_initial_sram() @@ -2390,7 +2390,7 @@ def write_string_to_rom(rom, target, string): rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes)) -def write_strings(rom, world, player, team): +def write_strings(rom, world, player, team, is_mystery=False): tt = TextTable() tt.removeUnwantedText() if world.shuffle[player] != 'vanilla': @@ -2905,7 +2905,22 @@ def write_strings(rom, world, player, team): sanc_text = "Dark Chapel" tt['menu_start_2'] = "{MENU}\n{SPEED0}\n≥@'s " + lh_text + "\n " + sanc_text + "\n{CHOICE3}" tt['menu_start_3'] = "{MENU}\n{SPEED0}\n≥@'s " + lh_text + "\n " + sanc_text + "\n Mountain Cave\n{CHOICE2}" - if world.mode[player] == 'inverted': + + if is_mystery: + tt['intro_main'] = CompressedTextMapper.convert( + "{INTRO}\n Episode III" + + "{PAUSE3}\n A Link to the Past" + + "{PAUSE3}\n Mystery Randomizer" + + "{PAUSE3}\n\n\n" + + "{PAUSE3}\nAfter mostly disregarding what happened in the first two games," + + "{PAUSE3}\nLink awakens to a massive mystery." + + "{PAUSE3}\nHe doesn't know why he's here, or what he must do." + + "{PAUSE3} {CHANGEPIC}\nGanon has moved around all the items in Hyrule." + + "{PAUSE7}\nYou will have to find all the items necessary to achieve your goal." + + "{PAUSE7}\nThis is your chance to be a hero." + + "{PAUSE3} {CHANGEPIC}\nYou must determine and achieve your goal." + + "{PAUSE9} {CHANGEPIC}", False) + elif world.mode[player] == 'inverted': tt['intro_main'] = CompressedTextMapper.convert( "{INTRO}\n Episode III" + "{PAUSE3}\n A Link to the Past" @@ -2919,6 +2934,7 @@ def write_strings(rom, world, player, team): + "{PAUSE7}\nThis is your chance to be a hero." + "{PAUSE3} {CHANGEPIC}\nYou must get the 7 crystals to beat Ganon." + "{PAUSE9} {CHANGEPIC}", False) + rom.write_bytes(0xE0000, tt.getBytes()) credits = Credits() From 5af7073ed101d66075591fa509f9f615eea3d90e Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Sun, 24 May 2026 17:16:51 -0500 Subject: [PATCH 65/76] Add bumper to enemy_deny --- Rom.py | 2 +- data/base2current.bps | Bin 157478 -> 157522 bytes source/enemizer/enemy_deny.yaml | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Rom.py b/Rom.py index 8ace2559..e3821adc 100644 --- a/Rom.py +++ b/Rom.py @@ -85,7 +85,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc from Versions import DRVersion, GKVersion, ORVersion JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'd5b351a79bab079408bdf19b0deaa655' +RANDOMIZERBASEHASH = '2ef2af81254cf2ee22295a12f82a7ce2' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 6d61d379716b80ab026900b8b8ecdf80578d71b7..b00557da919bc9b9a0ef79e083cdf0e5b7ee5507 100644 GIT binary patch delta 7058 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 Date: Mon, 25 May 2026 00:17:48 -0500 Subject: [PATCH 66/76] Update baserom --- Rom.py | 2 +- Rules.py | 5 ++--- data/base2current.bps | Bin 157522 -> 157515 bytes 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Rom.py b/Rom.py index f4393283..b476ba23 100644 --- a/Rom.py +++ b/Rom.py @@ -85,7 +85,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc from Versions import DRVersion, GKVersion, ORVersion JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '2ef2af81254cf2ee22295a12f82a7ce2' +RANDOMIZERBASEHASH = '100c3e1da68680a0f3d8e1fc94568de6' class JsonRom(object): diff --git a/Rules.py b/Rules.py index 1c9ba3f6..32802638 100644 --- a/Rules.py +++ b/Rules.py @@ -505,7 +505,7 @@ def global_rules(world, player): set_rule(world.get_entrance('Skull Woods Rock (West)', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Skull Woods Rock (East)', player), lambda state: state.can_lift_rocks(player)) # this more like an ohko rule - dependent on bird being present too - so enemizer could turn this off? - set_rule(world.get_entrance('Bumper Cave Ledge Drop', player), lambda state: world.can_take_damage or state.has('Cape', player) or state.has('Cane of Byrna', player) or state.has_sword(player)) + set_rule(world.get_entrance('Bumper Cave Ledge Drop', player), lambda state: state.has('Cape', player) or state.has('Cane of Byrna', player) or state.has_sword(player)) set_rule(world.get_entrance('Bumper Cave Rock (Outer)', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Bumper Cave Rock (Inner)', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Skull Woods Pass Rock (North)', player), lambda state: state.can_lift_heavy_rocks(player)) @@ -1404,8 +1404,7 @@ def ow_bunny_rules(world, player): add_bunny_rule(world.get_entrance('Skull Woods Forgotten Bush (East)', player), player) add_bunny_rule(world.get_entrance('Skull Woods Second Section Hole', player), player) add_bunny_rule(world.get_entrance('East Dark Death Mountain Bushes', player), player) - if not world.can_take_damage: - add_bunny_rule(world.get_entrance('Bumper Cave Ledge Drop', player), player) + add_bunny_rule(world.get_entrance('Bumper Cave Ledge Drop', player), player) add_bunny_rule(world.get_entrance('Bumper Cave Rock (Outer)', player), player) add_bunny_rule(world.get_entrance('Bumper Cave Rock (Inner)', player), player) add_bunny_rule(world.get_entrance('Skull Woods Pass Bush Row (West)', player), player) diff --git a/data/base2current.bps b/data/base2current.bps index b00557da919bc9b9a0ef79e083cdf0e5b7ee5507..40f9a6fd5ab4aa8cf85a585d073e84e03f13e632 100644 GIT binary patch delta 6575 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 From 5278f9f2c61da921dfceded1a51f648080406a21 Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Mon, 25 May 2026 10:51:55 -0500 Subject: [PATCH 67/76] Change more Dark Palace to Palace of Darkness to Appease Fouton --- Items.py | 6 +++--- Text.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Items.py b/Items.py index f9323e46..52a399fc 100644 --- a/Items.py +++ b/Items.py @@ -134,9 +134,9 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'Bow!\nJoin the archer class 'Big Key (Agahnims Tower)': (False, False, 'BigKey', 0x9B, 60, 'A big key for\nAgahnim\'s Tower', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Castle Tower'), 'Compass (Agahnims Tower)': (False, True, 'Compass', 0x8B, 10, 'A compass for\nAgahnim\'s Tower', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds null again', 'a compass to Castle Tower'), 'Map (Agahnims Tower)': (False, True, 'Map', 0x7B, 10, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Castle Tower'), - 'Small Key (Palace of Darkness)': (False, False, 'SmallKey', 0xA6, 40, 'A small key for\nDark Palace', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Palace of Darkness'), - 'Big Key (Palace of Darkness)': (False, False, 'BigKey', 0x99, 60, 'A big key for\nDark Palace', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Palace of Darkness'), - 'Compass (Palace of Darkness)': (False, True, 'Compass', 0x89, 10, 'A compass for\nDark Palace', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Palace of Darkness'), + 'Small Key (Palace of Darkness)': (False, False, 'SmallKey', 0xA6, 40, 'A small key for\nPalace of Darkness', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Palace of Darkness'), + 'Big Key (Palace of Darkness)': (False, False, 'BigKey', 0x99, 60, 'A big key for\nPalace of Darkness', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Palace of Darkness'), + 'Compass (Palace of Darkness)': (False, True, 'Compass', 0x89, 10, 'A compass for\nPalace of Darkness', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Palace of Darkness'), 'Map (Palace of Darkness)': (False, True, 'Map', 0x79, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Palace of Darkness'), 'Small Key (Thieves Town)': (False, False, 'SmallKey', 0xAB, 40, 'A small key for\nThieves Town', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Thieves\' Town'), 'Big Key (Thieves Town)': (False, False, 'BigKey', 0x94, 60, 'A big key for\nThieves Town', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Thieves\' Town'), diff --git a/Text.py b/Text.py index 31c8491a..261d7fed 100644 --- a/Text.py +++ b/Text.py @@ -1766,7 +1766,7 @@ class TextTable(object): text['sign_catfish'] = CompressedTextMapper.convert("Toss rocks\nToss items\nToss cookies") text['sign_north_village_of_outcasts'] = CompressedTextMapper.convert("↑ Skull Woods\n\n↓ Steve's Town") text['sign_south_of_bumper_cave'] = CompressedTextMapper.convert("\n→ Karkats cave") - text['sign_east_of_pyramid'] = CompressedTextMapper.convert("\n→ Dark Palace") + text['sign_east_of_pyramid'] = CompressedTextMapper.convert("\n→ Palace of Darkness") text['sign_east_of_bomb_shop'] = CompressedTextMapper.convert("\n← Bomb Shoppe") text['sign_east_of_mire'] = CompressedTextMapper.convert("\n← Misery Mire\n no way in.\n no way out.") text['sign_village_of_outcasts'] = CompressedTextMapper.convert("Have a Trulie Awesome Day!") From 2ff19b37816ecdf990f3d72ecabb10dda4ec6a4d Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Wed, 27 May 2026 23:16:45 -0500 Subject: [PATCH 68/76] Update baserom --- Rom.py | 14 +++++++------- data/base2current.bps | Bin 157515 -> 157834 bytes 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Rom.py b/Rom.py index b476ba23..0d1a2c4a 100644 --- a/Rom.py +++ b/Rom.py @@ -85,7 +85,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc from Versions import DRVersion, GKVersion, ORVersion JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '100c3e1da68680a0f3d8e1fc94568de6' +RANDOMIZERBASEHASH = '52dd9045023e436a59bf75652f757c1d' class JsonRom(object): @@ -1504,21 +1504,21 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): rom.write_byte(0x1CFF10, loot_source) if world.loothud[player] == 'never': - rom.write_byte(0x1CFF12, 0x00) + rom.write_byte(0x1CFF13, 0x00) elif world.loothud[player] == 'presence': - rom.write_byte(0x1CFF12, 0x01) + rom.write_byte(0x1CFF13, 0x01) rom.write_bytes(0x1CFF0E, [0x01, 0x01]) elif world.loothud[player] == 'value': - rom.write_byte(0x1CFF12, 0x01) + rom.write_byte(0x1CFF13, 0x01) rom.write_bytes(0x1CFF0E, [0xFF, 0xFF]) elif world.loothud[player] == 'dungeon_value': - rom.write_byte(0x1CFF12, 0x01) + rom.write_byte(0x1CFF13, 0x01) rom.write_bytes(0x1CFF0E, [0xFF, 0x01]) if world.showloot[player] == 'never': - rom.write_bytes(0x1CFF08, [0x00, 0x00, 0x00, 0x00]) + rom.write_bytes(0x1CFF08, [0x02, 0x00, 0x00, 0x00]) rom.write_byte(0x1CFF11, 0x00) - rom.write_byte(0x1CFF12, 0x00) # turn off hud icon too just to be safe + rom.write_byte(0x1CFF12, 0x00) elif world.showloot[player] == 'presence': rom.write_bytes(0x1CFF08, [0x01, 0x00, 0x00, 0x00]) rom.write_byte(0x1CFF11, 0x00) diff --git a/data/base2current.bps b/data/base2current.bps index 40f9a6fd5ab4aa8cf85a585d073e84e03f13e632..89a3172864b6a37fc46f3aa98c5db520faab370f 100644 GIT binary patch delta 14837 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 Z9l?{#I*0$>K&J@CY*-!X_R#C~{{fRdi8}xQ delta 14518 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 Date: Thu, 28 May 2026 06:30:13 -0500 Subject: [PATCH 69/76] Update baserom --- Rom.py | 2 +- data/base2current.bps | Bin 157834 -> 157835 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index 0d1a2c4a..268719e7 100644 --- a/Rom.py +++ b/Rom.py @@ -85,7 +85,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc from Versions import DRVersion, GKVersion, ORVersion JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '52dd9045023e436a59bf75652f757c1d' +RANDOMIZERBASEHASH = '9086f874ec9ae0d2f581e9bf0b43b60d' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 89a3172864b6a37fc46f3aa98c5db520faab370f..ab0165fcb5e4779bf937b0389a4903948d4edf03 100644 GIT binary patch delta 90 zcmV-g0Hyzm(Fu#u39yU<13HPtgO3A;j{^a>j{^dAVh{-j1(zj2EP$B?1qB5H3J8}J wW&$e^n+aN!vo>$nyHr~4w^c&!w@zjPP5}f~EmtX*pl1T&2&3Uiktz0#2v^%5bpQYW delta 89 zcmV-f0H*(o(Fuyt39yU<13-ztgO3A;j{^a>j{^dAVh{re1(zj2EP$B?1qB5H3I~@I vW&$e@mk?IzvvL^6zjs>hw<4FfPi6v60R&emRxOvIX9D5~HfCQb584&&F3cVA From f5dde931dc0b589e5898899b01b55f0e1fd43cf3 Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Thu, 28 May 2026 17:20:17 -0500 Subject: [PATCH 70/76] cave pots show on HUD --- Rom.py | 4 ++-- data/base2current.bps | Bin 157835 -> 157837 bytes 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Rom.py b/Rom.py index 268719e7..c374e1f3 100644 --- a/Rom.py +++ b/Rom.py @@ -85,7 +85,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc from Versions import DRVersion, GKVersion, ORVersion JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '9086f874ec9ae0d2f581e9bf0b43b60d' +RANDOMIZERBASEHASH = '44a806e1c56abb7e68266e345577badc' class JsonRom(object): @@ -1497,7 +1497,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): loot_source = 0x09 if world.prizeshuffle[player] != 'none': loot_source |= 0x10 - if world.pottery[player] not in ['none', 'cave']: + if world.pottery[player] != 'none': loot_source |= 0x02 if world.dropshuffle[player] != 'none': loot_source |= 0x04 diff --git a/data/base2current.bps b/data/base2current.bps index ab0165fcb5e4779bf937b0389a4903948d4edf03..939d20bf44f91ab3f65a2a9c47b0cfb6ad96061f 100644 GIT binary patch delta 67 zcmV-J0KET;(Fu*w39yU<1bc{tcY}}vw~zw?px6OnmoeM{%m#=q1w}-Bw*lP&b^!<% Z&@6ZdfVW1sf8GHEiU@O{%~IQiUzvvU8rc8< delta 65 zcmV-H0KWf?(Fu#u39yU<1UiYuc7u=uw~zw?px6OfmoeM{%mjum1xU98-2rw12p`Za Xcn5&DLbrk50R)N&qv1)BDfW#BQjQs* From 8451a6498473cf51e8dedc51317a36321327dfe7 Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Thu, 28 May 2026 18:20:57 -0500 Subject: [PATCH 71/76] Update baserom --- Rom.py | 2 +- data/base2current.bps | Bin 157837 -> 157838 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index c374e1f3..3224d281 100644 --- a/Rom.py +++ b/Rom.py @@ -85,7 +85,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc from Versions import DRVersion, GKVersion, ORVersion JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '44a806e1c56abb7e68266e345577badc' +RANDOMIZERBASEHASH = 'e6d4fd01beef37b55e26d7c4479624d3' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 939d20bf44f91ab3f65a2a9c47b0cfb6ad96061f..3e1bc0d7580b65c718ea8b3bae6189635dff9bd8 100644 GIT binary patch delta 54 zcmV-60LlN2(Fu;x34nwFv;uc!0ezQvWdbo0=qc!F_$l~l&MD6+-YMTHpE|d?Wdh3r M2oz^nn&@9 Date: Thu, 28 May 2026 21:03:10 -0500 Subject: [PATCH 72/76] Zora/Bottle Vendor ask about item before taking money when hints are on --- OverworldGlitchRules.py | 4 ++-- Rom.py | 11 ++++++++++- data/base2current.bps | Bin 157838 -> 157832 bytes patches/2way_mirror.ips | Bin 0 -> 14 bytes patches/quiet_zora.ips | Bin 0 -> 16 bytes 5 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 patches/2way_mirror.ips create mode 100644 patches/quiet_zora.ips diff --git a/OverworldGlitchRules.py b/OverworldGlitchRules.py index c01a53bc..8daa41cf 100644 --- a/OverworldGlitchRules.py +++ b/OverworldGlitchRules.py @@ -509,7 +509,7 @@ boots_clips = [ mirror_clips_local = [ ('Desert East Mirror Clip', 'Mire Area', 'Desert Mouth'), - ('EDDM Mirror Clip', 'East Dark Death Mountain (Bottom Left)', 'East Dark Death Mountain (Bottom)'), + ('EDDM Bridge Mirror Clip', 'East Dark Death Mountain (Bottom Left)', 'East Dark Death Mountain (Bottom)'), ('EDDM Mirror Clip', 'East Dark Death Mountain (Top)', 'Dark Death Mountain Ledge') ] @@ -520,4 +520,4 @@ mirror_clips = [ mirror_offsets = [ (['DM Offset Mirror', 'DDM Offset Mirror'], ['West Death Mountain (Bottom)', 'West Dark Death Mountain (Bottom)'], ['Hyrule Castle Courtyard Northeast', 'Pyramid Crack'], ['Pyramid Area', 'Hyrule Castle Courtyard']), (['DM To HC Ledge Offset Mirror', 'DDM To Pyramid Offset Mirror'], ['West Death Mountain (Bottom)', 'West Dark Death Mountain (Bottom)'], ['Hyrule Castle Ledge', 'Pyramid Area'], ['Pyramid Area', 'Hyrule Castle Area']) -] \ No newline at end of file +] diff --git a/Rom.py b/Rom.py index 3224d281..6d1c87c7 100644 --- a/Rom.py +++ b/Rom.py @@ -85,7 +85,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc from Versions import DRVersion, GKVersion, ORVersion JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'e6d4fd01beef37b55e26d7c4479624d3' +RANDOMIZERBASEHASH = '5c4c3cbe6d3fee849e66d4ac4a059792' class JsonRom(object): @@ -2485,6 +2485,15 @@ def write_strings(rom, world, player, team, is_mystery=False): # For hints, first we write hints about entrances, some from the inconvenient list others from all reasonable entrances. if world.hints[player]: + zoraitem = world.get_location('King Zora', player).item.hint_text + if len(zoraitem) <= 15: + tt['zora_meeting'] = f"Whaddaya want?\n ≥ {zoraitem.title()}\n Nothin'{{CHOICE}}" + else: + tt['zora_meeting'] = f"Do you want {zoraitem}?\n ≥ I'll pay\n No thanks{{CHOICE}}" + + bottleitem = world.get_location('Bottle Merchant', player).item.hint_text + tt['bottle_vendor_choice'] = f"Do you want {bottleitem}?\n ≥ I'll take it\n No thanks!\n{{CHOICE}}" + tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!' hint_locations = HintLocations.copy() random.shuffle(hint_locations) diff --git a/data/base2current.bps b/data/base2current.bps index 3e1bc0d7580b65c718ea8b3bae6189635dff9bd8..14ff761359a43f387a1ef70545c95db2549d6463 100644 GIT binary patch delta 56 zcmeCX$k}m`b3+d^b7ejSQO$8YU|@PFHH2{!4{Xjd4M{k}BhN SB~_+4XAY6{h*>Y<(}Dr+O%|X4 diff --git a/patches/2way_mirror.ips b/patches/2way_mirror.ips new file mode 100644 index 0000000000000000000000000000000000000000..6e908386ff2134190b347a5202471a3072fede89 GIT binary patch literal 14 VcmWG=3~}~gUg^xh*x>5#1^^sP1G@kK literal 0 HcmV?d00001 diff --git a/patches/quiet_zora.ips b/patches/quiet_zora.ips new file mode 100644 index 0000000000000000000000000000000000000000..3e5a6364424255fb30e063045a1d1c0b22e19669 GIT binary patch literal 16 XcmWG=3~}~g)Z5L#z`*><)!z*OA`%25 literal 0 HcmV?d00001 From 2055ed6d96dd1a73f23c08e5fcf2212b17340cbb Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Fri, 29 May 2026 19:21:17 -0500 Subject: [PATCH 73/76] Minor gfx/patch updates --- Rom.py | 8 ++++++-- patches/all_starting.ips | Bin 96 -> 102 bytes 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Rom.py b/Rom.py index 6d1c87c7..870a8a91 100644 --- a/Rom.py +++ b/Rom.py @@ -1007,6 +1007,10 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): rom.write_bytes(0x6D31B, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E]) rom.write_bytes(0x6D323, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E]) + bridge_item = world.get_location("Hobo", player).item + if bridge_item is None or not bridge_item.name.startswith("Bottle"): + rom.write_bytes(0x1E9C0, [0xFB, 0xFF, 0x03, 0x00, 0xAB, 0x00, 0x00, 0x00]) + # set light cones if world.dark_rooms[player] == 'no_dark_rooms': light_cone = 0x20 @@ -1779,7 +1783,7 @@ 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) - if uncle_location.item is None or uncle_location.item.name not in ['Sword and Shield']: + if uncle_location.item is None or uncle_location.item.name not in ['Blue Shield', 'Red Shield', 'Mirror Shield', 'Progressive Shield', '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]) @@ -2971,7 +2975,7 @@ def write_strings(rom, world, player, team, is_mystery=False): + "{PAUSE3} {CHANGEPIC}\nGanon has moved around all the items in Hyrule." + "{PAUSE7}\nYou will have to find all the items necessary to achieve your goal." + "{PAUSE7}\nThis is your chance to be a hero." - + "{PAUSE3} {CHANGEPIC}\nYou must determine and achieve your goal." + + "{PAUSE3} {CHANGEPIC}\nGood luck out there, and try not to die." + "{PAUSE9} {CHANGEPIC}", False) elif world.mode[player] == 'inverted': tt['intro_main'] = CompressedTextMapper.convert( diff --git a/patches/all_starting.ips b/patches/all_starting.ips index d3333b8e533092b1ca416c687fbea7e5ee920409..861f9e727aa1e06eeeb9e3b8b35d778e559cf377 100644 GIT binary patch delta 14 VcmYdDn-I$>VIt1J$l~hn1^^m%0{;L2 delta 8 PcmYdGm=MeC>hA^s49EhJ From 444ebda072df730f491e2c435c18c0327e6e6864 Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Mon, 1 Jun 2026 18:00:38 -0500 Subject: [PATCH 74/76] Up triforce count before triforces become lower priority --- Rom.py | 2 +- patches/colordorm.ips | Bin 0 -> 170 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 patches/colordorm.ips diff --git a/Rom.py b/Rom.py index 870a8a91..476e1199 100644 --- a/Rom.py +++ b/Rom.py @@ -1545,7 +1545,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): rom.write_byte(loot_icons + 0x52, 0x0B) # bomb bag is major triforce_piece_ids = [0x6B, 0x6C] - if world.treasure_hunt_count[player] > 20: + if world.treasure_hunt_count[player] > 100: for triforce_piece_id in triforce_piece_ids: rom.write_byte(loot_icons + triforce_piece_id, 0x04) diff --git a/patches/colordorm.ips b/patches/colordorm.ips new file mode 100644 index 0000000000000000000000000000000000000000..0b165cfc39c948b7a4ac1e0049bf9b43ff754624 GIT binary patch literal 170 zcmWG=3~~10yV1(P$jWz9f`O5Z@1_En)BuwPVA6tt(T(q>1DNyxlL25d0xX*VCNscf z0hp`+%Qk??4lp?ZOzvf1 Date: Wed, 3 Jun 2026 20:52:25 -0500 Subject: [PATCH 75/76] Separate map and hud loot icons --- Rom.py | 37 +++++++++++++++++++----------------- data/base2current.bps | Bin 157832 -> 157910 bytes resources/app/cli/args.json | 3 ++- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/Rom.py b/Rom.py index 476e1199..926dd74b 100644 --- a/Rom.py +++ b/Rom.py @@ -85,7 +85,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc from Versions import DRVersion, GKVersion, ORVersion JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '5c4c3cbe6d3fee849e66d4ac4a059792' +RANDOMIZERBASEHASH = 'cd81a8bfc1d67c6a13fe806c7f35a15f' class JsonRom(object): @@ -1505,33 +1505,36 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): loot_source |= 0x02 if world.dropshuffle[player] != 'none': loot_source |= 0x04 - rom.write_byte(0x1CFF10, loot_source) + rom.write_byte(0x1CFF20, loot_source) if world.loothud[player] == 'never': - rom.write_byte(0x1CFF13, 0x00) + rom.write_bytes(0x1CFF10, [0x00, 0x00, 0x00, 0x00]) + rom.write_byte(0x1CFF17, 0x00) elif world.loothud[player] == 'presence': - rom.write_byte(0x1CFF13, 0x01) - rom.write_bytes(0x1CFF0E, [0x01, 0x01]) + rom.write_bytes(0x1CFF10, [0x01, 0x01, 0x00, 0x00]) + rom.write_byte(0x1CFF17, 0x01) elif world.loothud[player] == 'value': - rom.write_byte(0x1CFF13, 0x01) - rom.write_bytes(0x1CFF0E, [0xFF, 0xFF]) + rom.write_bytes(0x1CFF10, [0x03, 0x03, 0x00, 0x00]) + rom.write_byte(0x1CFF17, 0x01) elif world.loothud[player] == 'dungeon_value': - rom.write_byte(0x1CFF13, 0x01) - rom.write_bytes(0x1CFF0E, [0xFF, 0x01]) + rom.write_bytes(0x1CFF10, [0x01, 0x03, 0x00, 0x00]) + rom.write_byte(0x1CFF17, 0x01) + elif world.loothud[player] == 'cave_value': + rom.write_bytes(0x1CFF10, [0x03, 0x01, 0x00, 0x00]) + rom.write_byte(0x1CFF17, 0x01) if world.showloot[player] == 'never': - rom.write_bytes(0x1CFF08, [0x02, 0x00, 0x00, 0x00]) - rom.write_byte(0x1CFF11, 0x00) - rom.write_byte(0x1CFF12, 0x00) + rom.write_bytes(0x1CFF08, [0x00, 0x00, 0x00, 0x00]) + rom.write_byte(0x1CFF0F, 0x00) elif world.showloot[player] == 'presence': rom.write_bytes(0x1CFF08, [0x01, 0x00, 0x00, 0x00]) - rom.write_byte(0x1CFF11, 0x00) + rom.write_byte(0x1CFF0F, 0x01) elif world.showloot[player] == 'compass': - rom.write_bytes(0x1CFF08, [0x01, 0x00, 0x02, 0x00]) - rom.write_byte(0x1CFF11, 0x01) + rom.write_bytes(0x1CFF08, [0x02, 0x00, 0x03, 0x00]) + rom.write_byte(0x1CFF0F, 0x01) elif world.showloot[player] == 'always': - rom.write_bytes(0x1CFF08, [0x02, 0x00, 0x00, 0x00]) - rom.write_byte(0x1CFF11, 0x00) + rom.write_bytes(0x1CFF08, [0x03, 0x00, 0x00, 0x00]) + rom.write_byte(0x1CFF0F, 0x01) if world.showmap[player] == 'visited': rom.write_bytes(0x1CFF00, [0x01, 0x00, 0x00, 0x05]) diff --git a/data/base2current.bps b/data/base2current.bps index 14ff761359a43f387a1ef70545c95db2549d6463..76f9c6d626cb628677fb5deed8b182fe543b7667 100644 GIT binary patch delta 2320 zcmW-gc~nzZ9>?#y2@nE=C|VE|A1Nq{bs}{^1Pq7>3KF*}jA9f<5!|ZPK}7Oi@L9tm zgeP1fVpIqTi7`=X<5Sx>oC6x9PMFRamvJjqhjC4-&Y5D5nVb3N`}wVRdB6LP4Eg_K z$lq4SFR0Z)$n|G<#ns4aqJYoG~R|C#-1X-*97sPXNCX1F~o3>GyGpdUxs+o zaJ!YYN6TPRUC(dPc?5B}Cwp4R_$j5`-1BQ{$%$_3_Ibw6ddc%{ZY^CedEd=Vr#0(n z_CRtCT-n{@my)vqFShE$zEzW%8C_&PDBx{z0Wjpx_&@$$$1%as{Sp-;%R%EH|bfBk;=>XY(v zAOPdYS>-tke?Jsdo+Xuv1>!(y;4aa=n1#t=5N02~PiP4LKm2uixgTYpF&I}%K&C<*G~%ogdmzCi8bad*!DGBWa=*(P>;h2^t&M7wBQw*6~1y3v32& z_kh5&oY)foUB8_=BW^^`{52i#&*)7Skt zD9bU$#!-UqVQg(gzNyd}cgDg|Vn;Prq2nl4z5`kQe=P-)WVWKQTRx*Wc;f~aW|R{RZkE_%NkV7s6v~eVr=Gy zLz-e-cY)2KG@G&U4Q?#K{1&ql^Actk=4G~qHr`C3Xkb^aq|`u{VJ@KP4ECAEL@2pT z_KD^!W)`k~j=2Q$C1wumLu=x2`!cqco^E(%-e27lNHqhU3Mfal6)JiXyMWd_#|06r zZ?)znHb_=kt%>6)ISE^ISCYYj1I}TD-eEKlu zQ!CdB1Xg^9Fy%9Je-Vr$nvq{7j3rvo%uE;>g4m2Jt>JyJaf0JG`n6#(O}EP#Qe6N^0$h$&Yy~*UWBLR=1bn4;Yt|MXpA0~33{cq>@?%WVrV5joSFb` z9+}<2rkY!!*EDsqM`=EOz7=}*&}jj1`;sjkCr$QoDq;} zwDCtvD4h-zA)Xv2&Dh(eFkE2gZgrt`n_cZ%J3pM^cN@)LpWHr`xtNpMW#`9&$4>Cr z9ex8XScb~vP`*vHJcg=$b9$Z8<}+H7)2`PQQB27ub|0nTdtL33Zmf!XCTBEo3)6KGcs8E%K8Pwm5bpv+@+1SNKRkP85PNbN=TWI>GXNFZF zL-gO@s+cgt8;)N0_Ivu&P1X5(v4HmiKlVGvDP~X?@WKAoTPAEoySKMKde9ZvF{fYs zf&vX$#rXk4F`ky&>2YPg&T zP*g~$wyN01j)hav(=T8etSmq=Yv2=6*_`WaKopN+#h) z;x1z{8KTI$x-_bJjI#7~dVfPB)AXNjr@deNsr`B(Oz;oaZj(2$vULqjk^>j4c}X2D j%KU~cK8C>jOe89S(|Bz7` delta 2287 zcmW-he^e7!7RP5^0we(;^5cM5HKZUvM2(1wN+cj4_MqUeN>PytRS>l5?z*68-k{SO z5W_IVw~%5~2q6O)l+%XMT}P>)LDbN*$76Bzu-Lk+KWO~9m37U&^pClp@BPlb@7$R; zZ|)9Bj}1w?j*Gypu6R+N98JUzJ^jUc*v5aEnh2ob1YfMHb59LBM zjxr%0N3o#p{QaTxkutpj`R|wwd2r(ff4KuqN&?}LvwNH}DIs4;5XLL6!~9%xQQC;9 z*L4^cFL5aNL*^>x1Gcp&Ckd!y=X9(j!6M>rncuo`U$5G zfDA`!`Dbh%#dKgXj_aRbIx(MNzGSj)%rh+d+1Zs$j7ZDhV5^x}%mMZSvlDZWt)@#w*AGL)YJfc~veUgoJ|?_f zd~hc_^_wRA6$H3VL&4{-hJNTkhmt{vY=%&IV#P*C^EZ((8KkUj3{P&UFK!{42uUl) z6sHnRB+5$zP*A76seer5pYstLRB_G*2boX%o>dt(ujsD|gH0X-1M(O7ql&s^bv2@1 zMSgqvo<;DVy$Ye#g65}y1@cx4Px}gH9Q{sMa@)WReRVv5JSKD3A0-eSY9__~o z4vyyr#)LMGS7i;$IALDz;R!pTdhT{;63)>n9DeS)`#2?pyBrT$jgk6=tz>l3ZMsQj7kA;ItbBy8Gr{C- z7uUvd;X9iTK`S0NL|bj;8wiON4;`XCj%4W|hFppkq=RTO5p7Ke!2xPE^-@DdVYwxa>KRbmT!qCxCdJ|^~rNkTY16;aKKnr58H-F9!Y;(asw z<%a5Ncg;!s*_X>0%b z)UKKIcbN{8O&m>(*ohHm=nYhmiEd_tY;9at6s-RB%p-G`$3#))T$8Z?(nZ_Y{g4s6 zQ^PR}j`_o!J%Vsb8fU3HbyDkOqsCzwF079psC+k50C`dwC8-NDLD-g2*#83Ko$7>|x zY%=w0v@6~A;|8#X^o}i8!p6hVwatDJXwF8E3^G1LdpClgeHSgb&U%N7Ae{|_^deh9 z4p>Km6`!J>&%id2x&eKZ3;qGNmZFJVFcqYi+JZNMDWb{$>)|(b$5Z rU197nC6!Uhd#}wEn-3g}Lz|00q)4@@2YpopG9-7aHLL2&61M*zgB$B- diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index a728cb10..0918413b 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -491,7 +491,8 @@ "never", "presence", "value", - "dungeon_value" + "dungeon_value", + "cave_value" ] }, "showmap": { From 16445355570a7041a175e636a36459606114f544 Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Sat, 6 Jun 2026 12:36:17 -0500 Subject: [PATCH 76/76] Update baserom --- Rom.py | 2 +- data/base2current.bps | Bin 157910 -> 158036 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index 926dd74b..d3c39520 100644 --- a/Rom.py +++ b/Rom.py @@ -85,7 +85,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc from Versions import DRVersion, GKVersion, ORVersion JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'cd81a8bfc1d67c6a13fe806c7f35a15f' +RANDOMIZERBASEHASH = 'c2ce7e227a019e083a173267daac828c' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 76f9c6d626cb628677fb5deed8b182fe543b7667..0de72d311c7ad235aa7d7e9a75559c495420c3cc 100644 GIT binary patch delta 3404 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