From 6162fddf47cfdf69dfaeb749a4762367a67962ab Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 9 Nov 2020 13:54:03 -0700 Subject: [PATCH] Lots of bugfixes - see notes --- BaseClasses.py | 8 +++++--- DoorShuffle.py | 8 ++++++++ DungeonGenerator.py | 6 ++++++ ItemList.py | 20 +++++++++++++++++++- Main.py | 7 +++++-- RELEASENOTES.md | 37 ++++++++++++++++++++++++++----------- Rom.py | 24 ++++++++++++++++++++++-- asm/drhooks.asm | 8 ++++++++ asm/keydropshuffle.asm | 20 ++++++++++++++++---- asm/overrides.asm | 15 ++++++++++++++- data/base2current.bps | Bin 130766 -> 130841 bytes 11 files changed, 129 insertions(+), 24 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 0ba6b8a7..cc6f9b61 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -476,7 +476,7 @@ class CollectionState(object): new_crystal_state = crystal_state for exit in new_region.exits: door = exit.door - if door is not None and door.crystal == CrystalBarrier.Either: + if door is not None and door.crystal == CrystalBarrier.Either and door.entrance.can_reach(self): new_crystal_state = CrystalBarrier.Either break if new_region in rrp: @@ -487,7 +487,7 @@ class CollectionState(object): for exit in new_region.exits: door = exit.door if door is not None and not door.blocked: - door_crystal_state = new_crystal_state & (door.crystal or CrystalBarrier.Either) + door_crystal_state = door.crystal if door.crystal else new_crystal_state bc[exit] = door_crystal_state queue.append((exit, door_crystal_state)) elif door is None: @@ -1906,7 +1906,8 @@ class Spoiler(object): 'enemy_damage': self.world.enemy_damage, 'players': self.world.players, 'teams': self.world.teams, - 'experimental' : self.world.experimental + 'experimental': self.world.experimental, + 'keydropshuffle': self.world.keydropshuffle, } def to_json(self): @@ -1967,6 +1968,7 @@ class Spoiler(object): outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player]) outfile.write('Hints: %s\n' % ('Yes' if self.metadata['hints'][player] else 'No')) outfile.write('Experimental: %s\n' % ('Yes' if self.metadata['experimental'][player] else 'No')) + outfile.write('Key Drops shuffled: %s\n' % ('Yes' if self.metadata['keydropshuffle'][player] else 'No')) if self.doors: outfile.write('\n\nDoors:\n\n') outfile.write('\n'.join( diff --git a/DoorShuffle.py b/DoorShuffle.py index 64a3f19f..d50220e0 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -40,6 +40,8 @@ def link_doors(world, player): connect_two_way(world, entrance, ext, player) for entrance, ext in straight_staircases: connect_two_way(world, entrance, ext, player) + + connect_custom(world, player) find_inaccessible_regions(world, player) @@ -197,6 +199,12 @@ def convert_key_doors(k_doors, world, player): return result +def connect_custom(world, player): + if hasattr(world, 'custom_doors') and world.custom_doors[player]: + for entrance, ext in world.custom_doors[player]: + connect_two_way(world, entrance, ext, player) + + def connect_simple_door(world, exit_name, region_name, player): region = world.get_region(region_name, player) world.get_entrance(exit_name, player).connect(region) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 47cc9a79..17e1bd41 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -564,6 +564,8 @@ def determine_paths_for_dungeon(world, player, all_regions, name): paths.append(('Hyrule Dungeon Cellblock', 'Sanctuary')) if world.doorShuffle[player] in ['basic'] and name == 'Thieves Town': paths.append('Thieves Attic Window') + elif 'Thieves Attic Window' in all_r_names: + paths.append('Thieves Attic Window') for boss in boss_path_checks: if boss in all_r_names: paths.append(boss) @@ -1260,6 +1262,9 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, sector = find_sector(r_name, all_sectors) reverse_d_map[sector] = key + complete_dungeons = {x: y for x, y in dungeon_map.items() if sum(len(sector.outstanding_doors) for sector in y.sectors) <= 0} + [dungeon_map.pop(key) for key in complete_dungeons.keys()] + # categorize sectors identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map, connections, dungeon_entrances, split_dungeon_entrances) @@ -1315,6 +1320,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, builder_info) # the rest assign_the_rest(dungeon_map, neutral_sectors, global_pole, builder_info) + dungeon_map.update(complete_dungeons) finished = True except NeutralizingException: pass diff --git a/ItemList.py b/ItemList.py index 7557c7a3..3414ed60 100644 --- a/ItemList.py +++ b/ItemList.py @@ -6,7 +6,7 @@ from BaseClasses import Region, RegionType, Shop, ShopType, Location from Bosses import place_bosses from Dungeons import get_dungeon_item_pool from EntranceShuffle import connect_entrance -from Fill import FillError, fill_restrictive +from Fill import FillError, fill_restrictive, fast_fill from Items import ItemFactory import source.classes.constants as CONST @@ -738,3 +738,21 @@ def test(): if __name__ == '__main__': test() + + +def fill_specific_items(world): + keypool = [item for item in world.itempool if item.smallkey] + cage = world.get_location('Tower of Hera - Basement Cage', 1) + c_dungeon = cage.parent_region.dungeon + key_item = next(x for x in keypool if c_dungeon.name in x.name or (c_dungeon.name == 'Hyrule Castle' and 'Escape' in x.name)) + world.itempool.remove(key_item) + all_state = world.get_all_state(True) + fill_restrictive(world, all_state, [cage], [key_item]) + + # somaria = next(item for item in world.itempool if item.name == 'Cane of Somaria') + # shooter = world.get_location('Palace of Darkness - Shooter Room', 1) + # world.itempool.remove(somaria) + # all_state = world.get_all_state(True) + # fill_restrictive(world, all_state, [shooter], [somaria]) + + diff --git a/Main.py b/Main.py index b921812e..963c6c16 100644 --- a/Main.py +++ b/Main.py @@ -21,10 +21,10 @@ from RoomData import create_rooms from Rules import set_rules from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items, balance_multiworld_progression -from ItemList import generate_itempool, difficulties, fill_prizes +from ItemList import generate_itempool, difficulties, fill_prizes, fill_specific_items from Utils import output_path, parse_player_names -__version__ = '0.2.0.7-u' +__version__ = '0.2.0.8-u' class EnemizerError(RuntimeError): pass @@ -139,6 +139,9 @@ def main(args, seed=None, fish=None): fill_prizes(world) + # used for debugging + # fill_specific_items(world) + logger.info(world.fish.translate("cli","cli","placing.dungeon.items")) shuffled_locations = None diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d75fef8a..92f602f7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,14 +3,19 @@ * Lobby shuffle added as Intensity level 3 * Can now be found in the spoiler * Known issues: + * If a dungeon is vanilla in ER and Sanc is in that dungeon and the dungeon has an entrance that needs to let link out: is broken. + * e.g. PoD, GT, TR + * Some TR lobbies that need a bomb aren't pre-opened. * Palettes aren't perfect - may add Sanctuary and Sewer palette back. May add a way to turn off palette "fixing" - * Certain hints in ER due to lobby changes + * Some ugly colors + * Invisible floors can be see in many palettes * Animated tiles aren't loaded correctly in lobbies * If a wallmaster grabs you and the lobby is dark, the lamp doesn't turn on * --keydropshuffle added (coming to the GUI soon). This add 33 new locations to the game where keys are found under pots and where enemies drop keys. This includes 32 small key location and the ball and chain guard who normally drop the HC Big Key. - * Multiworld untested - May need changes to MultiClient/MultiServer to recognize new locations + * Overall location count updated + * Setting mentioned in spoiler * GT Big Key count / total location count needs to be updated * --mixed_travel setting added * Due to Hammerjump, Hovering in PoD Arena, and the Mire Big Key Chest bomb jump two sections of a supertile that are @@ -30,14 +35,24 @@ otherwise unconnected logically can be reach using these glitches. To prevent th # Bug Fixes -* Fixed a situation where logic did not account properly for Big Key doors in standard Hyrule Castle -* Fixed a problem ER shuffle generation that did not account for lobbies moving around -* Fixed a problem with camera unlock (GT Mimics and Mire Minibridge) -* Fixed a problem with bad-pseudo layer at PoD map Balcony (unable to hit switch with Bomb) -* Fixed a problem with the Ganon hint when hints are turned off +* 2.0.8-u + * Player sprite disappears after picking up a key drop in keydropshuffle + * Sewers and Hyrule Castle compass problems + * Double count of the Hera Basement Cage item (both overall and compass) + * Unnecessary/inconsistent rug cutoff + * TR Crystal Maze thought you get through backwards without Somaria + * Ensure Thieves Attic Window area can always be reached + * Fixed where HC big key was not counted +* Prior fixes + * Fixed a situation where logic did not account properly for Big Key doors in standard Hyrule Castle + * Fixed a problem ER shuffle generation that did not account for lobbies moving around + * Fixed a problem with camera unlock (GT Mimics and Mire Minibridge) + * Fixed a problem with bad-pseudo layer at PoD map Balcony (unable to hit switch with Bomb) + * Fixed a problem with the Ganon hint when hints are turned off -# Known Issues +# Known Issues -(I'm planning to fix theese in this Unstable iteration hopefully) - -* Backward TR Crystal Maze locking Somaria \ No newline at end of file +* Multiworld = /missing command not working +* Potenial keylocks in multi-entrance dungeons +* Incorrect vanilla keylogic for Mire +* ER - Potential for Skull Woods West to be completely inaccessible in non-beatable logic \ No newline at end of file diff --git a/Rom.py b/Rom.py index 4b45ffe9..9270200a 100644 --- a/Rom.py +++ b/Rom.py @@ -24,7 +24,7 @@ from EntranceShuffle import door_addresses, exit_ids JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '78947c3825898cac3ab57cbe44b50390' +RANDOMIZERBASEHASH = 'fb2886fc00a7736369ce6ba90b526bc9' class JsonRom(object): @@ -709,8 +709,21 @@ def patch_rom(world, rom, player, team, enemized): write_custom_shops(rom, world, player) + def credits_digit(num): + # top: $54 is 1, 55 2, etc , so 57=4, 5C=9 + # bot: $7A is 1, 7B is 2, etc so 7D=4, 82=9 (zero unknown...) + return 0x53+num, 0x79+num + if world.keydropshuffle[player]: rom.write_byte(0x140000, 1) + mid_top, mid_bot = credits_digit(4) + last_top, last_bot = credits_digit(9) + # top half + rom.write_byte(0x118C53, mid_top) + rom.write_byte(0x118C54, last_top) + # bottom half + rom.write_byte(0x118C71, mid_bot) + rom.write_byte(0x118C72, last_bot) # patch medallion requirements if world.required_medallions[player][0] == 'Bombos': @@ -765,7 +778,7 @@ def patch_rom(world, rom, player, team, enemized): TRIFORCE_PIECE = ItemFactory('Triforce Piece', player).code GREEN_CLOCK = ItemFactory('Green Clock', player).code - rom.write_byte(0x18004F, 0x01) # Byrna Invulnerability: on + rom.write_byte(0x18004F, 0x01) # Byrna Invulnerability: on # handle difficulty_adjustments if world.difficulty_adjustments[player] == 'hard': @@ -2210,11 +2223,18 @@ def patch_shuffled_dark_sanc(world, rom, player): def update_compasses(rom, world, player): layouts = world.dungeon_layouts[player] + provided_dungeon = False for name, builder in layouts.items(): dungeon_id = compass_data[name][4] rom.write_byte(0x187000 + dungeon_id//2, builder.location_cnt) if builder.bk_provided: + if provided_dungeon: + logging.getLogger('').warning('Multiple dungeons have forced BKs! Compass code might need updating?') rom.write_byte(0x186FFF, dungeon_id) + provided_dungeon = True + if not provided_dungeon: + rom.write_byte(0x186FFF, 0xff) + InconvenientDungeonEntrances = {'Turtle Rock': 'Turtle Rock Main', diff --git a/asm/drhooks.asm b/asm/drhooks.asm index 41295aa7..9b603108 100644 --- a/asm/drhooks.asm +++ b/asm/drhooks.asm @@ -139,6 +139,14 @@ org $019dbd ; <- Bank01.asm : 4465 of Object_Draw8xN (LDA $9B52, Y : STA $7E2000 jsl CutoffEntranceRug : bra .nextTile : nop .nextTile +;maybe set 02e2 to 0 + +org $0799de ; <- Bank07.asm : 4088 (LDA.b #$15 : STA $5D) +JSL StoreTempBunnyState +; +org $08c450 ; <- ancilla_receive_item.asm : 146-148 (STY $5D : STZ $02D8) +JSL RetrieveBunnyState : NOP + ; These two, if enabled together, have implications for vanilla BK doors in IP/Hera/Mire ; IPBJ is common enough to consider not doing this. Mire is not a concern for vanilla - maybe glitched modes ; Hera BK door back can be seen with Pot clipping - likely useful for no logic seeds diff --git a/asm/keydropshuffle.asm b/asm/keydropshuffle.asm index 90a21c4f..67241d71 100644 --- a/asm/keydropshuffle.asm +++ b/asm/keydropshuffle.asm @@ -116,10 +116,12 @@ KeyGet: lda $7ef36f ; what we wrote over pha lda.l ShuffleKeyDrops : bne + - pla : rtl - + - ldy $0e80, x - phy + - pla : rtl + + ldy $0e80, x + lda $a0 : cmp #$87 : bne + + jsr ShouldKeyBeCountedForDungeon : bcc - + jsl CountChestKeyLong : bra - + + phy jsr KeyGetPlayer : sta !MULTIWORLD_ITEM_PLAYER_ID jsl.l $0791b3 ; Player_HaltDashAttackLong jsl.l Link_ReceiveItem @@ -136,6 +138,16 @@ KeyGet: pla : dec : rtl } +; Input Y - the item type +ShouldKeyBeCountedForDungeon: +{ + phx + lda $040c : lsr : tax + tya : cmp KeyTable, x : bne + + - plx : sec : rts + + cmp #$24 : beq - + plx : clc : rts +} BigKeyGet: { diff --git a/asm/overrides.asm b/asm/overrides.asm index e73f22ac..00984494 100644 --- a/asm/overrides.asm +++ b/asm/overrides.asm @@ -88,6 +88,7 @@ SuctionOverworldFix: CutoffRooms: db $bc, $a2, $1a, $49, $14, $8c, $9f, $c2 db $66, $5d, $a8 +; Don't forget CutoffRoomCount!!! CutoffEntranceRug: pha : phx @@ -96,7 +97,7 @@ CutoffEntranceRug: cmp #$000C : bne .norm + lda $a0 : sep #$20 : ldx #$0000 - cmp.l CutoffRooms, x : beq .check - inx : cpx #$0009 : !blt - ; CutoffRoom Count is here! + inx : cpx #$000B : !blt - ; CutoffRoomCount is here! rep #$20 .norm plx : pla : lda $9B52, y : sta $7E2000, x ; what we wrote over rtl @@ -108,3 +109,15 @@ rtl bra .norm .skip plx : pla : rtl + +StoreTempBunnyState: + LDA $5D : CMP #$1C : BNE + + STA $5F + + LDA #$15 : STA $5D ; what we wrote over +RTL + +RetrieveBunnyState: + STY $5D : STZ $02D8 ; what we wrote over + LDA $5F : BEQ + + STA $5D ++ RTL diff --git a/data/base2current.bps b/data/base2current.bps index 3c8ec12e285714cdb178fa4ffc7397b0ca51ee75..c53e557860881e053110e53fa3fea05d6659feea 100644 GIT binary patch delta 6251 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