From 626704cc43f33801f56a1c2456d98864f5569bee Mon Sep 17 00:00:00 2001 From: zig-for Date: Tue, 21 Mar 2023 01:26:03 +0900 Subject: [PATCH] Links Awakening: Implement New Game (#1334) Adds Link's Awakening: DX. Fully imports and forks LADXR, with permission - https://github.com/daid/LADXR --- Launcher.py | 2 + LinksAwakeningClient.py | 609 ++++++++++ Utils.py | 3 + data/sprites/ladx/Bowwow.bdiff | Bin 0 -> 7354 bytes data/sprites/ladx/Bunny.bdiff | Bin 0 -> 3083 bytes data/sprites/ladx/Luigi.bdiff | Bin 0 -> 13112 bytes data/sprites/ladx/Mario.bdiff | Bin 0 -> 9025 bytes data/sprites/ladx/Matty_LA.bdiff | Bin 0 -> 4433 bytes data/sprites/ladx/Richard.bdiff | Bin 0 -> 8207 bytes data/sprites/ladx/Tarin.bdiff | Bin 0 -> 8309 bytes host.yaml | 5 + inno_setup.iss | 45 +- worlds/ladx/Common.py | 2 + worlds/ladx/GpsTracker.py | 92 ++ worlds/ladx/ItemTracker.py | 283 +++++ worlds/ladx/Items.py | 304 +++++ worlds/ladx/LADXR/.tinyci | 18 + worlds/ladx/LADXR/LADXR_LICENSE | 21 + worlds/ladx/LADXR/README.md | 25 + worlds/ladx/LADXR/assembler.py | 845 ++++++++++++++ worlds/ladx/LADXR/backgroundEditor.py | 69 ++ worlds/ladx/LADXR/checkMetadata.py | 270 +++++ worlds/ladx/LADXR/entityData.py | 561 +++++++++ worlds/ladx/LADXR/entranceInfo.py | 136 +++ worlds/ladx/LADXR/generator.py | 427 +++++++ worlds/ladx/LADXR/getGFX.py | 41 + worlds/ladx/LADXR/hints.py | 66 ++ worlds/ladx/LADXR/itempool.py | 278 +++++ worlds/ladx/LADXR/locations/all.py | 26 + worlds/ladx/LADXR/locations/anglerKey.py | 6 + worlds/ladx/LADXR/locations/beachSword.py | 32 + worlds/ladx/LADXR/locations/birdKey.py | 23 + worlds/ladx/LADXR/locations/boomerangGuy.py | 94 ++ worlds/ladx/LADXR/locations/chest.py | 50 + worlds/ladx/LADXR/locations/constants.py | 131 +++ worlds/ladx/LADXR/locations/droppedKey.py | 57 + worlds/ladx/LADXR/locations/faceKey.py | 6 + .../ladx/LADXR/locations/fishingMinigame.py | 13 + worlds/ladx/LADXR/locations/goldLeaf.py | 12 + worlds/ladx/LADXR/locations/heartContainer.py | 15 + worlds/ladx/LADXR/locations/heartPiece.py | 12 + worlds/ladx/LADXR/locations/hookshot.py | 18 + worlds/ladx/LADXR/locations/instrument.py | 9 + worlds/ladx/LADXR/locations/itemInfo.py | 43 + worlds/ladx/LADXR/locations/items.py | 127 ++ worlds/ladx/LADXR/locations/keyLocation.py | 18 + worlds/ladx/LADXR/locations/madBatter.py | 23 + worlds/ladx/LADXR/locations/owlStatue.py | 41 + worlds/ladx/LADXR/locations/seashell.py | 14 + worlds/ladx/LADXR/locations/shop.py | 42 + worlds/ladx/LADXR/locations/song.py | 5 + worlds/ladx/LADXR/locations/startItem.py | 38 + worlds/ladx/LADXR/locations/toadstool.py | 18 + worlds/ladx/LADXR/locations/tradeSequence.py | 55 + worlds/ladx/LADXR/locations/tunicFairy.py | 27 + worlds/ladx/LADXR/locations/witch.py | 31 + worlds/ladx/LADXR/logic/__init__.py | 284 +++++ worlds/ladx/LADXR/logic/dungeon1.py | 46 + worlds/ladx/LADXR/logic/dungeon2.py | 62 + worlds/ladx/LADXR/logic/dungeon3.py | 89 ++ worlds/ladx/LADXR/logic/dungeon4.py | 81 ++ worlds/ladx/LADXR/logic/dungeon5.py | 89 ++ worlds/ladx/LADXR/logic/dungeon6.py | 65 ++ worlds/ladx/LADXR/logic/dungeon7.py | 65 ++ worlds/ladx/LADXR/logic/dungeon8.py | 107 ++ worlds/ladx/LADXR/logic/dungeonColor.py | 49 + worlds/ladx/LADXR/logic/location.py | 57 + worlds/ladx/LADXR/logic/overworld.py | 682 +++++++++++ worlds/ladx/LADXR/logic/requirements.py | 318 +++++ worlds/ladx/LADXR/main.py | 52 + worlds/ladx/LADXR/mapgen/__init__.py | 147 +++ worlds/ladx/LADXR/mapgen/enemygen.py | 59 + worlds/ladx/LADXR/mapgen/imagegenerator.py | 95 ++ worlds/ladx/LADXR/mapgen/locationgen.py | 203 ++++ worlds/ladx/LADXR/mapgen/locations/base.py | 24 + worlds/ladx/LADXR/mapgen/locations/chest.py | 73 ++ .../ladx/LADXR/mapgen/locations/entrance.py | 107 ++ .../LADXR/mapgen/locations/entrance_info.py | 341 ++++++ .../ladx/LADXR/mapgen/locations/seashell.py | 172 +++ worlds/ladx/LADXR/mapgen/logic.py | 146 +++ worlds/ladx/LADXR/mapgen/map.py | 231 ++++ worlds/ladx/LADXR/mapgen/roomgen.py | 78 ++ worlds/ladx/LADXR/mapgen/roomtype/base.py | 54 + worlds/ladx/LADXR/mapgen/roomtype/forest.py | 28 + worlds/ladx/LADXR/mapgen/roomtype/mountain.py | 38 + worlds/ladx/LADXR/mapgen/roomtype/town.py | 16 + worlds/ladx/LADXR/mapgen/roomtype/water.py | 30 + worlds/ladx/LADXR/mapgen/tileset.py | 253 ++++ worlds/ladx/LADXR/mapgen/util.py | 5 + worlds/ladx/LADXR/mapgen/wfc.py | 250 ++++ worlds/ladx/LADXR/patches/aesthetics.py | 436 +++++++ worlds/ladx/LADXR/patches/bank34.py | 125 ++ .../ladx/LADXR/patches/bank3e.asm/bowwow.asm | 303 +++++ .../ladx/LADXR/patches/bank3e.asm/chest.asm | 993 ++++++++++++++++ .../LADXR/patches/bank3e.asm/itemnames.asm | 494 ++++++++ worlds/ladx/LADXR/patches/bank3e.asm/link.asm | 89 ++ .../ladx/LADXR/patches/bank3e.asm/message.asm | 16 + .../LADXR/patches/bank3e.asm/multiworld.asm | 355 ++++++ worlds/ladx/LADXR/patches/bank3e.asm/owl.asm | 63 + worlds/ladx/LADXR/patches/bank3e.py | 225 ++++ worlds/ladx/LADXR/patches/bank3f.py | 386 ++++++ worlds/ladx/LADXR/patches/bingo.py | 1036 +++++++++++++++++ worlds/ladx/LADXR/patches/bomb.py | 20 + worlds/ladx/LADXR/patches/bowwow.py | 207 ++++ worlds/ladx/LADXR/patches/chest.py | 59 + worlds/ladx/LADXR/patches/core.py | 539 +++++++++ worlds/ladx/LADXR/patches/desert.py | 7 + worlds/ladx/LADXR/patches/droppedKey.py | 134 +++ worlds/ladx/LADXR/patches/dungeon.py | 129 ++ worlds/ladx/LADXR/patches/endscreen.py | 139 +++ worlds/ladx/LADXR/patches/enemies.py | 462 ++++++++ worlds/ladx/LADXR/patches/entrances.py | 58 + worlds/ladx/LADXR/patches/fishingMinigame.py | 19 + worlds/ladx/LADXR/patches/goal.py | 317 +++++ worlds/ladx/LADXR/patches/goldenLeaf.py | 34 + worlds/ladx/LADXR/patches/hardMode.py | 64 + worlds/ladx/LADXR/patches/health.py | 33 + worlds/ladx/LADXR/patches/heartPiece.py | 42 + worlds/ladx/LADXR/patches/instrument.py | 24 + worlds/ladx/LADXR/patches/inventory.py | 421 +++++++ worlds/ladx/LADXR/patches/madBatter.py | 42 + worlds/ladx/LADXR/patches/maptweaks.py | 27 + worlds/ladx/LADXR/patches/multiworld.py | 308 +++++ worlds/ladx/LADXR/patches/music.py | 27 + worlds/ladx/LADXR/patches/nyan.bin | Bin 0 -> 5760 bytes worlds/ladx/LADXR/patches/overworld.py | 224 ++++ .../ladx/LADXR/patches/overworld/dive/00.json | 124 ++ .../ladx/LADXR/patches/overworld/dive/01.json | 102 ++ .../ladx/LADXR/patches/overworld/dive/06.json | 113 ++ .../ladx/LADXR/patches/overworld/dive/16.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/62.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/6C.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/71.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/72.json | 80 ++ .../ladx/LADXR/patches/overworld/dive/73.json | 113 ++ .../ladx/LADXR/patches/overworld/dive/81.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/82.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/83.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/91.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/92.json | 102 ++ .../ladx/LADXR/patches/overworld/dive/93.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/A1.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/A2.json | 113 ++ .../ladx/LADXR/patches/overworld/dive/A3.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/B0.json | 80 ++ .../ladx/LADXR/patches/overworld/dive/B1.json | 102 ++ .../ladx/LADXR/patches/overworld/dive/B2.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/B3.json | 91 ++ worlds/ladx/LADXR/patches/owl.py | 144 +++ worlds/ladx/LADXR/patches/phone.py | 60 + worlds/ladx/LADXR/patches/photographer.py | 19 + worlds/ladx/LADXR/patches/reduceRNG.py | 9 + worlds/ladx/LADXR/patches/rooster.py | 40 + worlds/ladx/LADXR/patches/save.py | 54 + worlds/ladx/LADXR/patches/seashell.py | 64 + worlds/ladx/LADXR/patches/shop.py | 152 +++ worlds/ladx/LADXR/patches/softlock.py | 93 ++ worlds/ladx/LADXR/patches/songs.py | 159 +++ worlds/ladx/LADXR/patches/tarin.py | 51 + worlds/ladx/LADXR/patches/titleScreen.py | 89 ++ worlds/ladx/LADXR/patches/tradeSequence.py | 355 ++++++ worlds/ladx/LADXR/patches/trendy.py | 3 + worlds/ladx/LADXR/patches/tunicFairy.py | 45 + worlds/ladx/LADXR/patches/weapons.py | 64 + worlds/ladx/LADXR/patches/witch.py | 58 + worlds/ladx/LADXR/plan.py | 38 + worlds/ladx/LADXR/pointerTable.py | 207 ++++ worlds/ladx/LADXR/rom.py | 75 ++ worlds/ladx/LADXR/romTables.py | 219 ++++ worlds/ladx/LADXR/roomEditor.py | 584 ++++++++++ worlds/ladx/LADXR/settings.py | 321 +++++ worlds/ladx/LADXR/utils.py | 222 ++++ worlds/ladx/LADXR/worldSetup.py | 136 +++ worlds/ladx/Locations.py | 247 ++++ worlds/ladx/Options.py | 366 ++++++ worlds/ladx/Rom.py | 40 + worlds/ladx/Tracker.py | 236 ++++ worlds/ladx/__init__.py | 418 +++++++ worlds/ladx/docs/en_Links Awakening DX.md | 79 ++ worlds/ladx/docs/setup_en.md | 93 ++ 180 files changed, 24191 insertions(+), 2 deletions(-) create mode 100644 LinksAwakeningClient.py create mode 100644 data/sprites/ladx/Bowwow.bdiff create mode 100644 data/sprites/ladx/Bunny.bdiff create mode 100644 data/sprites/ladx/Luigi.bdiff create mode 100644 data/sprites/ladx/Mario.bdiff create mode 100644 data/sprites/ladx/Matty_LA.bdiff create mode 100644 data/sprites/ladx/Richard.bdiff create mode 100644 data/sprites/ladx/Tarin.bdiff create mode 100644 worlds/ladx/Common.py create mode 100644 worlds/ladx/GpsTracker.py create mode 100644 worlds/ladx/ItemTracker.py create mode 100644 worlds/ladx/Items.py create mode 100644 worlds/ladx/LADXR/.tinyci create mode 100644 worlds/ladx/LADXR/LADXR_LICENSE create mode 100644 worlds/ladx/LADXR/README.md create mode 100644 worlds/ladx/LADXR/assembler.py create mode 100644 worlds/ladx/LADXR/backgroundEditor.py create mode 100644 worlds/ladx/LADXR/checkMetadata.py create mode 100644 worlds/ladx/LADXR/entityData.py create mode 100644 worlds/ladx/LADXR/entranceInfo.py create mode 100644 worlds/ladx/LADXR/generator.py create mode 100644 worlds/ladx/LADXR/getGFX.py create mode 100644 worlds/ladx/LADXR/hints.py create mode 100644 worlds/ladx/LADXR/itempool.py create mode 100644 worlds/ladx/LADXR/locations/all.py create mode 100644 worlds/ladx/LADXR/locations/anglerKey.py create mode 100644 worlds/ladx/LADXR/locations/beachSword.py create mode 100644 worlds/ladx/LADXR/locations/birdKey.py create mode 100644 worlds/ladx/LADXR/locations/boomerangGuy.py create mode 100644 worlds/ladx/LADXR/locations/chest.py create mode 100644 worlds/ladx/LADXR/locations/constants.py create mode 100644 worlds/ladx/LADXR/locations/droppedKey.py create mode 100644 worlds/ladx/LADXR/locations/faceKey.py create mode 100644 worlds/ladx/LADXR/locations/fishingMinigame.py create mode 100644 worlds/ladx/LADXR/locations/goldLeaf.py create mode 100644 worlds/ladx/LADXR/locations/heartContainer.py create mode 100644 worlds/ladx/LADXR/locations/heartPiece.py create mode 100644 worlds/ladx/LADXR/locations/hookshot.py create mode 100644 worlds/ladx/LADXR/locations/instrument.py create mode 100644 worlds/ladx/LADXR/locations/itemInfo.py create mode 100644 worlds/ladx/LADXR/locations/items.py create mode 100644 worlds/ladx/LADXR/locations/keyLocation.py create mode 100644 worlds/ladx/LADXR/locations/madBatter.py create mode 100644 worlds/ladx/LADXR/locations/owlStatue.py create mode 100644 worlds/ladx/LADXR/locations/seashell.py create mode 100644 worlds/ladx/LADXR/locations/shop.py create mode 100644 worlds/ladx/LADXR/locations/song.py create mode 100644 worlds/ladx/LADXR/locations/startItem.py create mode 100644 worlds/ladx/LADXR/locations/toadstool.py create mode 100644 worlds/ladx/LADXR/locations/tradeSequence.py create mode 100644 worlds/ladx/LADXR/locations/tunicFairy.py create mode 100644 worlds/ladx/LADXR/locations/witch.py create mode 100644 worlds/ladx/LADXR/logic/__init__.py create mode 100644 worlds/ladx/LADXR/logic/dungeon1.py create mode 100644 worlds/ladx/LADXR/logic/dungeon2.py create mode 100644 worlds/ladx/LADXR/logic/dungeon3.py create mode 100644 worlds/ladx/LADXR/logic/dungeon4.py create mode 100644 worlds/ladx/LADXR/logic/dungeon5.py create mode 100644 worlds/ladx/LADXR/logic/dungeon6.py create mode 100644 worlds/ladx/LADXR/logic/dungeon7.py create mode 100644 worlds/ladx/LADXR/logic/dungeon8.py create mode 100644 worlds/ladx/LADXR/logic/dungeonColor.py create mode 100644 worlds/ladx/LADXR/logic/location.py create mode 100644 worlds/ladx/LADXR/logic/overworld.py create mode 100644 worlds/ladx/LADXR/logic/requirements.py create mode 100644 worlds/ladx/LADXR/main.py create mode 100644 worlds/ladx/LADXR/mapgen/__init__.py create mode 100644 worlds/ladx/LADXR/mapgen/enemygen.py create mode 100644 worlds/ladx/LADXR/mapgen/imagegenerator.py create mode 100644 worlds/ladx/LADXR/mapgen/locationgen.py create mode 100644 worlds/ladx/LADXR/mapgen/locations/base.py create mode 100644 worlds/ladx/LADXR/mapgen/locations/chest.py create mode 100644 worlds/ladx/LADXR/mapgen/locations/entrance.py create mode 100644 worlds/ladx/LADXR/mapgen/locations/entrance_info.py create mode 100644 worlds/ladx/LADXR/mapgen/locations/seashell.py create mode 100644 worlds/ladx/LADXR/mapgen/logic.py create mode 100644 worlds/ladx/LADXR/mapgen/map.py create mode 100644 worlds/ladx/LADXR/mapgen/roomgen.py create mode 100644 worlds/ladx/LADXR/mapgen/roomtype/base.py create mode 100644 worlds/ladx/LADXR/mapgen/roomtype/forest.py create mode 100644 worlds/ladx/LADXR/mapgen/roomtype/mountain.py create mode 100644 worlds/ladx/LADXR/mapgen/roomtype/town.py create mode 100644 worlds/ladx/LADXR/mapgen/roomtype/water.py create mode 100644 worlds/ladx/LADXR/mapgen/tileset.py create mode 100644 worlds/ladx/LADXR/mapgen/util.py create mode 100644 worlds/ladx/LADXR/mapgen/wfc.py create mode 100644 worlds/ladx/LADXR/patches/aesthetics.py create mode 100644 worlds/ladx/LADXR/patches/bank34.py create mode 100644 worlds/ladx/LADXR/patches/bank3e.asm/bowwow.asm create mode 100644 worlds/ladx/LADXR/patches/bank3e.asm/chest.asm create mode 100644 worlds/ladx/LADXR/patches/bank3e.asm/itemnames.asm create mode 100644 worlds/ladx/LADXR/patches/bank3e.asm/link.asm create mode 100644 worlds/ladx/LADXR/patches/bank3e.asm/message.asm create mode 100644 worlds/ladx/LADXR/patches/bank3e.asm/multiworld.asm create mode 100644 worlds/ladx/LADXR/patches/bank3e.asm/owl.asm create mode 100644 worlds/ladx/LADXR/patches/bank3e.py create mode 100644 worlds/ladx/LADXR/patches/bank3f.py create mode 100644 worlds/ladx/LADXR/patches/bingo.py create mode 100644 worlds/ladx/LADXR/patches/bomb.py create mode 100644 worlds/ladx/LADXR/patches/bowwow.py create mode 100644 worlds/ladx/LADXR/patches/chest.py create mode 100644 worlds/ladx/LADXR/patches/core.py create mode 100644 worlds/ladx/LADXR/patches/desert.py create mode 100644 worlds/ladx/LADXR/patches/droppedKey.py create mode 100644 worlds/ladx/LADXR/patches/dungeon.py create mode 100644 worlds/ladx/LADXR/patches/endscreen.py create mode 100644 worlds/ladx/LADXR/patches/enemies.py create mode 100644 worlds/ladx/LADXR/patches/entrances.py create mode 100644 worlds/ladx/LADXR/patches/fishingMinigame.py create mode 100644 worlds/ladx/LADXR/patches/goal.py create mode 100644 worlds/ladx/LADXR/patches/goldenLeaf.py create mode 100644 worlds/ladx/LADXR/patches/hardMode.py create mode 100644 worlds/ladx/LADXR/patches/health.py create mode 100644 worlds/ladx/LADXR/patches/heartPiece.py create mode 100644 worlds/ladx/LADXR/patches/instrument.py create mode 100644 worlds/ladx/LADXR/patches/inventory.py create mode 100644 worlds/ladx/LADXR/patches/madBatter.py create mode 100644 worlds/ladx/LADXR/patches/maptweaks.py create mode 100644 worlds/ladx/LADXR/patches/multiworld.py create mode 100644 worlds/ladx/LADXR/patches/music.py create mode 100644 worlds/ladx/LADXR/patches/nyan.bin create mode 100644 worlds/ladx/LADXR/patches/overworld.py create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/00.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/01.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/06.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/16.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/62.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/6C.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/71.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/72.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/73.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/81.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/82.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/83.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/91.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/92.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/93.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/A1.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/A2.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/A3.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/B0.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/B1.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/B2.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/B3.json create mode 100644 worlds/ladx/LADXR/patches/owl.py create mode 100644 worlds/ladx/LADXR/patches/phone.py create mode 100644 worlds/ladx/LADXR/patches/photographer.py create mode 100644 worlds/ladx/LADXR/patches/reduceRNG.py create mode 100644 worlds/ladx/LADXR/patches/rooster.py create mode 100644 worlds/ladx/LADXR/patches/save.py create mode 100644 worlds/ladx/LADXR/patches/seashell.py create mode 100644 worlds/ladx/LADXR/patches/shop.py create mode 100644 worlds/ladx/LADXR/patches/softlock.py create mode 100644 worlds/ladx/LADXR/patches/songs.py create mode 100644 worlds/ladx/LADXR/patches/tarin.py create mode 100644 worlds/ladx/LADXR/patches/titleScreen.py create mode 100644 worlds/ladx/LADXR/patches/tradeSequence.py create mode 100644 worlds/ladx/LADXR/patches/trendy.py create mode 100644 worlds/ladx/LADXR/patches/tunicFairy.py create mode 100644 worlds/ladx/LADXR/patches/weapons.py create mode 100644 worlds/ladx/LADXR/patches/witch.py create mode 100644 worlds/ladx/LADXR/plan.py create mode 100644 worlds/ladx/LADXR/pointerTable.py create mode 100644 worlds/ladx/LADXR/rom.py create mode 100644 worlds/ladx/LADXR/romTables.py create mode 100644 worlds/ladx/LADXR/roomEditor.py create mode 100644 worlds/ladx/LADXR/settings.py create mode 100644 worlds/ladx/LADXR/utils.py create mode 100644 worlds/ladx/LADXR/worldSetup.py create mode 100644 worlds/ladx/Locations.py create mode 100644 worlds/ladx/Options.py create mode 100644 worlds/ladx/Rom.py create mode 100644 worlds/ladx/Tracker.py create mode 100644 worlds/ladx/__init__.py create mode 100644 worlds/ladx/docs/en_Links Awakening DX.md create mode 100644 worlds/ladx/docs/setup_en.md diff --git a/Launcher.py b/Launcher.py index b1cc0fb4ea66..be6fbd76c8bd 100644 --- a/Launcher.py +++ b/Launcher.py @@ -134,6 +134,8 @@ def handles_file(self, path: str): Component('SNI Client', 'SNIClient', file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3', '.apsmw', '.apl2ac')), + Component('Links Awakening DX Client', 'LinksAwakeningClient', + file_identifier=SuffixIdentifier('.apladx')), Component('LttP Adjuster', 'LttPAdjuster'), # Factorio Component('Factorio Client', 'FactorioClient'), diff --git a/LinksAwakeningClient.py b/LinksAwakeningClient.py new file mode 100644 index 000000000000..e0557e4af4ea --- /dev/null +++ b/LinksAwakeningClient.py @@ -0,0 +1,609 @@ +import ModuleUpdate +ModuleUpdate.update() + +import Utils + +if __name__ == "__main__": + Utils.init_logging("LinksAwakeningContext", exception_logger="Client") + +import asyncio +import base64 +import binascii +import io +import logging +import select +import socket +import time +import typing +import urllib + +import colorama + + +from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger, + server_loop) +from NetUtils import ClientStatus +from worlds.ladx.Common import BASE_ID as LABaseID +from worlds.ladx.GpsTracker import GpsTracker +from worlds.ladx.ItemTracker import ItemTracker +from worlds.ladx.LADXR.checkMetadata import checkMetadataTable +from worlds.ladx.Locations import get_locations_to_id, meta_to_name +from worlds.ladx.Tracker import LocationTracker, MagpieBridge + +class GameboyException(Exception): + pass + + +class RetroArchDisconnectError(GameboyException): + pass + + +class InvalidEmulatorStateError(GameboyException): + pass + + +class BadRetroArchResponse(GameboyException): + pass + + +def magpie_logo(): + from kivy.uix.image import CoreImage + binary_data = """ +iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXN +SR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA +7DAcdvqGQAAADGSURBVDhPhVLBEcIwDHOYhjHCBuXHj2OTbAL8+ +MEGZIxOQ1CinOOk0Op0bmo7tlXXeR9FJMYDLOD9mwcLjQK7+hSZ +wgcWMZJOAGeGKtChNHFL0j+FZD3jSCuo0w7l03wDrWdg00C4/aW +eDEYNenuzPOfPspBnxf0kssE80vN0L8361j10P03DK4x6FHabuV +ear8fHme+b17rwSjbAXeUMLb+EVTV2QHm46MWQanmnydA98KsVS +XkV+qFpGQXrLhT/fqraQeQLuplpNH5g+WkAAAAASUVORK5CYII=""" + binary_data = base64.b64decode(binary_data) + data = io.BytesIO(binary_data) + return CoreImage(data, ext="png").texture + + +class LAClientConstants: + # Connector version + VERSION = 0x01 + # + # Memory locations of LADXR + ROMGameID = 0x0051 # 4 bytes + SlotName = 0x0134 + # Unused + # ROMWorldID = 0x0055 + # ROMConnectorVersion = 0x0056 + # RO: We should only act if this is higher then 6, as it indicates that the game is running normally + wGameplayType = 0xDB95 + # RO: Starts at 0, increases every time an item is received from the server and processed + wLinkSyncSequenceNumber = 0xDDF6 + wLinkStatusBits = 0xDDF7 # RW: + # Bit0: wLinkGive* contains valid data, set from script cleared from ROM. + wLinkHealth = 0xDB5A + wLinkGiveItem = 0xDDF8 # RW + wLinkGiveItemFrom = 0xDDF9 # RW + # All of these six bytes are unused, we can repurpose + # wLinkSendItemRoomHigh = 0xDDFA # RO + # wLinkSendItemRoomLow = 0xDDFB # RO + # wLinkSendItemTarget = 0xDDFC # RO + # wLinkSendItemItem = 0xDDFD # RO + # wLinkSendShopItem = 0xDDFE # RO, which item to send (1 based, order of the shop items) + # RO, which player to send to, but it's just the X position of the NPC used, so 0x18 is player 0 + # wLinkSendShopTarget = 0xDDFF + + + wRecvIndex = 0xDDFE # 0xDB58 + wCheckAddress = 0xC0FF - 0x4 + WRamCheckSize = 0x4 + WRamSafetyValue = bytearray([0]*WRamCheckSize) + + MinGameplayValue = 0x06 + MaxGameplayValue = 0x1A + VictoryGameplayAndSub = 0x0102 + + +class RAGameboy(): + cache = [] + cache_start = 0 + cache_size = 0 + last_cache_read = None + socket = None + + def __init__(self, address, port) -> None: + self.address = address + self.port = port + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + assert (self.socket) + self.socket.setblocking(False) + + def get_retroarch_version(self): + self.send(b'VERSION\n') + select.select([self.socket], [], []) + response_str, addr = self.socket.recvfrom(16) + return response_str.rstrip() + + def get_retroarch_status(self, timeout): + self.send(b'GET_STATUS\n') + select.select([self.socket], [], [], timeout) + response_str, addr = self.socket.recvfrom(1000, ) + return response_str.rstrip() + + def set_cache_limits(self, cache_start, cache_size): + self.cache_start = cache_start + self.cache_size = cache_size + + def send(self, b): + if type(b) is str: + b = b.encode('ascii') + self.socket.sendto(b, (self.address, self.port)) + + def recv(self): + select.select([self.socket], [], []) + response, _ = self.socket.recvfrom(4096) + return response + + async def async_recv(self): + response = await asyncio.get_event_loop().sock_recv(self.socket, 4096) + return response + + async def check_safe_gameplay(self, throw=True): + async def check_wram(): + check_values = await self.async_read_memory(LAClientConstants.wCheckAddress, LAClientConstants.WRamCheckSize) + + if check_values != LAClientConstants.WRamSafetyValue: + if throw: + raise InvalidEmulatorStateError() + return False + return True + + if not await check_wram(): + if throw: + raise InvalidEmulatorStateError() + return False + + gameplay_value = await self.async_read_memory(LAClientConstants.wGameplayType) + gameplay_value = gameplay_value[0] + # In gameplay or credits + if not (LAClientConstants.MinGameplayValue <= gameplay_value <= LAClientConstants.MaxGameplayValue) and gameplay_value != 0x1: + if throw: + logger.info("invalid emu state") + raise InvalidEmulatorStateError() + return False + if not await check_wram(): + return False + return True + + # We're sadly unable to update the whole cache at once + # as RetroArch only gives back some number of bytes at a time + # So instead read as big as chunks at a time as we can manage + async def update_cache(self): + # First read the safety address - if it's invalid, bail + self.cache = [] + + if not await self.check_safe_gameplay(): + return + + cache = [] + remaining_size = self.cache_size + while remaining_size: + block = await self.async_read_memory(self.cache_start + len(cache), remaining_size) + remaining_size -= len(block) + cache += block + + if not await self.check_safe_gameplay(): + return + + self.cache = cache + self.last_cache_read = time.time() + + async def read_memory_cache(self, addresses): + # TODO: can we just update once per frame? + if not self.last_cache_read or self.last_cache_read + 0.1 < time.time(): + await self.update_cache() + if not self.cache: + return None + assert (len(self.cache) == self.cache_size) + for address in addresses: + assert self.cache_start <= address <= self.cache_start + self.cache_size + r = {address: self.cache[address - self.cache_start] + for address in addresses} + return r + + async def async_read_memory_safe(self, address, size=1): + # whenever we do a read for a check, we need to make sure that we aren't reading + # garbage memory values - we also need to protect against reading a value, then the emulator resetting + # + # ...actually, we probably _only_ need the post check + + # Check before read + if not await self.check_safe_gameplay(): + return None + + # Do read + r = await self.async_read_memory(address, size) + + # Check after read + if not await self.check_safe_gameplay(): + return None + + return r + + def read_memory(self, address, size=1): + command = "READ_CORE_MEMORY" + + self.send(f'{command} {hex(address)} {size}\n') + response = self.recv() + + splits = response.decode().split(" ", 2) + + assert (splits[0] == command) + # Ignore the address for now + + # TODO: transform to bytes + if splits[2][:2] == "-1" or splits[0] != "READ_CORE_MEMORY": + raise BadRetroArchResponse() + return bytearray.fromhex(splits[2]) + + async def async_read_memory(self, address, size=1): + command = "READ_CORE_MEMORY" + + self.send(f'{command} {hex(address)} {size}\n') + response = await self.async_recv() + response = response[:-1] + splits = response.decode().split(" ", 2) + + assert (splits[0] == command) + # Ignore the address for now + + # TODO: transform to bytes + return bytearray.fromhex(splits[2]) + + def write_memory(self, address, bytes): + command = "WRITE_CORE_MEMORY" + + self.send(f'{command} {hex(address)} {" ".join(hex(b) for b in bytes)}') + select.select([self.socket], [], []) + response, _ = self.socket.recvfrom(4096) + + splits = response.decode().split(" ", 3) + + assert (splits[0] == command) + + if splits[2] == "-1": + logger.info(splits[3]) + + +class LinksAwakeningClient(): + socket = None + gameboy = None + tracker = None + auth = None + game_crc = None + pending_deathlink = False + deathlink_debounce = True + recvd_checks = {} + + def msg(self, m): + logger.info(m) + s = f"SHOW_MSG {m}\n" + self.gameboy.send(s) + + def __init__(self, retroarch_address="127.0.0.1", retroarch_port=55355): + self.gameboy = RAGameboy(retroarch_address, retroarch_port) + + async def wait_for_retroarch_connection(self): + logger.info("Waiting on connection to Retroarch...") + while True: + try: + version = self.gameboy.get_retroarch_version() + NO_CONTENT = b"GET_STATUS CONTENTLESS" + status = NO_CONTENT + core_type = None + GAME_BOY = b"game_boy" + while status == NO_CONTENT or core_type != GAME_BOY: + try: + status = self.gameboy.get_retroarch_status(0.1) + if status.count(b" ") < 2: + await asyncio.sleep(1.0) + continue + + GET_STATUS, PLAYING, info = status.split(b" ", 2) + if status.count(b",") < 2: + await asyncio.sleep(1.0) + continue + core_type, rom_name, self.game_crc = info.split(b",", 2) + if core_type != GAME_BOY: + logger.info( + f"Core type should be '{GAME_BOY}', found {core_type} instead - wrong type of ROM?") + await asyncio.sleep(1.0) + continue + except (BlockingIOError, TimeoutError) as e: + await asyncio.sleep(0.1) + pass + logger.info(f"Connected to Retroarch {version} {info}") + self.gameboy.read_memory(0x1000) + return + except ConnectionResetError: + await asyncio.sleep(1.0) + pass + + def reset_auth(self): + auth = binascii.hexlify(self.gameboy.read_memory(0x0134, 12)).decode() + + if self.auth: + assert (auth == self.auth) + + self.auth = auth + + async def wait_and_init_tracker(self): + await self.wait_for_game_ready() + self.tracker = LocationTracker(self.gameboy) + self.item_tracker = ItemTracker(self.gameboy) + self.gps_tracker = GpsTracker(self.gameboy) + + async def recved_item_from_ap(self, item_id, from_player, next_index): + # Don't allow getting an item until you've got your first check + if not self.tracker.has_start_item(): + return + + # Spin until we either: + # get an exception from a bad read (emu shut down or reset) + # beat the game + # the client handles the last pending item + status = (await self.gameboy.async_read_memory_safe(LAClientConstants.wLinkStatusBits))[0] + while not (await self.is_victory()) and status & 1 == 1: + time.sleep(0.1) + status = (await self.gameboy.async_read_memory_safe(LAClientConstants.wLinkStatusBits))[0] + + item_id -= LABaseID + # The player name table only goes up to 100, so don't go past that + # Even if it didn't, the remote player _index_ byte is just a byte, so 255 max + if from_player > 100: + from_player = 100 + + next_index += 1 + self.gameboy.write_memory(LAClientConstants.wLinkGiveItem, [ + item_id, from_player]) + status |= 1 + status = self.gameboy.write_memory(LAClientConstants.wLinkStatusBits, [status]) + self.gameboy.write_memory(LAClientConstants.wRecvIndex, [next_index]) + + async def wait_for_game_ready(self): + logger.info("Waiting on game to be in valid state...") + while not await self.gameboy.check_safe_gameplay(throw=False): + pass + logger.info("Ready!") + last_index = 0 + + async def is_victory(self): + return (await self.gameboy.read_memory_cache([LAClientConstants.wGameplayType]))[LAClientConstants.wGameplayType] == 1 + + async def main_tick(self, item_get_cb, win_cb, deathlink_cb): + await self.tracker.readChecks(item_get_cb) + await self.item_tracker.readItems() + await self.gps_tracker.read_location() + + next_index = self.gameboy.read_memory(LAClientConstants.wRecvIndex)[0] + if next_index != self.last_index: + self.last_index = next_index + # logger.info(f"Got new index {next_index}") + + current_health = (await self.gameboy.read_memory_cache([LAClientConstants.wLinkHealth]))[LAClientConstants.wLinkHealth] + if self.deathlink_debounce and current_health != 0: + self.deathlink_debounce = False + elif not self.deathlink_debounce and current_health == 0: + # logger.info("YOU DIED.") + await deathlink_cb() + self.deathlink_debounce = True + + if self.pending_deathlink: + logger.info("Got a deathlink") + self.gameboy.write_memory(LAClientConstants.wLinkHealth, [0]) + self.pending_deathlink = False + self.deathlink_debounce = True + + if await self.is_victory(): + await win_cb() + + recv_index = (await self.gameboy.async_read_memory_safe(LAClientConstants.wRecvIndex))[0] + + # Play back one at a time + if recv_index in self.recvd_checks: + item = self.recvd_checks[recv_index] + await self.recved_item_from_ap(item.item, item.player, recv_index) + + +all_tasks = set() + +def create_task_log_exception(awaitable) -> asyncio.Task: + async def _log_exception(awaitable): + try: + return await awaitable + except Exception as e: + logger.exception(e) + pass + finally: + all_tasks.remove(task) + task = asyncio.create_task(_log_exception(awaitable)) + all_tasks.add(task) + + +class LinksAwakeningContext(CommonContext): + tags = {"AP"} + game = "Links Awakening DX" + items_handling = 0b101 + want_slot_data = True + la_task = None + client = None + # TODO: does this need to re-read on reset? + found_checks = [] + last_resend = time.time() + + magpie = MagpieBridge() + magpie_task = None + won = False + + def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None: + self.client = LinksAwakeningClient() + super().__init__(server_address, password) + + def run_gui(self) -> None: + import webbrowser + import kvui + from kvui import Button, GameManager + from kivy.uix.image import Image + + class LADXManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago"), + ("Tracker", "Tracker"), + ] + base_title = "Archipelago Links Awakening DX Client" + + def build(self): + b = super().build() + + button = Button(text="", size=(30, 30), size_hint_x=None, + on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1')) + image = Image(size=(16, 16), texture=magpie_logo()) + button.add_widget(image) + + def set_center(_, center): + image.center = center + button.bind(center=set_center) + + self.connect_layout.add_widget(button) + return b + + self.ui = LADXManager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + async def send_checks(self): + message = [{"cmd": 'LocationChecks', "locations": self.found_checks}] + await self.send_msgs(message) + + ENABLE_DEATHLINK = False + async def send_deathlink(self): + if self.ENABLE_DEATHLINK: + message = [{"cmd": 'Deathlink', + 'time': time.time(), + 'cause': 'Had a nightmare', + # 'source': self.slot_info[self.slot].name, + }] + await self.send_msgs(message) + + async def send_victory(self): + if not self.won: + message = [{"cmd": "StatusUpdate", + "status": ClientStatus.CLIENT_GOAL}] + logger.info("victory!") + await self.send_msgs(message) + self.won = True + + async def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None: + if self.ENABLE_DEATHLINK: + self.client.pending_deathlink = True + + def new_checks(self, item_ids, ladxr_ids): + self.found_checks += item_ids + create_task_log_exception(self.send_checks()) + create_task_log_exception(self.magpie.send_new_checks(ladxr_ids)) + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(LinksAwakeningContext, self).server_auth(password_requested) + self.auth = self.client.auth + await self.get_username() + await self.send_connect() + + def on_package(self, cmd: str, args: dict): + if cmd == "Connected": + self.game = self.slot_info[self.slot].game + # TODO - use watcher_event + if cmd == "ReceivedItems": + for index, item in enumerate(args["items"], args["index"]): + self.client.recvd_checks[index] = item + + item_id_lookup = get_locations_to_id() + + async def run_game_loop(self): + def on_item_get(ladxr_checks): + checks = [self.item_id_lookup[meta_to_name( + checkMetadataTable[check.id])] for check in ladxr_checks] + self.new_checks(checks, [check.id for check in ladxr_checks]) + + async def victory(): + await self.send_victory() + + async def deathlink(): + await self.send_deathlink() + + self.magpie_task = asyncio.create_task(self.magpie.serve()) + + # yield to allow UI to start + await asyncio.sleep(0) + + while True: + try: + # TODO: cancel all client tasks + logger.info("(Re)Starting game loop") + self.found_checks.clear() + await self.client.wait_for_retroarch_connection() + self.client.reset_auth() + await self.client.wait_and_init_tracker() + + while True: + await self.client.main_tick(on_item_get, victory, deathlink) + await asyncio.sleep(0.1) + now = time.time() + if self.last_resend + 5.0 < now: + self.last_resend = now + await self.send_checks() + self.magpie.set_checks(self.client.tracker.all_checks) + await self.magpie.set_item_tracker(self.client.item_tracker) + await self.magpie.send_gps(self.client.gps_tracker) + + except GameboyException: + time.sleep(1.0) + pass + + +async def main(): + parser = get_base_parser(description="Link's Awakening Client.") + parser.add_argument("--url", help="Archipelago connection url") + + parser.add_argument('diff_file', default="", type=str, nargs="?", + help='Path to a .apladx Archipelago Binary Patch file') + args = parser.parse_args() + logger.info(args) + + if args.diff_file: + import Patch + logger.info("patch file was supplied - creating rom...") + meta, rom_file = Patch.create_rom_file(args.diff_file) + if "server" in meta: + args.url = meta["server"] + logger.info(f"wrote rom file to {rom_file}") + + if args.url: + url = urllib.parse.urlparse(args.url) + args.connect = url.netloc + if url.password: + args.password = urllib.parse.unquote(url.password) + + ctx = LinksAwakeningContext(args.connect, args.password) + + ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") + + # TODO: nothing about the lambda about has to be in a lambda + ctx.la_task = create_task_log_exception(ctx.run_game_loop()) + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + + await ctx.exit_event.wait() + await ctx.shutdown() + +if __name__ == '__main__': + colorama.init() + asyncio.run(main()) + colorama.deinit() diff --git a/Utils.py b/Utils.py index f7305a1e2a14..23acb9f180a1 100644 --- a/Utils.py +++ b/Utils.py @@ -260,6 +260,9 @@ def get_default_options() -> OptionsType: "lttp_options": { "rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc", }, + "ladx_options": { + "rom_file": "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc", + }, "server_options": { "host": None, "port": 38281, diff --git a/data/sprites/ladx/Bowwow.bdiff b/data/sprites/ladx/Bowwow.bdiff new file mode 100644 index 0000000000000000000000000000000000000000..bdfe9f42f23bf24c4ea8901ef9ba1dde5f4827a6 GIT binary patch literal 7354 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 XNBPQyn4D6uElTZHxZ+2Vu literal 0 HcmV?d00001 diff --git a/data/sprites/ladx/Luigi.bdiff b/data/sprites/ladx/Luigi.bdiff new file mode 100644 index 0000000000000000000000000000000000000000..1897a65d01463797e61e535ea26cb79882f42171 GIT binary patch literal 13112 zcmY*D6_?ncA?4Bfuck83Urib4FCdMlw-y~|Be4!RG}1u0f+zqBzpgl^S>*!q!fnx-x`R50seLUXAl5JL2y)2 zIPiWHxZodj$y7*l1|Uk~2*lCc=h||rT5u9Q=MZLaN$I~Cp(wy=>HjDixa9%1XGLNLtPfN^u2}FlpP`N!F$#p6AR1w6Nw>MF;>R_y zXyHj&7aQ?A_tMGgy_*70rH^JDW&p8*e1dw-ReWEi4%uMn48d|0jE}55u(JAk+YTLY z;^K+4KJnTXpq}8aX8BsGuW>t~MJv@aG=BWs!y|4H{b%z}X3ud>xcl#In^u{14pYhY z1)g19TW;2cZSu>ZFLI1)sHRV7$R=~+s|aY{?%+@UcjF_0`S!0?-a*>QTKy*DKq0D# z(uhRmr`J3<(w&*85xsP};S3IRyqHIRxp+*Jd!pN-)`FN_uWu5sDSJY#Auj(+{c0Pt z4WLkh8iqpU;E{jF8VBgL_K)HxeLX7(#usNmI+e{jYu#m$^+`vi<<;@blcI&5Zai7H z)BmHZaX=qRohYFCi%KldTqxh{KBjl0F`y;1xz-=wm{jP|1o)H4<5rclCqmZY(*}d< zpT{6tQHr$iv+Rdte7@p2{4a$atm1?A%y7gUhA;G2~HYh1-Q#gAp{JsE>ZsF4o#GG;QHc{w)v?b~5b#{K6M!7j$CZ7@Gd#{!+6j0P`tymx`Ni%Ak_SehAl zo3lckj<2j6?#yo}G69RveaRB$zJU1h3D7%(NeNbg?s7AHOtrrz;Vi3hpj#&Ae&bO` z*iZI5cNgzJ~ben!6+IolN+7rPJr5mmOE?FRP;2e83IgQBQ!wT{&Qc zTZ~X!M%j*)sfGt>R%?Wjgi8{>2&Yt(7fy(}i&It}K55+;M8`diRkpmaz!Jb}w;^kY zb*aQQQso>kdZEdlgI7i>LhxX(A+7~CQ(0LrD0u;+9*@P9>cJ(ozRK+|L*mf>ve^B| zSr8)6B;5DjkAXxvJ(+e>HLp6mTVhITIs*IR9 zsnO(v(G?>U>F{%&)eIn!piRSU1K9X|GzUwj)-5OoH2>+(B8VK|((x<}1&U(9QdC4R zHHZso6VI-@emfF4t9nbypY!Y!h{7jWm#Ri`2-=Uv$qMvXeHT#Ss0EWlD;>oIWN^+*0_JIttAzn;5>J;58$g$nAP`s&p!w>fOK?~Itu_}yPO=kw2E8IK?wiaqzDbzj zNF_TfKQ70-Vgp&QT3-9>OH1j&-bPoIp1L-^q;Z+`(VeE@ed>S^hiu7OLLufFcBPu4P8?uRKywK zY8abYa9^cn`mpQ710x*%Dl$eC{SL@n2zbmoYUEs628ncWI3y?Kfe?Te57*Lno-;P4 zf8IVlqh=tdAcsP&T_Q~ODY^Fqx;;B$ADx)9G#m4OaBTpOW$$dM)n{n?P&kN>bC+=^ zo6cY?{3O%fs2kAlZTcb#_>?=P4`&cH$|yDP@YICoC#4BNcIPL-%j{I!!Vha+-}PTT zR}A14<@>1H&AY7^&DZJIVRYIq`2JC*Vbr9YP_Xt&x$|7bTmC4X z{&UYULOZqX$rOr86iFmELAiCUQ3mdK)URf%M0z-zIU6*b~&nyKip6qM;5NcEIW;!?s_ zO`Ioq+a2T5mvbD($P)N%xw#%K{Gpd5p>XLu2`r*vU9Fr08?)g6ZOXEME(y%i*Z!}r z+WCI!s@g8aO^agUZZxn7PA2Mz_B6TH<-S_UQ#h|OE7#Pi!%X!vqQ{+PRSuI&A}*LF zk1j}P_pFGHQi3dBw@-9alO(!vvKyIJAo4XsN$|C4ozZ>YVK!x(r*mVPr@ldIJMWgc zWc9UzCGvB4CVnj#r=|$X=Ix}i&sYyP<@-;4(9?5YVzP4AG?N!ZwOy>G zBj2{K#XobeQh`^t#c?aQ+Xt>t96V$^0&iFclFL-75*p|@`4jAzhZ7{b)X{lT`-mtz z;WVZe4dHX|1J+w~&770d>z5hYMVcHt7T*#MkAG1;>j>O41ir<^GaOjgcu zce6_`U{uI^OFM;%igADf2gV~JA+82s-`-8-OsMIJFt!v28OiY%NB@>R zTT&u0YNn%pmz4($)GjkSiWiX7Fp2OlPbtg0eRovh?y{>8^QD^JEOpifF$5TQ;}fED zMEw?E_~AS|I=uLf@tnL@*fGPDjp&9J{Ny}{9T`nP(EXZ8?|(U~TdA=Y00;8_Q`Wz~ z|KZ!efBuvH{uliHyG_VIg0qLe@-pV#1Her;_=<*O1sn1~BHhNXh^ zDjDkR3`{~nvC2vTbTF6*P#cB;V4(|~|1V2*24K|DnR<&CuA#UShb%3xYinEcYtdpt zY}#3KadTEv`%4L@7{}EWHAEqJoqJsfEJW3vp^;*!4M)de@BV zLM8Jqa@ulg=(Gils%mT&un;*XEaD6U7ht8Q{UhV($VI>)Z1^7=#$ch9;0sDxss#Xh z7$9qrl7*|b`Y8VZz4#k>m8v0&UU1cVU9%WgBL^RJhQcmfj^+9UBcgbamZLg=v-_C!97(8WYSv&cLCYuZ;r zym$a=;RjquqRAsOp|>G(FPf?pDiyGLN%qb7IHP1OC`VF|I4QxaVZ+hJNq$M_AE24< z=gQxw)ShTeI+Mj@A+9cfI7t4|oDm4fKE{7x^*Y0w-X1|TBs8Y{Jg@%}Y>Hu_i>)@G z7So7t2MSHc%`jFOGaxC&JPlAoDMwRLnpg0g&+PL!u13{j95-YxZNDs;@$8hR^W#eA zk?VLSa|$S)8EkqyHeBu|(yklOr#xg1$p41`cKKp5b6;y3KfR&?zU@g2in}#Yyk-TC zP&A^Y1&}#Q2c~~&BGtfT^I#BY3@;>!oY#aS%2wRieVDA9g_{7R(LDcHE3(GnFq(f+ z2>2ClRHaDZ4Cz6NVm!wsIO$5U%eoho9OO;T8GnD370sP2ooSh zkvJ9n1y|}e`P0yxsCX;-2v#%Zjrb}m!F1)NPju{El&*RS1}@~-8D*B+HV18Q z{ebE*WM$o^sS52B+VQ|9E@C$$+C^yDx5u@ugHJ#0qcwgjZwfTCztCGi%KO8jfTO2@ zkyl`$&gi6i>+&Jk0qrrZ^{@(M`Pbp2vpsu9g67=8@#hRp5d(Pn!a`}sZ?6Z!NXaKF zc3=3iwOndqmRUhhfs4o#GCfu_T0vWGCPgrb2)x2C3UBl$E_NB3QOD-qrA07sKO^q6 zi0KZesK_*kAoZJYwpz{y;R3!%WmuC#WX^uXas3jX6=5JQ?S;=>&5ExQ4?Tv^Cp-cO zDR9<{8z!&A2m-U1xnR9BneD@^O5n*zr6bel($t0OjYgAP<&+Z;Fnh;e(k5jP7W4Lw zy-q+S7fJa;WWWS8voC{R+{PJ#O#J(51&=af{~}I)JpG5jP#Wti5=nSM*)zxhX&^hO zvW$%9+ntx8L|{~l?>2fc9YLBsF+1#W`{<@OM1epjf_y?x0STEG_2<1WD8Vi@4TCIc zra8#J%8w`;1-F^gxyW6lJ>o%KWJY=_XeF*YzbTq(R;u*n)%+`V^|f1kc_q%MWi$(GByK@ok=- z!XQe5X%FEhub;F>6fb6iyGX*+&FCaU!uvkp9cSxF)mRU~t0#S|2t~kcF#Bm%#D*6rZCWh2`zidUGZ3$*HFR*Pu zt82EVh4B19`}FgDeXPXT>WtA6l6js;nfaAe_HGt5sJe+Aec+!_uZqe`@(l3zhws8f8t(d8E0 zL4-7)kL1g8Jd^tBb)Xv2$woJUl~UmwC6#2ycATx_q}t}Izn5I9IBTBr(nVzOq>6pW zen^&_x)Nw;&>+=0RQz^F_m=O#=ew3*zRtW5cy#hQOy5f-y>I%#6gPd_&3s$TYu&74 z$(m^TXz9bs#$UNMrpKnsOL?!(hs5@aeR64|jboqLX4?BI1?qAnaWHG11IiDx>6cE@BUB1qh zO0=ka9HTc7Ux{FbW4WsW;iUPS@+M>&qjtBZ>>0k6FBK18m3>(pHN3gl4>!ies}_fS z{{m--5~=8dwuR_#NBp`|MRR^WCB>t0TY`o^Em5SnW+5=3SbuvZ(7F-is9?4~wWq); z7o>cV#nGMWLr^1yE)1|8`D4H<&2slm`OLk9CQ(@!$vJjAj}du=*&1z_DgyM_S(6F7Fg@@{V;&oa7sRxi?AsA zGZne~cn#hAWmcN`iPV8D+FPk?5Y%~C1^ekpf#uhbs5?z!R#d$pbM{W--BQqeoG=!y zC0=#DN%ki+_DtsVdU*AwDsNr8H?zhS)oPll4f-9ZT1~}@{g)N{j+OS&$KI~Dq@kmi zh4<5W8}dJ6qfJ$9AC{oll=qAG&qiv)#Vj~E(y?E2nfWZC>9q<(yY`({$-Y@fcf0ML zF&VRJy9NbK)_kde*@NR&rDBC1<9_=w(uSMq-K+Gon-pcpK*F7&&TH^S@JYyR+>ToXx=2X`=P20GAHDlrJb-X*acbGG~ z>pnHN_U+Wkd3iGQ`@LD&_&8DBK|fZN68Ee(Pl(2MR#ukNEPkuNM2?yqxb^cf#z|1)#Yj zowVTb`(-WHJe#=D0_4Q-__N>VL0xHu(W%DtV0P0wsu-%;4Zb1pdz0g9X(7gGP{N?i zZT72x3KFV=Pa;pP+M9*G8&A`P7WoY@$js#2Jtmv+36-s43MF?EUZLb@6=q@a5=>6V zD%Rqyt7kRN747i!r)U?o$0xhUMNLB27lN6&^ZTc#(Wjl@p@I)|YC5whR{q-C8eYfN zOB_1fuy1!S(NN!F(+0 zlHEXfTwGk_hW;#EV#xLbkFFYjAk&)xWzUbCoGLUkt8C!DucwG=LUM>d;UVgOWmyaY z1OgDmy!x*I`|s`3|Cax)K7adf+NRCF4#tc%Edi9JavT=;8jd{@zHXR>qC#z`<==9>-qOGUpQEB2W{;s$kGO()rT1#bX-|ijuoyV z)dyVJ;?=eLwI>AV1?BM)m)aIF?F4sS!*jLFIgt_w@>-jzQY}M-gtIzL?I`)EkQ*3& zB%Z{64U4stO4U-{Zk)X^2XNTu;Y?sZpZj0j_@a90C`ysM{7>L0{j)=!7?QIx$0WcOv|(IX!xdKxNaQ;5H)nze8jYEB z4b}Sh`EikJPl=8*aviyNRf2RDCOg24>rxIcV6i>lR02DWanG-$E#th{wiI%NSvlkNy&Aeu*&Sd|s%~hc-qN|GV&|-F8 zFXTC*V@%J4UnwbRp&kbQPBzSN~yD@8RLz zpMLN#^k+`YY=oeEtS*N&YZ!KXSGgRG<+{3^H;TmaO; zX@!pzVWxHQR+w+g1<)iNN&Ys3z-wzEYffaEwc#tjZHt zUN+!rmh?I25ov}B%d-L8rg3vDs(_p$aGe=Hy~eV8exyfolZf!sndMyCw`mXweh;@z z>}DDDGt0qi&-W1CMp3A7f)?F;@;G_z8J2acB9&{Su()OxV^!RkW=kh_4!l7t+MW*+ zt;*uT_rb0;;{NNF{>U!nkt&+$wv6s( z%iP0c0>vCz0=sgTt&u%Dk}XSj{*vYJPPn#NCF*psLGf#rs?;P6i)9ys_0?qgqjeH| zp&>R>>z)dcwvw>pdKmqSXeY+6e!I!0Y)@PAX~q%NMR}zoJZsWdgj;;3MT7N{)kbug z;q@xc%>V>}i%F+FUOJ(dyH2MSbm(fYq zbbGuW&_$%m1`CI@ZJi#w9rv&9T~@(GP_wL31_(G7TuZBzFd4i?cjKKT(07z~K0}g+ z8mtZD^r!6?fKKui=;Z6g8&UulYsRf|2l=Xt+`GC(p&R7=o{!TezqV!f7_At3r(jU6 zHub`qqg+0cgj=k6@q6D`WQ#y(*EElz7hs2Z)JeAD{WYb(xlO^xDmFpBhCA=U52_$kd*6Fz6u-WMEM~O z3N`W;z-Mo8wPRJ%XIabJ+v=)bN#N3Xwqcu6(zNVh%xT*_J3LMbEL5rYYAKw|naR)B zi(_h8` zi7b-IY2Q3PuD6MrC^!l?r;$<4V9!5F0SXMO$zJ9OCYM$`H|biMy8_3E_fbU&4bmqv z1m|gO;>Dv~(uu_Pbgp)I4LZv7ni&du%uU^&2xk!?({pF%#mF*qD``(?my7I1w42MA zDwC3Lk#90bS}&&YQ==K{TBA+XluJcpU5Idvu!)7~q#)mvCRpO-WD;>|*9~Zk8sE4Dr$wDP9`&6FZzdeGS~Co8BFA;%wF%df@@| zd?mX+Angr5Ooc^|Z824NFmVCj7fAV))=>=X%sxdOr#E+MkS)cG34x2AyM?WX;vR7Wb$2d*l_amV+`;$=hv3PE*$%&J zzFwg*$BMwF(RwzBO8Go>Z-n@k(K1I@rYo7O&UYGL^=qVdWgLdV!p3#geW@LvW0^2% zcg%fm2Z{=Zq|;LgH*yF>{ppF0;RU*gFaH5r@+Ef*zR`2FzxB0G5KiJ$*34E+O^*nu zG25gu2J&`Tw7!p^;bk zd-=sKqY~`oh)EnMgz>|n2KSmLU6GpsS>{wE45YlIf{vxbHcXTD=aZazmnK2#td_~J zW@_V2@tAR35IHy`OiARZR*GueBkY_cQbDKmUU|@i`2aku_l{W$2ZsibxJ-N~u zR3QXjLneWgRJ(jlUWa73yq6TnzPJQ=B)*I{4g?X7?`-z`PSQC&_f=jCOt^K373Eq9 z2>m3IJ!(Ho7pWCzDY=>^D{Y+|ARkgLnw_b6>an<8&rt(^V<)HOhe9FH2*hta1;DtZ zUNqT(Qs1ZzxOjh_xDkt8N+b8r)ZH& zZ0j)x%yg9-Qs8~fvyv+TKH7-OXJ=yg#&Ig03h4p6L9l0|UZ4%3*0j3D#9kC<7)#mf zYD2dg-6Abp`)hj52usQAmnEeK(jn;VsAuNPv_hjaDG70UaI^W?jsyjtPj0O=uVyW> z6?a-ren`KWDlIlvcz<^kj3;2I$|zsc4L;xaY5DzEpMk*u@U!5bg3DdWIYD7KrQ8)) zgK}Hj>rC%I`|F2nA6f^zsmwsr6gfPKppdQ|+bd|fg2ZHwWE$1+-BUUkD_s9CQ>-X= zpyB)F>WQ!Z=9AL@tZ(9xoF}-h#AMN9pMW{(K?C`t;>$tXri-H%g(gZ3dqd#vss*(P zqe`R6WIXfTfQ-=!NBTFP_Vdd>7LCaa(cufUtMNuB-u|{q*pW1*J;?Nl%eQftEB=?XbH8Qz(sUO}WmZ-J_v7ULt`Cui0_zVBFwz`S{J< zgH~C3kQ=3*tU~|1!Qtl%yPnr+O=~MnxrFxB@Bh$=^2ojjQXhZMfUDVVv0J(Kr7Xw| zIaVRIu3i}$Nn`+D_K8<)n}`l7?ujq}3@s-perAjhHuO_x?H_%@3dU#439h*=MWxF1 z=CaqQ+sasjdam*+uRqHz)o<~4CTey~G7p3(-XLs^w^2>Y^7>!!XF~WSVuZR0C5o$w zb;Nx*dN#U8N~o5fbG96~7v!+y?*ws(9DFv?4YR zB+7A(-lqGhXp5h2dmlVCr+U_(FX|iNJA3@4-&RtwSaR$R)u-%tvocf)<+35lr>#@u z5uGY;YSk+RCt0Y-Or42xebz6iuPWK_A@aXj2>Xg(PwFKnGhspnnbhAhyDQ|aPRlEc zbsN79gg>-DYLj2On!!%l{%vK2W%;*;T8VqziRKj?xFSQcHltS&bG5!@0FGp7dAL`3 zoaiO-z;XVKPvY+iU^vK+Mvlq9&+=vltG8A7ZS}?NzNpkd$?Lzmui7peM?EGXUjp4Z z4(zAF1{L7X5;iQozqMJ$@^CpPEv=9j8K~l(?I@gr^=V7Q(&( z9YOn7EO*|?ah4XQ1I^4@1|dIRR=>DPmkn!ItQjk~#~pjh?d_8)muP}TH*JYit$VjA z{6xM;_R6)bE^7I2)~NiMjK9$I$9G$B){3Ljo+%}`{wveuww1Oq-jWETYTlNf07?2k z&G4T5kRqlt`d~Z9fPMj64L1V@ot0kwY!IrZ-vDE`4_cl-O%Dx?$W&;fTY;@RInm`& z%&Vvh*^K(wZdRsYnUcu-TvqdKKwp_AWy0FPu^MSYQ*69I(zvDihD| z=%+hHv-1O^pkKl%AwzW{#dLaa8<(l-oCopjVkV& zv5EbjX@z{zG~TxSWhwCWO|#gzbzDb(-!Z@YL;h;#-n>coxv&j((yU+5U(^m0fuyyD zd!W|^Z{o(XXO#wRB-mI+s?X$__a_#4F+1MFZJC~3a+Ad_LjK7#OX z1{iNYj{3_GJpZ{JWVeSA-qH+F29ff@`MtVM{Rfkymhy=SRo|cAKwHF+5m6AQq?CWg zOCAAsLBTzWiP_P+JP+~g$KG+&9+Zq0dJ=Q2k3oZq3IlrI-Qz~(b_cTw=Vr*$NCZLB zMc-R_vY4KIPb1=$jV9kAzdWt zV{!5J%CmqYzmfadVv=r)#*!z2lbMoi43)L(`n@Ry zqCuNP-QQVzA8L#%ysAb)XS|?hk-Mzk;4dLret*-oHWP#6ol2~qTq2S;Wk>9^+wX=CZE!Cm~;g^Xh z93=#wt0D*N!Z5B%vvj^*`0b9Q5M_anws1Z5uXyKvQ%vFG9?m3xNtpWinb?%^h+|(L zK&7TtqVtW*$#5%+zX+2QNEXF1gG9wdUfwq}$_vX^^J!jDiy4?T=`V&2%~9Z8YE4CcS;lM z_TRg;qwg+Jq-U5x^;e61C4W1)T>e=RzSt(2|9*RfU2Hn|uX7BjYs<(-MN$D9x5PRj zYyQ7uj`FTOOa>y9T$0~yTvITQ#(aXhyE}gETRG^aXt}CZmsMt#wY2&X#lZExDN^EN zJg03yrOAf#sk$I`>C5gg%?zW3I@M<8{uTfcyX^(b8h0LO zdD|p)6H~Ex$Biei;8`zP;s)Rjad>n@J(_7DdzRwSw;w+}LmN_23dUYRw>p7Gw( z=*DfxDgD}hdvfikv@kj2Y@XtPg_cRF_D57D^EN}fyYZkJ>sNyoS10#rJD%g@F{7N} z?W2oZmOYJ($Zngz`O&f9kJHBlT1`4aDyN-NOUa9n<5fk!zhj3ld$W zE^~Ha&8r79e2=Q=9ZW8Ap9%d~XC@0UcDj+kMn;MAW_0jx@1ps9dOd1g5|GMA^cUE+Nv&7Paq#VQ7yP9Ml>DPY;QQXVD2|rm$h` zaZl`?zqVTVZYxu@3j5sBp1*#m0dGo(ma4gWqZl*=mtJJ%Cc^c5Eb_R)@?lyYQC;#p z*!8U`GS?q(ZiF+?tB-3$67`!fVw|_B*!D1XL295_*=Un>8C)7aJF656SH1w)0Ier~ zXBY(8wB2&|McmGLi&etYA0q^_UQRcaQPJtnx9Cu8)um*!!c@aEUDb0A7ff`QZ~D92 zy5_1Px9~6mRA7TKCG&4O$~bbSw)T|O-O#F^<_^L7sPSb5rP750w;-mmKOCK-9)D7D z_EpeNlCBn716{4{E!qMfTSi|jy?+GBR+y*dPnisi*9YUMnH&@ig(!0EO8;sTtI5~k zY%sJcRADCtSu@j`EHliSsX&aR$;JvlSNHCkgvo*-*c3qhn832wr#yphDkuW@`YoK?ug&fb3` zNKlkR?nmt>`##U;a(#26%6xHw2fQ;-zk?=>#~sRNOpByA3jaWq)5X!U%v?OJ|4Y@iY!fkzs&UB`>+NFBvz}Pw+$;^Jt!S3Hlk*N=<)N{XVVL zE1BFXFERAk5)7st=-_%$m%cW&>Wb~%Sg>xN)6nuPb$id8we-cXKsEp)sQ?vvev#T; z7dP}koUIzoO1l&0@TQl`TSKD4re7}!J31OGZj#;iaovR(EN=E)oSwcTQG}lnHMxq2 zX7_+pn_R_XyO&R*(YF^l_5m!heRU(@0;#2MitD<6(-=^$SvOB6h4`e94yc#g35lA4 zuRN2aUZ-%YdrOUf1*>jrJC6ThJe9Fa#f=5njbv?zuVqciO8ohG%r(oX+Agx^`vGw1 zGf4=3wqP-nrXDBdy&B^lLCJ1&p!R_q?2U}Xhhq?R9kEEM0gefkJ>tbl{9-$zPN8`4 zKzOoWbRD+MU}s0yZ^!oSEX*SoVXsoj5L$b?7(pGc7kB>O*i5YOje>Lnu|$stPGYZA zEL|(Uzrfx!OLXNQFPmy=i|3+=2p)eHANIiWk#r}$oESPMT#i&-ytU^rz-dJzLVuX{ z4@L{DwedE8Co+w#M8;}#&HwC}ZR#LlnvtHeGTn)M=X6aCet}i8ia)=o)LX*s1LbC83wM+C pb2yfmrj%awc06RgvHvW`YGhd;{>@ZoM6u%4|Cm{%6IfV){{#Ae?ePEr literal 0 HcmV?d00001 diff --git a/data/sprites/ladx/Mario.bdiff b/data/sprites/ladx/Mario.bdiff new file mode 100644 index 0000000000000000000000000000000000000000..389fa94ab12862aef4cb035ff2ae688704ed25bd GIT binary patch literal 9025 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 XP>{Zp(BMK^XgL>hML1B900000cik=_ literal 0 HcmV?d00001 diff --git a/data/sprites/ladx/Richard.bdiff b/data/sprites/ladx/Richard.bdiff new file mode 100644 index 0000000000000000000000000000000000000000..a244d7026b42cfe902c28e77685720ed54ff2349 GIT binary patch literal 8207 zcmYkA2T&8t)`kN~=)n*|Z=rW-p%+6BkkCQ8G?6YKML;x^(2?E|si7!Rqzlq}uL6Q1 z0s_*D4aJ}DyLaaP@67JmIlHrGW@q-9caEBghK`mNQalC>_*d_N|A_!s;(r%vR<80A zs+N2vmhB&{eF4PSCx8E|roVr>lrN61zW=@dd%LUiiTR&UgD;1-7Cfdw?%%-H_V9B7 z=1K&>d76lj^xr%G8z2A(VEUgm&@ir49Z^W&%h#!N+s#;Us|+bL+Cl;@knDu;Jt6`Q zsfN*V|Dgq#B;B$r~I0z7RUF@HVkn8b3F%uP`;}E9bs+yQxLz0d5lu=&XD3T=7s-g=B z5PQS$hM_Wa+8Id_zDHUal6PI1s0aywK$t`F7gz)|hyl2KX!Q?#Pff$=z$3k=xTKjs zXbN>>e7dRut;YRGKF1GzKaS$B_LZJm@a7_p7(KU@c!d*xq0J4$O0Ot(D|hWKAB~gE zLcilRI*(6LdR%CjqpW0)UH?~6A!XmWxFVxUR}Yk1&Bgac zAnTMM{y8*Cdr?DR!w+Dbd*$x zembd9Ywj86jP%D1c4>z^m%%Q2((+A1p=bdFh!n1$=%cH{Hwz+B1)$QY2v4t~?y_Jxc zjuuoeGhm@90#!<6x|A>zL1JM}(0ac3qz1fi@d`o5U_x zPG%UOk?458%q&Si0u#MdUKAUbmu>vO?CWZxSM;E#7mGMzsi*rxRXPdoif z#tV-8jq??*qj!HO-svtO2N|rozm4FrJ(E(FoXpqk5Dd!jDrF`>q?rs%j zBv=xy+%hk93{qMG`M4-252aM`glAyv{fRe3(0X~?EB z7B^@!bQT8{qZidUDQEz|sNbrC7O}i(_ms5jZ>~zPenl%`8@A;+h+~_HJ?515we-+h zFJSaNWpPVH8MElE#x#uyPSHRfZgQN+BJul1qMDq{n6z0mXW0WSjD)!(2zQldDjl2) zA<=_Y+Gijk(HdUnlYKm?ksB}O>f0Wrm=8nrm=8e9gR!%QO~Ki>+-->eFRSvNfJ z*WvaHEl@*=tz~Q4yfkgsf8|NBEog1HAh`5L(QBUYyR=_VsR$=J$CjEMvmC5y?^v3s zKx%sLeLg)CuupkrmC3C{PvL?4{i&i3w*(jQM*2}QCiG|lZT0=T>&S6Uwc=sg@YK~c zf2B3mVKaR4LzE(_IwSt%77ft;Pj8e^w0uMl{{*m6sjnWxM2TAWIj5V}nJ7H|NZp$m zOVZpVu4m%x!&~?RgGzy|pG?WjT?wd&&-hBI`rAL|eR$kn!xHEtD8ZB@VR!MzQZ35795`ORe;8ISpexl+99jAKZsdjx6Yrwztv1-7o9M7tId8 zp*T-kBo~!wk$k7vFR;bs*`E^V(7x7?7|qRq(+mtE8PDLr_0eQTQ-J^iYO7#J+8i5 zKfI!Eoz_tUSs_&REx3>=n2N}x`T0bUAP^``ACnDlQN`qwARZGuIhf>8LdyTXHW!`N zE&%-TpRawbqyL2e%u|04{$FMt_8wQUh+{6d`2L`HVeP@2fC}xPMsG3twY9@{K^{TA zSH*Zj#vWs=a&+Vg7=}joFP|ebxX| zQMDHmjm;Q|Bh0TPA?M~<&>_)<1T1^$JaQ$Ey&~g8yC?_cR#6#(W3Q-mYmy;E8sf<} zF>|OHs?5U+#78A$9B&|qw12v!)%?@tbjWCaN2C5SB zn3+lb!4eV)V1_7b&68uHapGjeOh|VxBt+m_Tst^5#Typd$@l@T808#8^{0Y+ACh`-`(p9|B74#fy>dtB3(fv>9n9x&v0d$B;rD> z0bkrFWvyflTK5o8h;y>Zj568WtP?kub5DXjFJEU1Ibpz;KCdi|bafSv&O=T^kWNPF z^G-$2&)`dpz}&Bwf~heaxj6Ttl@C{k!`z~)iDndRwB8YfV&bn7lygF_at88_r;d^> z?L&^MPoswRza$4`dM4T!KXSXBK%wm%Jnlqf6o4C0C)g`!NO6SA&SHk5c3l}p7;b!I z7|-xo&`nOZCQ1@Wbd$G|%u*17rZY*EjMP-w(FF?@(+dk&Ws{jN*^OX?>Md=yDXPZe zu=v@OzYSXvL1A$OkDGtLE}U&=0S4L^`x7P=Z{)KqOAF1bMri%mTyEkjV?05}vaE32oMsi0TQu#0$IRm57Q7oPnV$qA$YW@Or zOEG593_6~>J+Q2#*UlkLQ<4Xhx~oq@EZ)QDc>|9MjKvwoNa%VNM{{Y~+aBN6*&d(k z9l;<>7M3#dr_%k&jO-tH@JW>mS1Rmt?$A|?>eHm&k*^t_ZhO!zU^TzfhoRPKM9Ak|XSztQ6Yk3WP z-bg>KmOIq!;to;n^gHGB?k^VwD%^z#h&5;<9uL!v>#4kB+Ohddtn-db&2syqvpFY? zY}Dt@2=y{_M*APh1?7Od;C#;M={|y{t9rk5A`#cf2tS%FPQ9Jga4v(A*inrn8hfQ7 zd#1tqp}gXJz3bDXaK zQ!v=1(@?|Ow>|W>$t_)_xd7SIpKSY|_Pz&1u}UQfGH4etzg8KCUv`cPdFK1fgsn-1 zH9A?i=NumPSLZyfyq#ZXR8px4*S@=6*t9$f%TZ(+*atr#ABYN4=#DfkIelZt-S(*d z$0@H~Yk_FN&Qbu9((Zxg-P#|9L%hlTuLV5fNt)!#B{iPX;3+5`HY6_BK8)t!Raaf) zo+(R9mI#$KmA{?4Z&T+#X~oTA>xXRj;LPFfR35|yDX6i-<3@95^3{)_OSs zOlLWkn0b${d40!*2FmZ>5`=wnU;}2H_BG~&OdI{mrN(;1Q=O>W4CV1_L&?aV@2_0! zI_F#CYhe7ZgThxnFFem~7EyhPRD7Xle;|6HMZUOJRzt#0nNb9j@LC7o3Y>^(bsL@Lvq?wSI%r<7;sm# zOm2MCN!q@A9;28D($+&YMUR~$`S15-Je@Sgq%f^kfS)CCREnG>kxvazLNubxgg<-1 z)84=Qp5NAZK*mkiY3YCb8=rHceGn;H$1kw!WdUt*#l+-_r?qOaX-}+OEZ4BAgqSNPM!L8 zHIH8p5}qezua&}Eq+k@?cl|+Nk#iE~!MZ$?Bw^o11HXIcP0xwnm0!&1XW~Yit@6@T zIEDRAS$2TB2dUuUUMKhg^)cm~fVA7pnYX~xkPpw3z{J6?IDLjSaAN2Eo3o5V`SVP! zk<{C@9wx_n%YCXkZbDxfcL>W`g%mG--6hG@=j0|3tcG{`3iE45CKZiLQ5Z z#sV*^3No_}=Bl6j(Rk{4fwGB8z##eGtkI97|De&FNXrTZ;%4dQ;H9EtC>ShAT*T>| z8=-m6<_^b_?x3_&hz%c}?`C3V)QiB!ey#G!4+|@9JzI0}G0`z%Dsj0-GZ2=N)rhtE zB#ZL>L7?&Us*8^uEA>a}pY11Yi|jOrWt}wE(&#&vgiK3$aM5Rg=h)taH<{&=BOl5k zH(aq(4(K!6x7C^RUZd3BI?aPoS9WibQtof8JY-|hf1HrEKk@EC31cJ~;Ut`_#AApI z>G76og3;&#`=j#Jn%0Vd2D*ic_83i?GLjr!wj0r~c2!VB1*vLuHu1d@bs~$>=ZOA;A$Vrtx_#mdXpa%CdjT2=Qi6-}S8`tTT zbYhO544*Zv2d&(t7VPeJC>KjSVOKR{)Nd_`nNl(laF7>%t^PR{WU^woy#A4dT9o`* z`07Dm^R_Sh4g-jj&W|=-sNP0Z$*oB@+#%*xHmSYu0&S})Ck|-FNv`c`6;quUXHF%%hzc= z?-|Y;bUFB9oT#go#o%gcj~Od)0<-hVX7n-K#;_qiV+3eE_BEy+t$s4yN@$qQYl+A; z`Bb;kK^MDnm|$6`Jc!-a=bBq(?cKMZv0Gf?A{kb(4ED3A+ML+xo%h)xGhssbQO#qp z1<|JRw$c1!(^KLAlkptU3?~l8zXjYnlRqCa0C}>vBu?Q2aJDo8eYz~o?q4QP{r4v# zuU86RKILtyUfyJ9zr~xJrs-cNAN*lxE9{ks-Mx^SQLnt93X`6<@T=;uJ0(@h8^oimcZA!!O;^ zdWq=Y3yQ~_il6o$G5#D~v0m@cI-(+ZqP*qrEBXvvltdr5(ny9<=;|>rXb3!_4o&tb znvavWe-712HrE9IM9NTo(u!vtvYLD&M#g>hs-5wp)}K|Y=+X^$*jda1QQ~-cS#Jtn zyt3WqfbOaQidTO&rIrf|KoO+k_b$A@yq6A+YU4&aL@CIWf1YAycAX?6=kB-XkmJ?_ z^ZH80G^9$L2EkrejLaMvc=C&_cDx(i+dl5C*w&XBl^o2$mGg(z`hdRf1vDmq4yqp| ze&&FvO65t|mF(_==!!SoywHK3gN7`v%N#v#xk;bpwCN>Qa+p0reL~59am$4K7n#f{ zS5%J*Ee|taD7o=mK|~Y98i%e{5IB{zD|_S2&{U@Z%+zJ2?ly~kML&I(X>KqeM=5q` z=(`-lA5219V=45MZ%?pm-7x-u6aB{HPNqLK)!O7af z$^I{Bk?3ZG>V$i)KZz=4hQxIT~fy?Jr zNja}+?krC)=KOmL*Q4nk!;9i7-Aegxg!Mhudu8J7P|L3}%1^UM(?$nVvno;Sxxx4w-P<462e|PBWBxR88k^ zA6koanBfuPG1qBfQ~E7##87G_;B%Q3mJBAo1-@nTC)9ze^->{3Jg&H7@d6yJtQ%YT zY=&zbj(V|}Hp%PS#xQ$nZ0r1Ggz!Xu$hFDRTw zwYO&7C4Q`|9h=@1OLq8TL*aVEzrMo4jE{McHZP$^NN2OMgF)_m$;C}v$1=<`J61gx z5LZ9%tAB65^%1K3{K2!rn@h8B(I?o=kGDgDoII&s9Xh<8)T zSc!}Ra^9TEzF$34AQ+8<5puH``Dx1%B%z!8i%G*@yG?nj%@8B*=qQa*;J5d&1}#Pw zQZKb{DeU?GA=OVn;~je6kLL)I+_X25cp!t*~GSbyy4{O#mTaw+sx#& zh!?RRKdGNccA>xX({B_*bO=5%r`Ehryv9bYWkE$tzYVwEt2WP1h|HnW7t2TnakSgS z5t$B(9u|ibu`Bu2iln%exjFm7 zp%pndzp03E7S`6iJN~R`^01z3yOTvI(}~YXiYxbGeX%}0uB@~)ZAtj^G)SGZ{hiOe z9ol^6Cv+@B|6cIc7M3gVCg0=r!>}-Jm~UAC+f-5U)tB8glZee+Z_pE)6^8wIEW))Q zwgc(g^13%l8amh!dM8RqZv6CTk^_UGQmnamFQxpigLg3%-4is)u$JY7Jo&?3b!!^C zST7@O0aDf#K-R>y3oTZ8XGoOTalLLpMNfj7Qm7{YpCGF{jR(DP7^PdH(RF2`*6Q5%FFkBKUo}~ zdb<8f5bDd>4?Ng^`Y_!2w-x1E^)G=%21>s8ZEppVx}moF)-rb;vO}unSN>8mi<3ao zJLRJ6M{ge@g~z9=klQ#RDoM?~Z@1{@*T{d!z1dO?z)x7q(;j~t=*nH*HMpWyam>*2 z2tD2*uNU>bN3YpKfJjoT-<;!@HWSYU*0Tz{i{G%>ot&#s2H08*X!Lps(FcDWu@AOwtm=tu`cwT;dbo$ z@mC`GMecxk7dfhX!_Z11a10qb0US%gR(IPED-$6M(aZF=Hyir69Ca-6B=oB9jpq)? zoW|W{fOX^se#(RkFo5$5|jpc!fP%&q2EI`NNN0tk`;lVAIDu)R$C7|Je;1 zO@!jFZR%kGB{*Cg3fl@QB=fe0lYMn*i~0Qv8w(_ zf{OCC&yr()I+*@h>w#(ajo3lm9zzE_e^(k;@k_= zMBE6xgPq8xl@;c#W}?jX03q9}`myLM|bTfi>#l`)?@y1cTE+&PU9 ziWj{#>UnVI%j>MoS*fW3+p5u7iTUqz>dj2d(dSd?Bv3QAScw1w7us$l{UIpRO`1!OFT3CGfF?@o*CQAwub8aEM{GBIRRp{Ttb`EfLe>yjBHXY z@NR{>Rup_@>f_w03zgJ!V^bfMv*kr}?l3wS%B4!lf7r4K=l2Krr>2muE_;@nK9nDRy`rl^yh3JRi3v@99`+b`po-Xzdp21!yU*MQ-@`~mS+2Z{)iclA2$G|} zZl?+al{*GplqfXivzw?DU4 zTh2mf4H&u*X#;^Rp%0yhQpG4s-#%i)rr%T&s)*Xm((Kwg{g4szHqg*jpacb)iSSeQ z`^N8Cep0f7omZ1jYve@SU7C%p-K-tm{0_h5hdVfm(F`Fxts9<>W$=r&(4LUz)8e7) z*b2iIB!8^?XKL{%6O?mw>hSgrxrsS4{dL+3FnsZ|q#F=pUN|FeR@SiOUyzn)e0bg0 z=c@aLh0uMLtR_7a*j|3@aoe`qq1v1Ssy62})Gc8=ZBABF@IhjzUWI-#IhL{SRt_H^ z5e5wIrtJ|MB99K$Ol4KGFRBn3qi-EFBzzs?MSjQQo09j&XsCY998HQ`^R(d^T&Oix2lo478;F&Nst5nNA6_*Hv{mb|5K=%p??3(iFZ})e=g#)-kFP%%EVl+`BJTcHvUaYC$a2GxK`0vK=v-4# zqOPhXxT`r2ef4W@qd5j)DctdYbqC}q>-zr^kEL9g9Bg~&hc-7Qc^*T$eGzn z6_750Bmszm@MRXVA`k%Jf=wg{3jl+W2v8&d03*YXfe5sbbaWsUPz+NU34o*w&w&KY zfY}KSV|bD>03Asb9eV-778b*cq(kCohR4R}01gNMb0X`NYYA?~C8NB?6=3IZ#Z-d@ z079e6kS+;yv}&Y4MzGz#8}L9~Gyy1-!774c4Y@V5C;scXfYpABi<9?gp5W-iFWLSP z=0hF5Bg#Q?ez@$!(OJnJvDSda+xV_yhq6>Vp1C3Ei0zNVrJ2_Bbi?5p61o!ZF6;bm z+rYkiGncHNEDD{!@lSon=yA`u76+22Rs0q{?d;)?shdMZGEu6=smzN@lF4*x0}Ka( zLf#`9R`O)L_X%noh=ffsJ~;{2>a^_!v}ecYcW2PK3MZ>XH}PtbyMJ2N_W;i)sURSB z^LQNeD8JSi0$J1hu5s_B))IBcs^~fe{qdZW>4to;ax!96;ag-+soPiJy|}06cd%n$ z^Xb|Uf{YYZZQ|CWK`&g1N=MwpEO>=c&Lif4yCNPxB?U)eAv5#G0tPMV!dzB;)*fV9oD^Br-^l-TA(z-PFG1_2fG0#(^q9 z=&zW}HErW`RFyPi{af)OFZIk6*$tQ5^tf0B%KUG9_NAE)lcW~6g=HdX*@e}eklKmq zmyGJJ8jjU*ZIumAeKWna*Dzp}*!I6tN@LatExsH+XPm3@KP$FJv%c9a_HGLJ<-0;Gd zO@k&r8C`XC&8a?g`p(BqtfiCqII{@R>3=&gqc+eOz{J>YG1D zZ5sA=mQyD`m?1MG;b@38AD2voTf^+V3p07i>f!l%Q-JtkDtN*@Xbq{bR`i<@%GT`g z9`0ErGzwi{n2O4+pcd`8CVAb}86g>pOOWvf$&ih>5mGT+B5k{+k-hAeh z2kNId>qI~E$g^&6xILpimm6ah_1#W~3A@jU2&#IBt#(v}x{#+E*}iT&u~%6Y-*I4H z0Y{Mv7ePP-PH$~m7S4Jcc+d(+hi2a2yUV($mFyS8qNRpE@nYErOZV?WC8VtLJC0n^Tlu zv6=A)Onn|15xzxkpypTM?+Q4M(b852gL%ZYY@@x{D9a5n?#NL^M_3iglJ(V1Uky22 z`hR%*Uswu$%i#k6(j)#+GUCs}puZGLnu{w|!|r!~ExXUD=;*!*4_dtYXz}mg8KC(& zcx>KHWfoeVOy?TFlb5&?|4lrfSq5Bad;k@pTbXypN*ZNGZMV3ja-da0fQp1 zNy^L90RSWbfd7y0bpJLYl)wo3*N9+CTROjq4gK(=nbXhAT14IF99AzWUG|>HdAGSg| zLk(yB#YncvPT;m@-?cvvuWU+!5A6kO+N7#5#Z$oDO@?^;rHHj?c*MrLMMOTA5x8RK z(=A(r6qYF7csV;8JBflwy#Zc32xA)SF3N>APZ?V*soc!*$efx@A)5@vPVa3bfqBYS zf>LEv2_STA(Ki`gF7wg z&Iccpt|8i*lTnUAg~Dm!=18VsRECBWz{c>{vggnENgMjbE?HuB^s9^rtu8pPgt<+d zw;1y`Z-XK4$`~bcDtuFhiHk7;CfB4 zRze1sicZ>TRPSb=Eew2ao4n{;)aTE!Pjl3PQl0c^7MrWag)ebzYqV;rEP1y%IbggK zLaYTHAhE_89WNh$I9=>Ex>A#5eJ^udT|(Kuy%-$J=>r!yW%|#O8!vw&W{? zoA@1QrwK09QPpbu;@e8kjrq-w1PewZ;Wq*<@5b%xd}vAbZqYm;!2}|n<2s1k(a7kL z8}9PI-kE%$H_;R2udWs!q2}{&lSkM`$5ajH2QxJuH`O_jH<#Kj*gvO;TsEam+<=U+ z=+&}3;M)3p_=RYA^NfejkP@H{&{;dZ_l~pDl4ud~YPj1=inE#usIprdabKh$A7#OR z{XOH{(v8~dT1D?;Po~IFY}D=HddB}{&8X~PGk{{_KS%ceYx+N@_UZqfeq?^q|FB7S zlDYhm+fmPlu!rCLm$IFE(+>OEAN8DGdn%uPx_9>D=bKO6L4==O-#^`X9{*D!=Somr zH$K*-Y6`2b3nkGf!(YSU2&k(f7Mo=>8wbzG61ZYP>Ko0tx@6Sof@Xi;te|DA6Efn z%Eka-2kuJj|6mOabOrw_zyN@f%72st06?jv1|$I067_MJN;~$t-};#O1rN1_VKw|% zXgqIdG@PA|MHdpy?#hm4w4uBrb+DPU1u>E&X6eR1!3_@z)gK}7g2)Vn5m8I%5P_Tv zO|3~sUOh2_96)wu{?Qo>m|q}b@L&LKBnVXo0P%)K0zi>?x_<$HM*_f9>bzJ|N5yyo zR*6Mq3vY)X&(O!FFU%h5PKkU(7l08ZBDDvxboB>_k~x?9jzWX7Yg#b2ZZ)-Pwi1g7 zEfJn~4K4U@q<@Bac1PpKG((lOl{M9IEz?G%Co(kh6DK`CecoIHF;n@f_jBa848S^_ zf@=jm0!Sf93Qv2V{v=H{hMI)}mtZ=c!Zn_)hk5|ltuS<=PVgt9fZEabWHewrq#mM9 zlDjnDNsaWWCldMO+i*EL%m(d97UM*v`@Y6}uq5Mf3`Vqm_US?vL#C#RF|T~=8HRR4 zQ-ixw`Sak7wXKga=p&2LmMeFBj`auaH)H6o(QkUw$xHS(aA z(pUR{5xyl>m6V8PE$D;J3%4OU-KNcDXR2m)ba3iQE!SR>8mR;-rbRRJ8B6ieNEh+UVRigm=jx%Z9>X6k zQ|o)iu>6skQSz$`sPhi@I+YS8`s|V5AYQow^6wCSg>5E@V@UUVqpuW&!kgXMc_`i@J9PNH zcJQAi@voYga@p=$AHc*ODfOETrVpGhgBzyQI)UROEt4PeQy&61%nw6>JNHG)`&%ij zTcosiHIvF8iqd{rK5&w~C16!#ev8^vRc5r%>e2}euVE^x4PIpU zRgvOhIw6&s0}Mcxp->piGFm3Js=i-?efHksgiSD!Yx8u(l5flG^+3z+-_f(TN?SLY z8k&NuZDmJ7VyUeLZf&2d4R0&C1fdnX-J{LsTKPHmyJ!HdViZHTK%ubVe`X{DH0C#`} zF>ZwtmgN~lPwh3i`W_xnwt5?l3~`M>clc`-aL9E`97T!TB_qN1`6TQ{c?q|n=4b74zyTO*J*eq9JLx;0ed1gWTdX6WG{>J_sFB;~Q z^PEf;NErk@p<#aSkwZ3+unz9?f7kFQ8#x|}prFpA;vS1tq_#@GVU^xO%NxTiv>X#V zw;WygV|L)%&^tPD{trExT53<_zqdo%S?fOF>?NP4K!T0qFZ~inZ@651WpnHesX=kR zggbhdDLJZsUIJ%k=2_lDY|uMIw`3{AdmBVcgxT!R*DQrWq;@f_$ub~`wOvC!OT)AJSq=8 zJup)o2#LX?r8bZVyTrOx+RjMS=F^zm3gIgBY-><^q8!r@V-2$Y{uxub$a$_Zxcn4v zxlqX<6t&D83Ym~U2}Y!1FJ1>+%I|Da!Kl>2RNLhKsbTm*Hud>sZ7iSG z^^%Itz~1E;A1dwXx(nk={A{wlL5%INsS_RTP0uuFNl`xfk8`^6(Vc2}7tbdw7JIP3MrU76 zgY5=ZdcVJCx~pLBn05kqKN*)+PdGUzs!IxSV&>A_yOwupW!@W%zgeqzR8+}uxX!_#C zst7N-ty0XNYH#*3ti;cC{MwSao=a}#BQjw8>nAf^_`s5}#ClQntc(4f^S%|1zQC@3$?Dn5;{TfHNZ z{##lD=;$U2xV7UbC@3FJu@>f?AEqj14j&T09$Wu* z`l&oL*LgjF_V_Ec`2qGNc;ts){#}NFV-ne0{bz6_kJufgav-6m$dbpsS9IDg=XGMW zMVynqpKN&-iLjlEzk%SY`^1<+hEJ)4)%czFP(fvQE-^@GOIgV){821vATa4Yfizu~ zf1FELv^($*z|HU}+5We{bP*qPgy-(N8*SC)AD!OIEw462TIZ^!_wQl6q`#{srQh)2d zzvolj+CfyaEZ$U4Axpk@X>U`k=|*{N{Ubc{jkoUbyKbUD9q~8Ul$8=iO!s%83QPVv zyM&?wq?QKJOUzazNz?X1@#MbR^OOt$S|5}4dS?oX&gu*Y`K8L76&X_PxMB1Yht~?Z zOKBx;XMv->wAGE?U->*SsC*ibTa;C3_Ffh>jdC(q#xAQDmq~-LSgUXbr8{TAW0lXq zwRAhiG67XN0dRJ|jLUFQdZc&XPRx#)un<0#z0n1n;$yG&&bG`yUD)E0OHzr=p5GG{ z_Qg64Emit1Iyc##fOX~3^$z}ShOe2lvY*$ORGD+Xg1LHXckHbfZc1ic2mLj-FB|tId?=-UsMg;U#yN3kqqi-y4MvfzRtbUenr}XGY_Vh7#fi2Q@;;&`);#s zQKr#1^J5=4y>&RcSvG-h36#+!7pr`{@CUuO&%*aL z&#HWz1H)Dlb(`8;yhtk=>h6tEv~h;uPf)Bjaj)Y*yX_;yn97dAXP*T+3Pi}Vgi8#g z9njHj-cOGGecJVnwBJ2I6@-5lHJzBEGHhsG9y|Az^TPCo=Wkq@H19;)^xM1;(J>1* zYxu10_$}1(=Gfny&=mlS;vC7v$aQE4A@E7@ArfDi{CLq=cm%d{`Re48^|R#>Af z*+vJG#^%7rutyiwOAsS*tNRs)Vx=ql1;OK0Aq7aE3YS)WsCjnF@5gvYIdkeCkg;zq@-&GUd&|4J_;p6JVDHQxjYsL>jfR>pPqj0S z6|UC`#Er@9<|rFKDli3(Pty3m>3x0f%27hNol9=!Mlo%ks*l*=-G+S8O~Ep)e!v-P zI|#j~Ntj;lp5~NwBuN3+iI!P)Fs3g45$^eQw;pOo3iYGOC^}f7oF=Tut`-`Zs@dcp z5Hht@R2iyNfd(Pt2Wj9i$H_&=;LyQKEbV-C{lXBkWu;ru*6%idbDQNc%f}zcN6vb} z)8*jm>H;vh!dWN1u*&x65yDr(NAZUSw)xby({;L7NuAnpq><%JC*fr!EW3U|=cynY z`7uj@R@0=Af~4{>Y_8F$uS-tML-P3^=FeA~@025sFXZb_d&f+DO;UEK#jpvL${Q_N zlLEstQF6M^CW7^QyeIPtbU*6rO)BLCpuTK!ES9><{56_p3yTXOnFW+pK#F%&G$vQ3 z$<$Yh%BeX;gK@P}qf9g1GQ%gmYBaIl`&^H*$GQ0ZQGT}MDGkpKd~^0|%qio5w#@`i zt$YY&63wifB@o8<#kT`zBU7pTQ*Uf`0B5oXTWRfR3>VU13$AsxCsA*9SumV$0CzsS z>b%ZJl^uIzu0HkMWMOsu3{O}eW>M}ro49KqK$8@>1XYOY+==+jAl*CMqbc;YwPn{R zt-$X4n(+;G{-6G`8S1a!zV(=Y*^lWEyxscpwA$+NkIBS};BekT;z(h=@X+Q3*O?bR z()=;@>0#!Mi-1Uw%&N`3hxL;(JC(7{CS%b~sX%9=;OOG`Pwmc%@G>&cz;QOY&t((y z?*2~l(ZIpjW4XH#!YKrfKn+~^GNYu3Bl=a$)SV_tebH1M-?0Y|SNJrB32AvbQL*%O z0{n!1BKN1TcTrmzZ?v)=@s@79n3c-Bg=Y4yHe;a6;ZWaLeHu2^)A>N|9V0$1g3pVj zlEUc(_sY3sVy>cje1UPc_%!AsF3J+hjr?}=046!n@5yO&bN(%|*bmMS&;M8zS62et z{7SWVR=T!#_x?#Vo=@wU^^hL#s1Sn>)gJ2o5D|q-N9?MkW0wY?wcbm6d*8!?U*fw6 zRHU%tMF!pnhF&k^B(A>cv?LGO*-{BBs&iPgN|7dnC~w3iY^-i+EfEsjxFdjrv2WPO zZN+)|CTzHkD!o8aY(-xa0&|{g3-p4)%@cHYX9{jwS3;j$)(+>WMN_8;+2W<$GsMN^ zFm;P4<9i4nv3|$S!V-yc_TN8l%7aVP7WV0)dF10vH`Sjw-H+!>vO_x)$60kP)CYhLxP0T^e6=oxMM=a6NclD@XU*Yw|DG8jmtop9Cz+nR5GGXl1gu_8E@n_IZ9W%@)qF zWtqcvqK#2bDV0%NdR;hC8Mc`7?o#iM;x6PssS2DH|US5>e;=7vvkhN9Y|>XmS{XSrKB?27z^veHuC8h*rp7qX zyxw9v$DJY8nCRZ{`00J^InrLMXMV`%lMF(S`5znp?G;((F21&T;9JU16O@3?<#s09 zu;?{?o!UFceuug@!#zYJ7q@=8ob%PViVTIr3lHXcHFf7(8QbqfZOa&PV+B?ZW81Ez zG{PM)P2DHxB)tx2KN&UEE@+!9>F}%DaWXMPh7#ViHHtxJem=5$WQN#F+JdBCZtYC2 z=$;d4^eBQHhRvq^R((qLg7|GmyMeq zEV-t6!ODUCm6!N<(j@-D7MQM;*A?tHc#5NOlS-RbmsEgzGAfGon8t2>RYI!xa5K$} zMJd)PCAK?8DQ35iIL8@2w?BLxJDV7hdKc{YHMOX!y!(ZWChMWBwAx6p23+S=)sSt@ z36!yjQERZ`?vF2{{dC_8ln-_Fayya@MJzjLk7Wd^n%P?K8)W2XioQq5TbNoY!v>&z z_v20N<}+ruvGr{gKIy6Y-tMC8&;1^%EEo2?#jyZ1htH~b#Pv7`VCcg?d%C%hbY?2K z;S~-Oo5wu}h@g4_rxwdsES)w#kEg$s>Z|s=bygn^ 0 then Result := redrom - else if Assigned(RedRomFilePage) then + else if Assigned(RedROMFilePage) then begin R := CompareStr(GetMD5OfFile(RedROMFilePage.Values[0]), '3d45c1ee9abd5738df46d2bdda8b57dc') if R <> 0 then @@ -592,7 +611,7 @@ function GetBlueROMPath(Param: string): string; begin if Length(bluerom) > 0 then Result := bluerom - else if Assigned(BlueRomFilePage) then + else if Assigned(BlueROMFilePage) then begin R := CompareStr(GetMD5OfFile(BlueROMFilePage.Values[0]), '50927e843568814f7ed45ec4f944bd8b') if R <> 0 then @@ -604,6 +623,22 @@ begin Result := ''; end; +function GetLADXROMPath(Param: string): string; +begin + if Length(ladxrom) > 0 then + Result := ladxrom + else if Assigned(LADXROMFilePage) then + begin + R := CompareStr(GetMD5OfFile(LADXROMFilePage.Values[0]), '07c211479386825042efb4ad31bb525f') + if R <> 0 then + MsgBox('Link''s Awakening DX ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); + + Result := LADXROMFilePage.Values[0] + end + else + Result := ''; + end; + procedure InitializeWizard(); begin AddOoTRomPage(); @@ -640,6 +675,10 @@ begin if Length(bluerom) = 0 then BlueROMFilePage:= AddGBRomPage('Pokemon Blue (UE) [S][!].gb'); + ladxrom := CheckRom('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc','07c211479386825042efb4ad31bb525f'); + if Length(ladxrom) = 0 then + LADXROMFilePage:= AddGBRomPage('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc'); + l2acrom := CheckRom('Lufia II - Rise of the Sinistrals (USA).sfc', '6efc477d6203ed2b3b9133c1cd9e9c5d'); if Length(l2acrom) = 0 then L2ACROMFilePage:= AddRomPage('Lufia II - Rise of the Sinistrals (USA).sfc'); @@ -669,4 +708,6 @@ begin Result := not (WizardIsComponentSelected('generator/pkmn_r') or WizardIsComponentSelected('client/pkmn/red')); if (assigned(BlueROMFilePage)) and (PageID = BlueROMFilePage.ID) then Result := not (WizardIsComponentSelected('generator/pkmn_b') or WizardIsComponentSelected('client/pkmn/blue')); + if (assigned(LADXROMFilePage)) and (PageID = LADXROMFilePage.ID) then + Result := not (WizardIsComponentSelected('generator/ladx') or WizardIsComponentSelected('client/ladx')); end; diff --git a/worlds/ladx/Common.py b/worlds/ladx/Common.py new file mode 100644 index 000000000000..e85e9767b91d --- /dev/null +++ b/worlds/ladx/Common.py @@ -0,0 +1,2 @@ +LINKS_AWAKENING = "Links Awakening DX" +BASE_ID = 10000000 \ No newline at end of file diff --git a/worlds/ladx/GpsTracker.py b/worlds/ladx/GpsTracker.py new file mode 100644 index 000000000000..1ea465eb162c --- /dev/null +++ b/worlds/ladx/GpsTracker.py @@ -0,0 +1,92 @@ +import json +roomAddress = 0xFFF6 +mapIdAddress = 0xFFF7 +indoorFlagAddress = 0xDBA5 +entranceRoomOffset = 0xD800 +screenCoordAddress = 0xFFFA + +mapMap = { + 0x00: 0x01, + 0x01: 0x01, + 0x02: 0x01, + 0x03: 0x01, + 0x04: 0x01, + 0x05: 0x01, + 0x06: 0x02, + 0x07: 0x02, + 0x08: 0x02, + 0x09: 0x02, + 0x0A: 0x02, + 0x0B: 0x02, + 0x0C: 0x02, + 0x0D: 0x02, + 0x0E: 0x02, + 0x0F: 0x02, + 0x10: 0x02, + 0x11: 0x02, + 0x12: 0x02, + 0x13: 0x02, + 0x14: 0x02, + 0x15: 0x02, + 0x16: 0x02, + 0x17: 0x02, + 0x18: 0x02, + 0x19: 0x02, + 0x1D: 0x01, + 0x1E: 0x01, + 0x1F: 0x01, + 0xFF: 0x03, +} + +class GpsTracker: + room = None + location_changed = False + screenX = 0 + screenY = 0 + indoors = None + + def __init__(self, gameboy) -> None: + self.gameboy = gameboy + + async def read_byte(self, b): + return (await self.gameboy.async_read_memory(b))[0] + + async def read_location(self): + indoors = await self.read_byte(indoorFlagAddress) + + if indoors != self.indoors and self.indoors != None: + self.indoorsChanged = True + + self.indoors = indoors + + mapId = await self.read_byte(mapIdAddress) + if mapId not in mapMap: + print(f'Unknown map ID {hex(mapId)}') + return + + mapDigit = mapMap[mapId] << 8 if indoors else 0 + last_room = self.room + self.room = await self.read_byte(roomAddress) + mapDigit + + coords = await self.read_byte(screenCoordAddress) + self.screenX = coords & 0x0F + self.screenY = (coords & 0xF0) >> 4 + + if (self.room != last_room): + self.location_changed = True + + last_message = {} + async def send_location(self, socket, diff=False): + if self.room is None: + return + message = { + "type":"location", + "refresh": True, + "version":"1.0", + "room": f'0x{self.room:02X}', + "x": self.screenX, + "y": self.screenY, + } + if message != self.last_message: + self.last_message = message + await socket.send(json.dumps(message)) diff --git a/worlds/ladx/ItemTracker.py b/worlds/ladx/ItemTracker.py new file mode 100644 index 000000000000..92ef71633e0f --- /dev/null +++ b/worlds/ladx/ItemTracker.py @@ -0,0 +1,283 @@ +import json +gameStateAddress = 0xDB95 +validGameStates = {0x0B, 0x0C} +gameStateResetThreshold = 0x06 + +inventorySlotCount = 16 +inventoryStartAddress = 0xDB00 +inventoryEndAddress = inventoryStartAddress + inventorySlotCount + +inventoryItemIds = { + 0x02: 'BOMB', + 0x05: 'BOW', + 0x06: 'HOOKSHOT', + 0x07: 'MAGIC_ROD', + 0x08: 'PEGASUS_BOOTS', + 0x09: 'OCARINA', + 0x0A: 'FEATHER', + 0x0B: 'SHOVEL', + 0x0C: 'MAGIC_POWDER', + 0x0D: 'BOOMERANG', + 0x0E: 'TOADSTOOL', + 0x0F: 'ROOSTER', +} + +dungeonKeyDoors = [ + { # D1 + 0xD907: [0x04], + 0xD909: [0x40], + 0xD90F: [0x01], + }, + { # D2 + 0xD921: [0x02], + 0xD925: [0x02], + 0xD931: [0x02], + 0xD932: [0x08], + 0xD935: [0x04], + }, + { # D3 + 0xD945: [0x40], + 0xD946: [0x40], + 0xD949: [0x40], + 0xD94A: [0x40], + 0xD956: [0x01, 0x02, 0x04, 0x08], + }, + { # D4 + 0xD969: [0x04], + 0xD96A: [0x40], + 0xD96E: [0x40], + 0xD978: [0x01], + 0xD979: [0x04], + }, + { # D5 + 0xD98C: [0x40], + 0xD994: [0x40], + 0xD99F: [0x04], + }, + { # D6 + 0xD9C3: [0x40], + 0xD9C6: [0x40], + 0xD9D0: [0x04], + }, + { # D7 + 0xDA10: [0x04], + 0xDA1E: [0x40], + 0xDA21: [0x40], + }, + { # D8 + 0xDA39: [0x02], + 0xDA3B: [0x01], + 0xDA42: [0x40], + 0xDA43: [0x40], + 0xDA44: [0x40], + 0xDA49: [0x40], + 0xDA4A: [0x01], + }, + { # D0(9) + 0xDDE5: [0x02], + 0xDDE9: [0x04], + 0xDDF0: [0x04], + }, +] + +dungeonItemAddresses = [ + 0xDB16, # D1 + 0xDB1B, # D2 + 0xDB20, # D3 + 0xDB25, # D4 + 0xDB2A, # D5 + 0xDB2F, # D6 + 0xDB34, # D7 + 0xDB39, # D8 + 0xDDDA, # Color Dungeon +] + +dungeonItemOffsets = { + 'MAP{}': 0, + 'COMPASS{}': 1, + 'STONE_BEAK{}': 2, + 'NIGHTMARE_KEY{}': 3, + 'KEY{}': 4, +} + +class Item: + def __init__(self, id, address, threshold=0, mask=None, increaseOnly=False, count=False, max=None): + self.id = id + self.address = address + self.threshold = threshold + self.mask = mask + self.increaseOnly = increaseOnly + self.count = count + self.value = 0 if increaseOnly else None + self.rawValue = 0 + self.diff = 0 + self.max = max + + def set(self, byte, extra): + oldValue = self.value + + if self.mask: + byte = byte & self.mask + + if not self.count: + byte = int(byte > self.threshold) + else: + # LADX seems to store one decimal digit per nibble + byte = byte - (byte // 16 * 6) + + byte += extra + + if self.max and byte > self.max: + byte = self.max + + if self.increaseOnly: + if byte > self.rawValue: + self.value += byte - self.rawValue + else: + self.value = byte + + self.rawValue = byte + + if oldValue != self.value: + self.diff += self.value - (oldValue or 0) + +class ItemTracker: + def __init__(self, gameboy) -> None: + self.gameboy = gameboy + self.loadItems() + pass + extraItems = {} + + async def readRamByte(self, byte): + return (await self.gameboy.read_memory_cache([byte]))[byte] + + def loadItems(self): + self.items = [ + Item('BOMB', None), + Item('BOW', None), + Item('HOOKSHOT', None), + Item('MAGIC_ROD', None), + Item('PEGASUS_BOOTS', None), + Item('OCARINA', None), + Item('FEATHER', None), + Item('SHOVEL', None), + Item('MAGIC_POWDER', None), + Item('BOOMERANG', None), + Item('TOADSTOOL', None), + Item('ROOSTER', None), + Item('SWORD', 0xDB4E, count=True), + Item('POWER_BRACELET', 0xDB43, count=True), + Item('SHIELD', 0xDB44, count=True), + Item('BOWWOW', 0xDB56), + Item('MAX_POWDER_UPGRADE', 0xDB76, threshold=0x20), + Item('MAX_BOMBS_UPGRADE', 0xDB77, threshold=0x30), + Item('MAX_ARROWS_UPGRADE', 0xDB78, threshold=0x30), + Item('TAIL_KEY', 0xDB11), + Item('SLIME_KEY', 0xDB15), + Item('ANGLER_KEY', 0xDB12), + Item('FACE_KEY', 0xDB13), + Item('BIRD_KEY', 0xDB14), + Item('FLIPPERS', 0xDB3E), + Item('SEASHELL', 0xDB41, count=True), + Item('GOLD_LEAF', 0xDB42, count=True, max=5), + Item('INSTRUMENT1', 0xDB65, mask=1 << 1), + Item('INSTRUMENT2', 0xDB66, mask=1 << 1), + Item('INSTRUMENT3', 0xDB67, mask=1 << 1), + Item('INSTRUMENT4', 0xDB68, mask=1 << 1), + Item('INSTRUMENT5', 0xDB69, mask=1 << 1), + Item('INSTRUMENT6', 0xDB6A, mask=1 << 1), + Item('INSTRUMENT7', 0xDB6B, mask=1 << 1), + Item('INSTRUMENT8', 0xDB6C, mask=1 << 1), + Item('TRADING_ITEM_YOSHI_DOLL', 0xDB40, mask=1 << 0), + Item('TRADING_ITEM_RIBBON', 0xDB40, mask=1 << 1), + Item('TRADING_ITEM_DOG_FOOD', 0xDB40, mask=1 << 2), + Item('TRADING_ITEM_BANANAS', 0xDB40, mask=1 << 3), + Item('TRADING_ITEM_STICK', 0xDB40, mask=1 << 4), + Item('TRADING_ITEM_HONEYCOMB', 0xDB40, mask=1 << 5), + Item('TRADING_ITEM_PINEAPPLE', 0xDB40, mask=1 << 6), + Item('TRADING_ITEM_HIBISCUS', 0xDB40, mask=1 << 7), + Item('TRADING_ITEM_LETTER', 0xDB7F, mask=1 << 0), + Item('TRADING_ITEM_BROOM', 0xDB7F, mask=1 << 1), + Item('TRADING_ITEM_FISHING_HOOK', 0xDB7F, mask=1 << 2), + Item('TRADING_ITEM_NECKLACE', 0xDB7F, mask=1 << 3), + Item('TRADING_ITEM_SCALE', 0xDB7F, mask=1 << 4), + Item('TRADING_ITEM_MAGNIFYING_GLASS', 0xDB7F, mask=1 << 5), + Item('SONG1', 0xDB49, mask=1 << 2), + Item('SONG2', 0xDB49, mask=1 << 1), + Item('SONG3', 0xDB49, mask=1 << 0), + Item('RED_TUNIC', 0xDB6D, mask=1 << 0), + Item('BLUE_TUNIC', 0xDB6D, mask=1 << 1), + Item('GREAT_FAIRY', 0xDDE1, mask=1 << 4), + ] + + for i in range(len(dungeonItemAddresses)): + for item, offset in dungeonItemOffsets.items(): + if item.startswith('KEY'): + self.items.append(Item(item.format(i + 1), dungeonItemAddresses[i] + offset, count=True)) + else: + self.items.append(Item(item.format(i + 1), dungeonItemAddresses[i] + offset)) + + self.itemDict = {item.id: item for item in self.items} + + async def readItems(state): + extraItems = state.extraItems + missingItems = {x for x in state.items if x.address == None} + + # Add keys for opened key doors + for i in range(len(dungeonKeyDoors)): + item = f'KEY{i + 1}' + extraItems[item] = 0 + + for address, masks in dungeonKeyDoors[i].items(): + for mask in masks: + value = await state.readRamByte(address) & mask + if value > 0: + extraItems[item] += 1 + + # Main inventory items + for i in range(inventoryStartAddress, inventoryEndAddress): + value = await state.readRamByte(i) + + if value in inventoryItemIds: + item = state.itemDict[inventoryItemIds[value]] + extra = extraItems[item.id] if item.id in extraItems else 0 + item.set(1, extra) + missingItems.remove(item) + + for item in missingItems: + extra = extraItems[item.id] if item.id in extraItems else 0 + item.set(0, extra) + + # All other items + for item in [x for x in state.items if x.address]: + extra = extraItems[item.id] if item.id in extraItems else 0 + item.set(await state.readRamByte(item.address), extra) + + async def sendItems(self, socket, diff=False): + if not self.items: + return + message = { + "type":"item", + "refresh": True, + "version":"1.0", + "diff": diff, + "items": [], + } + items = self.items + if diff: + items = [item for item in items if item.diff != 0] + if not items: + return + for item in items: + value = item.diff if diff else item.value + + message["items"].append( + { + 'id': item.id, + 'qty': value, + } + ) + + item.diff = 0 + + await socket.send(json.dumps(message)) \ No newline at end of file diff --git a/worlds/ladx/Items.py b/worlds/ladx/Items.py new file mode 100644 index 000000000000..ff5db6950a71 --- /dev/null +++ b/worlds/ladx/Items.py @@ -0,0 +1,304 @@ +from BaseClasses import Item, ItemClassification +from . import Common +import typing +from enum import IntEnum +from .LADXR.locations.constants import CHEST_ITEMS + +class ItemData(typing.NamedTuple): + item_name: str + ladxr_id: str + classification: ItemClassification + mark_only_first_progression: bool = False + created_for_players = set() + @property + def item_id(self): + return CHEST_ITEMS[self.ladxr_id] + + +class DungeonItemType(IntEnum): + INSTRUMENT = 0 + NIGHTMARE_KEY = 1 + KEY = 2 + STONE_BEAK = 3 + MAP = 4 + COMPASS = 5 + +class DungeonItemData(ItemData): + @property + def dungeon_index(self): + return int(self.ladxr_id[-1]) + + @property + def dungeon_item_type(self): + s = self.ladxr_id[:-1] + return DungeonItemType.__dict__[s] + +class LinksAwakeningItem(Item): + game: str = Common.LINKS_AWAKENING + + def __init__(self, item_data, world, player): + classification = item_data.classification + if callable(classification): + classification = classification(world, player) + # this doesn't work lol + MARK_FIRST_ITEM = False + if MARK_FIRST_ITEM: + if item_data.mark_only_first_progression: + if player in item_data.created_for_players: + classification = ItemClassification.filler + else: + item_data.created_for_players.add(player) + super().__init__(item_data.item_name, classification, Common.BASE_ID + item_data.item_id, player) + self.item_data = item_data + +# TODO: use _NAMES instead? +class ItemName: + POWER_BRACELET = "Progressive Power Bracelet" + SHIELD = "Progressive Shield" + BOW = "Bow" + HOOKSHOT = "Hookshot" + MAGIC_ROD = "Magic Rod" + PEGASUS_BOOTS = "Pegasus Boots" + OCARINA = "Ocarina" + FEATHER = "Feather" + SHOVEL = "Shovel" + MAGIC_POWDER = "Magic Powder" + BOMB = "Bomb" + SWORD = "Progressive Sword" + FLIPPERS = "Flippers" + MAGNIFYING_LENS = "Magnifying Lens" + MEDICINE = "Medicine" + TAIL_KEY = "Tail Key" + ANGLER_KEY = "Angler Key" + FACE_KEY = "Face Key" + BIRD_KEY = "Bird Key" + SLIME_KEY = "Slime Key" + GOLD_LEAF = "Gold Leaf" + RUPEES_20 = "20 Rupees" + RUPEES_50 = "50 Rupees" + RUPEES_100 = "100 Rupees" + RUPEES_200 = "200 Rupees" + RUPEES_500 = "500 Rupees" + SEASHELL = "Seashell" + MESSAGE = "Master Stalfos' Message" + GEL = "Gel" + BOOMERANG = "Boomerang" + HEART_PIECE = "Heart Piece" + BOWWOW = "BowWow" + ARROWS_10 = "10 Arrows" + SINGLE_ARROW = "Single Arrow" + ROOSTER = "Rooster" + MAX_POWDER_UPGRADE = "Max Powder Upgrade" + MAX_BOMBS_UPGRADE = "Max Bombs Upgrade" + MAX_ARROWS_UPGRADE = "Max Arrows Upgrade" + RED_TUNIC = "Red Tunic" + BLUE_TUNIC = "Blue Tunic" + HEART_CONTAINER = "Heart Container" + BAD_HEART_CONTAINER = "Bad Heart Container" + TOADSTOOL = "Toadstool" + KEY = "Key" + KEY1 = "Small Key (Tail Cave)" + KEY2 = "Small Key (Bottle Grotto)" + KEY3 = "Small Key (Key Cavern)" + KEY4 = "Small Key (Angler's Tunnel)" + KEY5 = "Small Key (Catfish's Maw)" + KEY6 = "Small Key (Face Shrine)" + KEY7 = "Small Key (Eagle's Tower)" + KEY8 = "Small Key (Turtle Rock)" + KEY9 = "Small Key (Color Dungeon)" + NIGHTMARE_KEY = "Nightmare Key" + NIGHTMARE_KEY1 = "Nightmare Key (Tail Cave)" + NIGHTMARE_KEY2 = "Nightmare Key (Bottle Grotto)" + NIGHTMARE_KEY3 = "Nightmare Key (Key Cavern)" + NIGHTMARE_KEY4 = "Nightmare Key (Angler's Tunnel)" + NIGHTMARE_KEY5 = "Nightmare Key (Catfish's Maw)" + NIGHTMARE_KEY6 = "Nightmare Key (Face Shrine)" + NIGHTMARE_KEY7 = "Nightmare Key (Eagle's Tower)" + NIGHTMARE_KEY8 = "Nightmare Key (Turtle Rock)" + NIGHTMARE_KEY9 = "Nightmare Key (Color Dungeon)" + MAP = "Map" + MAP1 = "Dungeon Map (Tail Cave)" + MAP2 = "Dungeon Map (Bottle Grotto)" + MAP3 = "Dungeon Map (Key Cavern)" + MAP4 = "Dungeon Map (Angler's Tunnel)" + MAP5 = "Dungeon Map (Catfish's Maw)" + MAP6 = "Dungeon Map (Face Shrine)" + MAP7 = "Dungeon Map (Eagle's Tower)" + MAP8 = "Dungeon Map (Turtle Rock)" + MAP9 = "Dungeon Map (Color Dungeon)" + COMPASS = "Compass" + COMPASS1 = "Compass (Tail Cave)" + COMPASS2 = "Compass (Bottle Grotto)" + COMPASS3 = "Compass (Key Cavern)" + COMPASS4 = "Compass (Angler's Tunnel)" + COMPASS5 = "Compass (Catfish's Maw)" + COMPASS6 = "Compass (Face Shrine)" + COMPASS7 = "Compass (Eagle's Tower)" + COMPASS8 = "Compass (Turtle Rock)" + COMPASS9 = "Compass (Color Dungeon)" + STONE_BEAK = "Stone Beak" + STONE_BEAK1 = "Stone Beak (Tail Cave)" + STONE_BEAK2 = "Stone Beak (Bottle Grotto)" + STONE_BEAK3 = "Stone Beak (Key Cavern)" + STONE_BEAK4 = "Stone Beak (Angler's Tunnel)" + STONE_BEAK5 = "Stone Beak (Catfish's Maw)" + STONE_BEAK6 = "Stone Beak (Face Shrine)" + STONE_BEAK7 = "Stone Beak (Eagle's Tower)" + STONE_BEAK8 = "Stone Beak (Turtle Rock)" + STONE_BEAK9 = "Stone Beak (Color Dungeon)" + SONG1 = "Ballad of the Wind Fish" + SONG2 = "Manbo's Mambo" + SONG3 = "Frog's Song of Soul" + INSTRUMENT1 = "Full Moon Cello" + INSTRUMENT2 = "Conch Horn" + INSTRUMENT3 = "Sea Lily's Bell" + INSTRUMENT4 = "Surf Harp" + INSTRUMENT5 = "Wind Marimba" + INSTRUMENT6 = "Coral Triangle" + INSTRUMENT7 = "Organ of Evening Calm" + INSTRUMENT8 = "Thunder Drum" + TRADING_ITEM_YOSHI_DOLL = "Yoshi Doll" + TRADING_ITEM_RIBBON = "Ribbon" + TRADING_ITEM_DOG_FOOD = "Dog Food" + TRADING_ITEM_BANANAS = "Bananas" + TRADING_ITEM_STICK = "Stick" + TRADING_ITEM_HONEYCOMB = "Honeycomb" + TRADING_ITEM_PINEAPPLE = "Pineapple" + TRADING_ITEM_HIBISCUS = "Hibiscus" + TRADING_ITEM_LETTER = "Letter" + TRADING_ITEM_BROOM = "Broom" + TRADING_ITEM_FISHING_HOOK = "Fishing Hook" + TRADING_ITEM_NECKLACE = "Necklace" + TRADING_ITEM_SCALE = "Scale" + TRADING_ITEM_MAGNIFYING_GLASS = "Magnifying Glass" + +trade_item_prog = ItemClassification.progression + +links_awakening_items = [ + ItemData(ItemName.POWER_BRACELET, "POWER_BRACELET", ItemClassification.progression), + ItemData(ItemName.SHIELD, "SHIELD", ItemClassification.progression), + ItemData(ItemName.BOW, "BOW", ItemClassification.progression), + ItemData(ItemName.HOOKSHOT, "HOOKSHOT", ItemClassification.progression), + ItemData(ItemName.MAGIC_ROD, "MAGIC_ROD", ItemClassification.progression), + ItemData(ItemName.PEGASUS_BOOTS, "PEGASUS_BOOTS", ItemClassification.progression), + ItemData(ItemName.OCARINA, "OCARINA", ItemClassification.progression), + ItemData(ItemName.FEATHER, "FEATHER", ItemClassification.progression), + ItemData(ItemName.SHOVEL, "SHOVEL", ItemClassification.progression), + ItemData(ItemName.MAGIC_POWDER, "MAGIC_POWDER", ItemClassification.progression, True), + ItemData(ItemName.BOMB, "BOMB", ItemClassification.progression, True), + ItemData(ItemName.SWORD, "SWORD", ItemClassification.progression), + ItemData(ItemName.FLIPPERS, "FLIPPERS", ItemClassification.progression), + ItemData(ItemName.MAGNIFYING_LENS, "MAGNIFYING_LENS", ItemClassification.progression), + ItemData(ItemName.MEDICINE, "MEDICINE", ItemClassification.useful), + ItemData(ItemName.TAIL_KEY, "TAIL_KEY", ItemClassification.progression), + ItemData(ItemName.ANGLER_KEY, "ANGLER_KEY", ItemClassification.progression), + ItemData(ItemName.FACE_KEY, "FACE_KEY", ItemClassification.progression), + ItemData(ItemName.BIRD_KEY, "BIRD_KEY", ItemClassification.progression), + ItemData(ItemName.SLIME_KEY, "SLIME_KEY", ItemClassification.progression), + ItemData(ItemName.GOLD_LEAF, "GOLD_LEAF", ItemClassification.progression), + ItemData(ItemName.RUPEES_20, "RUPEES_20", ItemClassification.filler), + ItemData(ItemName.RUPEES_50, "RUPEES_50", ItemClassification.useful), + ItemData(ItemName.RUPEES_100, "RUPEES_100", ItemClassification.progression_skip_balancing), + ItemData(ItemName.RUPEES_200, "RUPEES_200", ItemClassification.progression_skip_balancing), + ItemData(ItemName.RUPEES_500, "RUPEES_500", ItemClassification.progression_skip_balancing), + ItemData(ItemName.SEASHELL, "SEASHELL", ItemClassification.progression_skip_balancing), + ItemData(ItemName.MESSAGE, "MESSAGE", ItemClassification.progression), + ItemData(ItemName.GEL, "GEL", ItemClassification.trap), + ItemData(ItemName.BOOMERANG, "BOOMERANG", ItemClassification.progression), + ItemData(ItemName.HEART_PIECE, "HEART_PIECE", ItemClassification.filler), + ItemData(ItemName.BOWWOW, "BOWWOW", ItemClassification.progression), + ItemData(ItemName.ARROWS_10, "ARROWS_10", ItemClassification.filler), + ItemData(ItemName.SINGLE_ARROW, "SINGLE_ARROW", ItemClassification.filler), + ItemData(ItemName.ROOSTER, "ROOSTER", ItemClassification.progression), + ItemData(ItemName.MAX_POWDER_UPGRADE, "MAX_POWDER_UPGRADE", ItemClassification.filler), + ItemData(ItemName.MAX_BOMBS_UPGRADE, "MAX_BOMBS_UPGRADE", ItemClassification.filler), + ItemData(ItemName.MAX_ARROWS_UPGRADE, "MAX_ARROWS_UPGRADE", ItemClassification.filler), + ItemData(ItemName.RED_TUNIC, "RED_TUNIC", ItemClassification.useful), + ItemData(ItemName.BLUE_TUNIC, "BLUE_TUNIC", ItemClassification.useful), + ItemData(ItemName.HEART_CONTAINER, "HEART_CONTAINER", ItemClassification.useful), + #ItemData(ItemName.BAD_HEART_CONTAINER, "BAD_HEART_CONTAINER", ItemClassification.trap), + ItemData(ItemName.TOADSTOOL, "TOADSTOOL", ItemClassification.progression), + DungeonItemData(ItemName.KEY, "KEY", ItemClassification.progression), + DungeonItemData(ItemName.KEY1, "KEY1", ItemClassification.progression), + DungeonItemData(ItemName.KEY2, "KEY2", ItemClassification.progression), + DungeonItemData(ItemName.KEY3, "KEY3", ItemClassification.progression), + DungeonItemData(ItemName.KEY4, "KEY4", ItemClassification.progression), + DungeonItemData(ItemName.KEY5, "KEY5", ItemClassification.progression), + DungeonItemData(ItemName.KEY6, "KEY6", ItemClassification.progression), + DungeonItemData(ItemName.KEY7, "KEY7", ItemClassification.progression), + DungeonItemData(ItemName.KEY8, "KEY8", ItemClassification.progression), + DungeonItemData(ItemName.KEY9, "KEY9", ItemClassification.progression), + DungeonItemData(ItemName.NIGHTMARE_KEY, "NIGHTMARE_KEY", ItemClassification.progression), + DungeonItemData(ItemName.NIGHTMARE_KEY1, "NIGHTMARE_KEY1", ItemClassification.progression), + DungeonItemData(ItemName.NIGHTMARE_KEY2, "NIGHTMARE_KEY2", ItemClassification.progression), + DungeonItemData(ItemName.NIGHTMARE_KEY3, "NIGHTMARE_KEY3", ItemClassification.progression), + DungeonItemData(ItemName.NIGHTMARE_KEY4, "NIGHTMARE_KEY4", ItemClassification.progression), + DungeonItemData(ItemName.NIGHTMARE_KEY5, "NIGHTMARE_KEY5", ItemClassification.progression), + DungeonItemData(ItemName.NIGHTMARE_KEY6, "NIGHTMARE_KEY6", ItemClassification.progression), + DungeonItemData(ItemName.NIGHTMARE_KEY7, "NIGHTMARE_KEY7", ItemClassification.progression), + DungeonItemData(ItemName.NIGHTMARE_KEY8, "NIGHTMARE_KEY8", ItemClassification.progression), + DungeonItemData(ItemName.NIGHTMARE_KEY9, "NIGHTMARE_KEY9", ItemClassification.progression), + DungeonItemData(ItemName.MAP, "MAP", ItemClassification.filler), + DungeonItemData(ItemName.MAP1, "MAP1", ItemClassification.filler), + DungeonItemData(ItemName.MAP2, "MAP2", ItemClassification.filler), + DungeonItemData(ItemName.MAP3, "MAP3", ItemClassification.filler), + DungeonItemData(ItemName.MAP4, "MAP4", ItemClassification.filler), + DungeonItemData(ItemName.MAP5, "MAP5", ItemClassification.filler), + DungeonItemData(ItemName.MAP6, "MAP6", ItemClassification.filler), + DungeonItemData(ItemName.MAP7, "MAP7", ItemClassification.filler), + DungeonItemData(ItemName.MAP8, "MAP8", ItemClassification.filler), + DungeonItemData(ItemName.MAP9, "MAP9", ItemClassification.filler), + DungeonItemData(ItemName.COMPASS, "COMPASS", ItemClassification.filler), + DungeonItemData(ItemName.COMPASS1, "COMPASS1", ItemClassification.filler), + DungeonItemData(ItemName.COMPASS2, "COMPASS2", ItemClassification.filler), + DungeonItemData(ItemName.COMPASS3, "COMPASS3", ItemClassification.filler), + DungeonItemData(ItemName.COMPASS4, "COMPASS4", ItemClassification.filler), + DungeonItemData(ItemName.COMPASS5, "COMPASS5", ItemClassification.filler), + DungeonItemData(ItemName.COMPASS6, "COMPASS6", ItemClassification.filler), + DungeonItemData(ItemName.COMPASS7, "COMPASS7", ItemClassification.filler), + DungeonItemData(ItemName.COMPASS8, "COMPASS8", ItemClassification.filler), + DungeonItemData(ItemName.COMPASS9, "COMPASS9", ItemClassification.filler), + DungeonItemData(ItemName.STONE_BEAK, "STONE_BEAK", ItemClassification.filler), + DungeonItemData(ItemName.STONE_BEAK1, "STONE_BEAK1", ItemClassification.filler), + DungeonItemData(ItemName.STONE_BEAK2, "STONE_BEAK2", ItemClassification.filler), + DungeonItemData(ItemName.STONE_BEAK3, "STONE_BEAK3", ItemClassification.filler), + DungeonItemData(ItemName.STONE_BEAK4, "STONE_BEAK4", ItemClassification.filler), + DungeonItemData(ItemName.STONE_BEAK5, "STONE_BEAK5", ItemClassification.filler), + DungeonItemData(ItemName.STONE_BEAK6, "STONE_BEAK6", ItemClassification.filler), + DungeonItemData(ItemName.STONE_BEAK7, "STONE_BEAK7", ItemClassification.filler), + DungeonItemData(ItemName.STONE_BEAK8, "STONE_BEAK8", ItemClassification.filler), + DungeonItemData(ItemName.STONE_BEAK9, "STONE_BEAK9", ItemClassification.filler), + ItemData(ItemName.SONG1, "SONG1", ItemClassification.progression), + ItemData(ItemName.SONG2, "SONG2", ItemClassification.useful), + ItemData(ItemName.SONG3, "SONG3", ItemClassification.progression), + DungeonItemData(ItemName.INSTRUMENT1, "INSTRUMENT1", ItemClassification.progression), + DungeonItemData(ItemName.INSTRUMENT2, "INSTRUMENT2", ItemClassification.progression), + DungeonItemData(ItemName.INSTRUMENT3, "INSTRUMENT3", ItemClassification.progression), + DungeonItemData(ItemName.INSTRUMENT4, "INSTRUMENT4", ItemClassification.progression), + DungeonItemData(ItemName.INSTRUMENT5, "INSTRUMENT5", ItemClassification.progression), + DungeonItemData(ItemName.INSTRUMENT6, "INSTRUMENT6", ItemClassification.progression), + DungeonItemData(ItemName.INSTRUMENT7, "INSTRUMENT7", ItemClassification.progression), + DungeonItemData(ItemName.INSTRUMENT8, "INSTRUMENT8", ItemClassification.progression), + ItemData(ItemName.TRADING_ITEM_YOSHI_DOLL, "TRADING_ITEM_YOSHI_DOLL", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_RIBBON, "TRADING_ITEM_RIBBON", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_DOG_FOOD, "TRADING_ITEM_DOG_FOOD", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_BANANAS, "TRADING_ITEM_BANANAS", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_STICK, "TRADING_ITEM_STICK", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_HONEYCOMB, "TRADING_ITEM_HONEYCOMB", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_PINEAPPLE, "TRADING_ITEM_PINEAPPLE", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_HIBISCUS, "TRADING_ITEM_HIBISCUS", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_LETTER, "TRADING_ITEM_LETTER", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_BROOM, "TRADING_ITEM_BROOM", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_FISHING_HOOK, "TRADING_ITEM_FISHING_HOOK", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_NECKLACE, "TRADING_ITEM_NECKLACE", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_SCALE, "TRADING_ITEM_SCALE", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_MAGNIFYING_GLASS, "TRADING_ITEM_MAGNIFYING_GLASS", trade_item_prog) +] + +ladxr_item_to_la_item_name = { + item.ladxr_id: item.item_name for item in links_awakening_items +} + +links_awakening_items_by_name = { + item.item_name : item for item in links_awakening_items +} diff --git a/worlds/ladx/LADXR/.tinyci b/worlds/ladx/LADXR/.tinyci new file mode 100644 index 000000000000..292c64049600 --- /dev/null +++ b/worlds/ladx/LADXR/.tinyci @@ -0,0 +1,18 @@ +[tinyci] +enabled = True + +[build-test] +directory = _test +commands = + python3 ../main.py ../input.gbc --timeout 120 --output /dev/null + python3 ../main.py ../input.gbc --timeout 120 -s seashells=0 -s heartpiece=0 -s dungeon_items=keysanity --output /dev/null + python3 ../main.py ../input.gbc --timeout 120 -s logic=glitched -s dungeon_items=keysanity -s heartpiece=0 -s seashells=0 -s heartcontainers=0 -s instruments=1 -s owlstatues=both -s dungeonshuffle=1 -s witch=0 -s boomerang=gift -s steal=never -s goal=random --output /dev/null + python3 ../main.py ../input.gbc --timeout 120 -s logic=casual -s dungeon_items=keysy -s itempool=casual --output /dev/null + python3 ../main.py ../input.gbc --timeout 120 -s textmode=none --output /dev/null + python3 ../main.py ../input.gbc --timeout 120 -s overworld=dungeondive --output /dev/null +ignore = + python3 ../main.py ../input.gbc --timeout 120 --seashells --heartpiece --entranceshuffle simple --output /dev/null + python3 ../main.py ../input.gbc --timeout 120 --seashells --heartpiece --entranceshuffle advanced --output /dev/null + python3 ../main.py ../input.gbc --timeout 120 --seashells --heartpiece --entranceshuffle insanity --output /dev/null + python3 ../main.py ../input.gbc --timeout 120 --seashells --heartpiece --spoilerformat text --spoilerfilename /dev/null --output /dev/null + python3 ../main.py ../input.gbc --timeout 120 --seashells --heartpiece --spoilerformat json --spoilerfilename /dev/null --output /dev/null diff --git a/worlds/ladx/LADXR/LADXR_LICENSE b/worlds/ladx/LADXR/LADXR_LICENSE new file mode 100644 index 000000000000..3a83b309af9a --- /dev/null +++ b/worlds/ladx/LADXR/LADXR_LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Daid + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/worlds/ladx/LADXR/README.md b/worlds/ladx/LADXR/README.md new file mode 100644 index 000000000000..eeea602dafe2 --- /dev/null +++ b/worlds/ladx/LADXR/README.md @@ -0,0 +1,25 @@ +# Legend Of Zelda: Link's Awakening DX: Randomizer +Or, LADXR for short. + +## What is this? + +See https://daid.github.io/LADXR/ + +## Usage + +The only requirements are: to use python3, and the English v1.0 ROM for Links Awakening DX. + +The proper SHA-1 for the rom is `d90ac17e9bf17b6c61624ad9f05447bdb5efc01a`. + +Basic usage: +`python3 main.py zelda.gbc` + +The script will generate a new rom with item locations shuffled. There are many options, see `-h` on the script for details. + +## Development + +This is still in the early stage of development. Important bits are: +* `randomizer.py`: Contains the actual logic to randomize the rom, and checks to make sure it can be solved. +* `logic/*.py`: Contains the logic definitions of what connects to what in the world and what it requires to access that part. +* `locations/*.py`: Contains definitions of location types, and what items can be there. As well as the code on how to place an item there. For example the Chest class has a list of all items that can be in a chest. And the needed rom patch to put that an item in a specific chest. +* `patches/*.py`: Various patches on the code that are not directly related to a specific location. But more general fixes diff --git a/worlds/ladx/LADXR/assembler.py b/worlds/ladx/LADXR/assembler.py new file mode 100644 index 000000000000..07fcfde566f4 --- /dev/null +++ b/worlds/ladx/LADXR/assembler.py @@ -0,0 +1,845 @@ +import binascii +from typing import Optional, Dict, ItemsView, List, Union, Tuple +import unicodedata + +from . import utils +import re + + +REGS8 = {"A": 7, "B": 0, "C": 1, "D": 2, "E": 3, "H": 4, "L": 5, "[HL]": 6} +REGS16A = {"BC": 0, "DE": 1, "HL": 2, "SP": 3} +REGS16B = {"BC": 0, "DE": 1, "HL": 2, "AF": 3} +FLAGS = {"NZ": 0x00, "Z": 0x08, "NC": 0x10, "C": 0x18} + +CONST_MAP: Dict[str, int] = {} + + +class ExprBase: + def asReg8(self) -> Optional[int]: + return None + + def isA(self, kind: str, value: Optional[str] = None) -> bool: + return False + + +class Token(ExprBase): + def __init__(self, kind: str, value: Union[str, int], line_nr: int) -> None: + self.kind = kind + self.value = value + self.line_nr = line_nr + + def isA(self, kind: str, value: Optional[str] = None) -> bool: + return self.kind == kind and (value is None or value == self.value) + + def __repr__(self) -> str: + return "[%s:%s:%d]" % (self.kind, self.value, self.line_nr) + + def asReg8(self) -> Optional[int]: + if self.kind == 'ID': + return REGS8.get(str(self.value), None) + return None + + +class REF(ExprBase): + def __init__(self, expr: ExprBase) -> None: + self.expr = expr + + def asReg8(self) -> Optional[int]: + if self.expr.isA('ID', 'HL'): + return REGS8['[HL]'] + return None + + def __repr__(self) -> str: + return "[%s]" % (self.expr) + + +class OP(ExprBase): + def __init__(self, op: str, left: ExprBase, right: Optional[ExprBase] = None): + self.op = op + self.left = left + self.right = right + + def __repr__(self) -> str: + return "%s %s %s" % (self.left, self.op, self.right) + + @staticmethod + def make(op: str, left: ExprBase, right: Optional[ExprBase] = None) -> ExprBase: + if left.isA('NUMBER') and right is not None and right.isA('NUMBER'): + assert isinstance(right, Token) and isinstance(right.value, int) + assert isinstance(left, Token) and isinstance(left.value, int) + if op == '+': + left.value += right.value + return left + if op == '-': + left.value -= right.value + return left + if op == '*': + left.value *= right.value + return left + if op == '/': + left.value //= right.value + return left + if left.isA('NUMBER') and right is None: + assert isinstance(left, Token) and isinstance(left.value, int) + if op == '+': + return left + if op == '-': + left.value = -left.value + return left + return OP(op, left, right) + + +class Tokenizer: + TOKEN_REGEX = re.compile('|'.join('(?P<%s>%s)' % pair for pair in [ + ('NUMBER', r'\d+(\.\d*)?'), + ('HEX', r'\$[0-9A-Fa-f]+'), + ('ASSIGN', r':='), + ('COMMENT', r';[^\n]+'), + ('LABEL', r':'), + ('DIRECTIVE', r'#[A-Za-z_]+'), + ('STRING', '[a-zA-Z]?"[^"]*"'), + ('ID', r'\.?[A-Za-z_][A-Za-z0-9_\.]*'), + ('OP', r'[+\-*/,\(\)]'), + ('REFOPEN', r'\['), + ('REFCLOSE', r'\]'), + ('NEWLINE', r'\n'), + ('SKIP', r'[ \t]+'), + ('MISMATCH', r'.'), + ])) + + def __init__(self, code: str) -> None: + self.__tokens: List[Token] = [] + line_num = 1 + for mo in self.TOKEN_REGEX.finditer(code): + kind = mo.lastgroup + assert kind is not None + value: Union[str, int] = mo.group() + if kind == 'MISMATCH': + print(code.split("\n")[line_num-1]) + raise RuntimeError("Syntax error on line: %d: %s\n%s", line_num, value) + elif kind == 'SKIP': + pass + elif kind == 'COMMENT': + pass + else: + if kind == 'NUMBER': + value = int(value) + elif kind == 'HEX': + value = int(str(value)[1:], 16) + kind = 'NUMBER' + elif kind == 'ID': + value = str(value).upper() + self.__tokens.append(Token(kind, value, line_num)) + if kind == 'NEWLINE': + line_num += 1 + self.__tokens.append(Token('NEWLINE', '\n', line_num)) + + def peek(self) -> Token: + return self.__tokens[0] + + def pop(self) -> Token: + return self.__tokens.pop(0) + + def expect(self, kind: str, value: Optional[str] = None) -> None: + pop = self.pop() + if not pop.isA(kind, value): + if value is not None: + raise SyntaxError("%s != %s:%s" % (pop, kind, value)) + raise SyntaxError("%s != %s" % (pop, kind)) + + def __bool__(self) -> bool: + return bool(self.__tokens) + + +class Assembler: + SIMPLE_INSTR = { + 'NOP': 0x00, + 'RLCA': 0x07, + 'RRCA': 0x0F, + 'STOP': 0x010, + 'RLA': 0x17, + 'RRA': 0x1F, + 'DAA': 0x27, + 'CPL': 0x2F, + 'SCF': 0x37, + 'CCF': 0x3F, + 'HALT': 0x76, + 'RETI': 0xD9, + 'DI': 0xF3, + 'EI': 0xFB, + } + + LINK_REL8 = 0 + LINK_ABS8 = 1 + LINK_ABS16 = 2 + + def __init__(self, base_address: Optional[int] = None) -> None: + self.__base_address = base_address or -1 + self.__result = bytearray() + self.__label: Dict[str, int] = {} + self.__constant: Dict[str, int] = {} + self.__link: Dict[int, Tuple[int, ExprBase]] = {} + self.__scope: Optional[str] = None + + self.__tok = Tokenizer("") + + def process(self, code: str) -> None: + conditional_stack = [True] + self.__tok = Tokenizer(code) + try: + while self.__tok: + start = self.__tok.pop() + if start.kind == 'NEWLINE': + pass # Empty newline + elif start.kind == 'DIRECTIVE': + if start.value == '#IF': + t = self.parseExpression() + assert isinstance(t, Token) + conditional_stack.append(conditional_stack[-1] and t.value != 0) + self.__tok.expect('NEWLINE') + elif start.value == '#ELSE': + conditional_stack[-1] = not conditional_stack[-1] and conditional_stack[-2] + self.__tok.expect('NEWLINE') + elif start.value == '#ENDIF': + conditional_stack.pop() + assert conditional_stack + self.__tok.expect('NEWLINE') + else: + raise SyntaxError(start) + elif not conditional_stack[-1]: + while not self.__tok.pop().isA('NEWLINE'): + pass + elif start.kind == 'ID': + if start.value == 'DB': + self.instrDB() + self.__tok.expect('NEWLINE') + elif start.value == 'DW': + self.instrDW() + self.__tok.expect('NEWLINE') + elif start.value == 'LD': + self.instrLD() + self.__tok.expect('NEWLINE') + elif start.value == 'LDH': + self.instrLDH() + self.__tok.expect('NEWLINE') + elif start.value == 'LDI': + self.instrLDI() + self.__tok.expect('NEWLINE') + elif start.value == 'LDD': + self.instrLDD() + self.__tok.expect('NEWLINE') + elif start.value == 'INC': + self.instrINC() + self.__tok.expect('NEWLINE') + elif start.value == 'DEC': + self.instrDEC() + self.__tok.expect('NEWLINE') + elif start.value == 'ADD': + self.instrADD() + self.__tok.expect('NEWLINE') + elif start.value == 'ADC': + self.instrALU(0x88) + self.__tok.expect('NEWLINE') + elif start.value == 'SUB': + self.instrALU(0x90) + self.__tok.expect('NEWLINE') + elif start.value == 'SBC': + self.instrALU(0x98) + self.__tok.expect('NEWLINE') + elif start.value == 'AND': + self.instrALU(0xA0) + self.__tok.expect('NEWLINE') + elif start.value == 'XOR': + self.instrALU(0xA8) + self.__tok.expect('NEWLINE') + elif start.value == 'OR': + self.instrALU(0xB0) + self.__tok.expect('NEWLINE') + elif start.value == 'CP': + self.instrALU(0xB8) + self.__tok.expect('NEWLINE') + elif start.value == 'BIT': + self.instrBIT(0x40) + self.__tok.expect('NEWLINE') + elif start.value == 'RES': + self.instrBIT(0x80) + self.__tok.expect('NEWLINE') + elif start.value == 'SET': + self.instrBIT(0xC0) + self.__tok.expect('NEWLINE') + elif start.value == 'RET': + self.instrRET() + self.__tok.expect('NEWLINE') + elif start.value == 'CALL': + self.instrCALL() + self.__tok.expect('NEWLINE') + elif start.value == 'RLC': + self.instrCB(0x00) + self.__tok.expect('NEWLINE') + elif start.value == 'RRC': + self.instrCB(0x08) + self.__tok.expect('NEWLINE') + elif start.value == 'RL': + self.instrCB(0x10) + self.__tok.expect('NEWLINE') + elif start.value == 'RR': + self.instrCB(0x18) + self.__tok.expect('NEWLINE') + elif start.value == 'SLA': + self.instrCB(0x20) + self.__tok.expect('NEWLINE') + elif start.value == 'SRA': + self.instrCB(0x28) + self.__tok.expect('NEWLINE') + elif start.value == 'SWAP': + self.instrCB(0x30) + self.__tok.expect('NEWLINE') + elif start.value == 'SRL': + self.instrCB(0x38) + self.__tok.expect('NEWLINE') + elif start.value == 'RST': + self.instrRST() + self.__tok.expect('NEWLINE') + elif start.value == 'JP': + self.instrJP() + self.__tok.expect('NEWLINE') + elif start.value == 'JR': + self.instrJR() + self.__tok.expect('NEWLINE') + elif start.value == 'PUSH': + self.instrPUSHPOP(0xC5) + self.__tok.expect('NEWLINE') + elif start.value == 'POP': + self.instrPUSHPOP(0xC1) + self.__tok.expect('NEWLINE') + elif start.value in self.SIMPLE_INSTR: + self.__result.append(self.SIMPLE_INSTR[str(start.value)]) + self.__tok.expect('NEWLINE') + elif self.__tok.peek().kind == 'LABEL': + self.__tok.pop() + self.addLabel(str(start.value)) + elif self.__tok.peek().kind == 'ASSIGN': + self.__tok.pop() + value = self.__tok.pop() + if value.kind != 'NUMBER': + raise SyntaxError(start) + self.addConstant(str(start.value), int(value.value)) + else: + raise SyntaxError(start) + else: + raise SyntaxError(start) + except SyntaxError: + print("Syntax error on line: %s" % code.split("\n")[self.__tok.peek().line_nr-1]) + raise + + def insert8(self, expr: ExprBase) -> None: + if expr.isA('NUMBER'): + assert isinstance(expr, Token) + value = int(expr.value) + else: + self.__link[len(self.__result)] = (Assembler.LINK_ABS8, expr) + value = 0 + assert 0 <= value < 256 + self.__result.append(value) + + def insertRel8(self, expr: ExprBase) -> None: + if expr.isA('NUMBER'): + assert isinstance(expr, Token) + self.__result.append(int(expr.value)) + else: + self.__link[len(self.__result)] = (Assembler.LINK_REL8, expr) + self.__result.append(0x00) + + def insert16(self, expr: ExprBase) -> None: + if expr.isA('NUMBER'): + assert isinstance(expr, Token) + value = int(expr.value) + else: + self.__link[len(self.__result)] = (Assembler.LINK_ABS16, expr) + value = 0 + assert 0 <= value <= 0xFFFF + self.__result.append(value & 0xFF) + self.__result.append(value >> 8) + + def insertString(self, string: str) -> None: + if string.startswith('"') and string.endswith('"'): + string = string[1:-1] + string = unicodedata.normalize('NFKD', string) + self.__result += string.encode("latin1", "ignore") + elif string.startswith("m\"") and string.endswith("\""): + self.__result += utils.formatText(string[2:-1].replace("|", "\n")) + else: + raise SyntaxError + + def instrLD(self) -> None: + left_param = self.parseParam() + self.__tok.expect('OP', ',') + right_param = self.parseParam() + lr8 = left_param.asReg8() + rr8 = right_param.asReg8() + if lr8 is not None and rr8 is not None: + self.__result.append(0x40 | (lr8 << 3) | rr8) + elif left_param.isA('ID', 'A') and isinstance(right_param, REF): + if right_param.expr.isA('ID', 'BC'): + self.__result.append(0x0A) + elif right_param.expr.isA('ID', 'DE'): + self.__result.append(0x1A) + elif right_param.expr.isA('ID', 'HL+'): # TODO + self.__result.append(0x2A) + elif right_param.expr.isA('ID', 'HL-'): # TODO + self.__result.append(0x3A) + elif right_param.expr.isA('ID', 'C'): + self.__result.append(0xF2) + else: + self.__result.append(0xFA) + self.insert16(right_param.expr) + elif right_param.isA('ID', 'A') and isinstance(left_param, REF): + if left_param.expr.isA('ID', 'BC'): + self.__result.append(0x02) + elif left_param.expr.isA('ID', 'DE'): + self.__result.append(0x12) + elif left_param.expr.isA('ID', 'HL+'): # TODO + self.__result.append(0x22) + elif left_param.expr.isA('ID', 'HL-'): # TODO + self.__result.append(0x32) + elif left_param.expr.isA('ID', 'C'): + self.__result.append(0xE2) + else: + self.__result.append(0xEA) + self.insert16(left_param.expr) + elif left_param.isA('ID', 'BC'): + self.__result.append(0x01) + self.insert16(right_param) + elif left_param.isA('ID', 'DE'): + self.__result.append(0x11) + self.insert16(right_param) + elif left_param.isA('ID', 'HL'): + self.__result.append(0x21) + self.insert16(right_param) + elif left_param.isA('ID', 'SP'): + if right_param.isA('ID', 'HL'): + self.__result.append(0xF9) + else: + self.__result.append(0x31) + self.insert16(right_param) + elif right_param.isA('ID', 'SP') and isinstance(left_param, REF): + self.__result.append(0x08) + self.insert16(left_param.expr) + elif lr8 is not None: + self.__result.append(0x06 | (lr8 << 3)) + self.insert8(right_param) + else: + raise SyntaxError + + def instrLDH(self) -> None: + left_param = self.parseParam() + self.__tok.expect('OP', ',') + right_param = self.parseParam() + if left_param.isA('ID', 'A') and isinstance(right_param, REF): + if right_param.expr.isA('ID', 'C'): + self.__result.append(0xF2) + else: + self.__result.append(0xF0) + self.insert8(right_param.expr) + elif right_param.isA('ID', 'A') and isinstance(left_param, REF): + if left_param.expr.isA('ID', 'C'): + self.__result.append(0xE2) + else: + self.__result.append(0xE0) + self.insert8(left_param.expr) + else: + raise SyntaxError + + def instrLDI(self) -> None: + left_param = self.parseParam() + self.__tok.expect('OP', ',') + right_param = self.parseParam() + if left_param.isA('ID', 'A') and isinstance(right_param, REF) and right_param.expr.isA('ID', 'HL'): + self.__result.append(0x2A) + elif right_param.isA('ID', 'A') and isinstance(left_param, REF) and left_param.expr.isA('ID', 'HL'): + self.__result.append(0x22) + else: + raise SyntaxError + + def instrLDD(self) -> None: + left_param = self.parseParam() + self.__tok.expect('OP', ',') + right_param = self.parseParam() + if left_param.isA('ID', 'A') and isinstance(right_param, REF) and right_param.expr.isA('ID', 'HL'): + self.__result.append(0x3A) + elif right_param.isA('ID', 'A') and isinstance(left_param, REF) and left_param.expr.isA('ID', 'HL'): + self.__result.append(0x32) + else: + raise SyntaxError + + def instrINC(self) -> None: + param = self.parseParam() + r8 = param.asReg8() + if r8 is not None: + self.__result.append(0x04 | (r8 << 3)) + elif param.isA('ID', 'BC'): + self.__result.append(0x03) + elif param.isA('ID', 'DE'): + self.__result.append(0x13) + elif param.isA('ID', 'HL'): + self.__result.append(0x23) + elif param.isA('ID', 'SP'): + self.__result.append(0x33) + else: + raise SyntaxError + + def instrDEC(self) -> None: + param = self.parseParam() + r8 = param.asReg8() + if r8 is not None: + self.__result.append(0x05 | (r8 << 3)) + elif param.isA('ID', 'BC'): + self.__result.append(0x0B) + elif param.isA('ID', 'DE'): + self.__result.append(0x1B) + elif param.isA('ID', 'HL'): + self.__result.append(0x2B) + elif param.isA('ID', 'SP'): + self.__result.append(0x3B) + else: + raise SyntaxError + + def instrADD(self) -> None: + left_param = self.parseParam() + self.__tok.expect('OP', ',') + right_param = self.parseParam() + + if left_param.isA('ID', 'A'): + rr8 = right_param.asReg8() + if rr8 is not None: + self.__result.append(0x80 | rr8) + else: + self.__result.append(0xC6) + self.insert8(right_param) + elif left_param.isA('ID', 'HL') and right_param.isA('ID') and isinstance(right_param, Token) and right_param.value in REGS16A: + self.__result.append(0x09 | REGS16A[str(right_param.value)] << 4) + elif left_param.isA('ID', 'SP'): + self.__result.append(0xE8) + self.insert8(right_param) + else: + raise SyntaxError + + def instrALU(self, code_value: int) -> None: + param = self.parseParam() + if param.isA('ID', 'A') and self.__tok.peek().isA('OP', ','): + self.__tok.pop() + param = self.parseParam() + r8 = param.asReg8() + if r8 is not None: + self.__result.append(code_value | r8) + else: + self.__result.append(code_value | 0x46) + self.insert8(param) + + def instrRST(self) -> None: + param = self.parseParam() + if param.isA('NUMBER') and isinstance(param, Token) and (int(param.value) & ~0x38) == 0: + self.__result.append(0xC7 | int(param.value)) + else: + raise SyntaxError + + def instrPUSHPOP(self, code_value: int) -> None: + param = self.parseParam() + if param.isA('ID') and isinstance(param, Token) and str(param.value) in REGS16B: + self.__result.append(code_value | (REGS16B[str(param.value)] << 4)) + else: + raise SyntaxError + + def instrJR(self) -> None: + param = self.parseParam() + if self.__tok.peek().isA('OP', ','): + self.__tok.pop() + condition = param + param = self.parseParam() + if condition.isA('ID') and isinstance(condition, Token) and str(condition.value) in FLAGS: + self.__result.append(0x20 | FLAGS[str(condition.value)]) + else: + raise SyntaxError + else: + self.__result.append(0x18) + self.insertRel8(param) + + def instrCB(self, code_value: int) -> None: + param = self.parseParam() + r8 = param.asReg8() + if r8 is not None: + self.__result.append(0xCB) + self.__result.append(code_value | r8) + else: + raise SyntaxError + + def instrBIT(self, code_value: int) -> None: + left_param = self.parseParam() + self.__tok.expect('OP', ',') + right_param = self.parseParam() + rr8 = right_param.asReg8() + if left_param.isA('NUMBER') and isinstance(left_param, Token) and rr8 is not None: + self.__result.append(0xCB) + self.__result.append(code_value | (int(left_param.value) << 3) | rr8) + else: + raise SyntaxError + + def instrRET(self) -> None: + if self.__tok.peek().isA('ID'): + condition = self.__tok.pop() + if condition.isA('ID') and condition.value in FLAGS: + self.__result.append(0xC0 | FLAGS[str(condition.value)]) + else: + raise SyntaxError + else: + self.__result.append(0xC9) + + def instrCALL(self) -> None: + param = self.parseParam() + if self.__tok.peek().isA('OP', ','): + self.__tok.pop() + condition = param + param = self.parseParam() + if condition.isA('ID') and isinstance(condition, Token) and condition.value in FLAGS: + self.__result.append(0xC4 | FLAGS[str(condition.value)]) + else: + raise SyntaxError + else: + self.__result.append(0xCD) + self.insert16(param) + + def instrJP(self) -> None: + param = self.parseParam() + if self.__tok.peek().isA('OP', ','): + self.__tok.pop() + condition = param + param = self.parseParam() + if condition.isA('ID') and isinstance(condition, Token) and condition.value in FLAGS: + self.__result.append(0xC2 | FLAGS[str(condition.value)]) + else: + raise SyntaxError + elif param.isA('ID', 'HL'): + self.__result.append(0xE9) + return + else: + self.__result.append(0xC3) + self.insert16(param) + + def instrDW(self) -> None: + param = self.parseExpression() + self.insert16(param) + while self.__tok.peek().isA('OP', ','): + self.__tok.pop() + param = self.parseExpression() + self.insert16(param) + + def instrDB(self) -> None: + param = self.parseExpression() + if param.isA('STRING'): + assert isinstance(param, Token) + self.insertString(str(param.value)) + else: + self.insert8(param) + while self.__tok.peek().isA('OP', ','): + self.__tok.pop() + param = self.parseExpression() + if param.isA('STRING'): + assert isinstance(param, Token) + self.insertString(str(param.value)) + else: + self.insert8(param) + + def addLabel(self, label: str) -> None: + if label.startswith("."): + assert self.__scope is not None + label = self.__scope + label + else: + assert "." not in label, label + self.__scope = label + assert label not in self.__label, "Duplicate label: %s" % (label) + assert label not in self.__constant, "Duplicate label: %s" % (label) + self.__label[label] = len(self.__result) + + def addConstant(self, name: str, value: int) -> None: + assert name not in self.__constant, "Duplicate constant: %s" % (name) + assert name not in self.__label, "Duplicate constant: %s" % (name) + self.__constant[name] = value + + def parseParam(self) -> ExprBase: + t = self.__tok.peek() + if t.kind == 'REFOPEN': + self.__tok.pop() + expr = self.parseExpression() + self.__tok.expect('REFCLOSE') + return REF(expr) + return self.parseExpression() + + def parseExpression(self) -> ExprBase: + t = self.parseAddSub() + return t + + def parseAddSub(self) -> ExprBase: + t = self.parseFactor() + p = self.__tok.peek() + if p.isA('OP', '+') or p.isA('OP', '-'): + self.__tok.pop() + return OP.make(str(p.value), t, self.parseAddSub()) + return t + + def parseFactor(self) -> ExprBase: + t = self.parseUnary() + p = self.__tok.peek() + if p.isA('OP', '*') or p.isA('OP', '/'): + self.__tok.pop() + return OP.make(str(p.value), t, self.parseFactor()) + return t + + def parseUnary(self) -> ExprBase: + t = self.__tok.pop() + if t.isA('OP', '-') or t.isA('OP', '+'): + return OP.make(str(t.value), self.parseUnary()) + elif t.isA('OP', '('): + result = self.parseExpression() + self.__tok.expect('OP', ')') + return result + if t.kind not in ('ID', 'NUMBER', 'STRING'): + raise SyntaxError + if t.isA('ID') and t.value in CONST_MAP: + t.kind = 'NUMBER' + t.value = CONST_MAP[str(t.value)] + elif t.isA('ID') and t.value in self.__constant: + t.kind = 'NUMBER' + t.value = self.__constant[str(t.value)] + elif t.isA('ID') and str(t.value).startswith("."): + assert self.__scope is not None + t.value = self.__scope + str(t.value) + return t + + def link(self) -> None: + for offset, (link_type, link_expr) in self.__link.items(): + expr = self.resolveExpr(link_expr) + assert expr is not None + assert expr.isA('NUMBER'), expr + assert isinstance(expr, Token) + value = int(expr.value) + if link_type == Assembler.LINK_REL8: + byte = (value - self.__base_address) - offset - 1 + assert -128 <= byte <= 127, expr + self.__result[offset] = byte & 0xFF + elif link_type == Assembler.LINK_ABS8: + assert 0 <= value <= 0xFF + self.__result[offset] = value & 0xFF + elif link_type == Assembler.LINK_ABS16: + assert self.__base_address >= 0, "Cannot place absolute values in a relocatable code piece" + assert 0 <= value <= 0xFFFF + self.__result[offset] = value & 0xFF + self.__result[offset + 1] = value >> 8 + else: + raise RuntimeError + + def resolveExpr(self, expr: Optional[ExprBase]) -> Optional[ExprBase]: + if expr is None: + return None + elif isinstance(expr, OP): + left = self.resolveExpr(expr.left) + assert left is not None + return OP.make(expr.op, left, self.resolveExpr(expr.right)) + elif isinstance(expr, Token) and expr.isA('ID') and isinstance(expr, Token) and expr.value in self.__label: + return Token('NUMBER', self.__label[str(expr.value)] + self.__base_address, expr.line_nr) + return expr + + def getResult(self) -> bytearray: + return self.__result + + def getLabels(self) -> ItemsView[str, int]: + return self.__label.items() + + +def const(name: str, value: int) -> None: + name = name.upper() + assert name not in CONST_MAP + CONST_MAP[name] = value + + +def resetConsts() -> None: + CONST_MAP.clear() + + +def ASM(code: str, base_address: Optional[int] = None, labels_result: Optional[Dict[str, int]] = None) -> bytes: + asm = Assembler(base_address) + asm.process(code) + asm.link() + if labels_result is not None: + assert base_address is not None + for label, offset in asm.getLabels(): + labels_result[label] = base_address + offset + return binascii.hexlify(asm.getResult()) + + +def allOpcodesTest() -> None: + import json + opcodes = json.load(open("Opcodes.json", "rt")) + for label in (False, True): + for prefix, codes in opcodes.items(): + for num, op in codes.items(): + if op['mnemonic'].startswith('ILLEGAL_') or op['mnemonic'] == 'PREFIX': + continue + params = [] + postfix = '' + for o in op['operands']: + name = o['name'] + if name == 'd16' or name == 'a16': + if label: + name = 'LABEL' + else: + name = '$0000' + if name == 'd8' or name == 'a8': + name = '$00' + if name == 'r8': + if label and num != '0xE8': + name = 'LABEL' + else: + name = '$00' + if name[-1] == 'H' and name[0].isnumeric(): + name = '$' + name[:-1] + if o['immediate']: + params.append(name) + else: + params.append("[%s]" % (name)) + if 'increment' in o and o['increment']: + postfix = 'I' + if 'decrement' in o and o['decrement']: + postfix = 'D' + code = op["mnemonic"] + postfix + " " + ", ".join(params) + code = code.strip() + try: + data = ASM("LABEL:\n%s" % (code), 0x0000) + if prefix == 'cbprefixed': + assert data[0:2] == b'cb' + data = data[2:] + assert data[0:2] == num[2:].encode('ascii').lower(), data[0:2] + b"!=" + num[2:].encode('ascii').lower() + except Exception as e: + print("%s\t\t|%r|\t%s" % (code, e, num)) + print(op) + + +if __name__ == "__main__": + #allOpcodesTest() + const("CONST1", 1) + const("CONST2", 2) + ASM(""" + ld a, (123) + ld hl, $1234 + 456 + ld hl, $1234 + CONST1 + ld hl, label + ld hl, label.end - label + ld c, label.end - label +label: + nop +.end: + """, 0) + ASM(""" + jr label +label: + """) + assert ASM("db 1 + 2 * 3") == b'07' diff --git a/worlds/ladx/LADXR/backgroundEditor.py b/worlds/ladx/LADXR/backgroundEditor.py new file mode 100644 index 000000000000..cab58850fa56 --- /dev/null +++ b/worlds/ladx/LADXR/backgroundEditor.py @@ -0,0 +1,69 @@ + +class BackgroundEditor: + def __init__(self, rom, index, *, attributes=False): + self.__index = index + self.__is_attributes = attributes + + self.tiles = {} + if attributes: + data = rom.background_attributes[index] + else: + data = rom.background_tiles[index] + idx = 0 + while data[idx] != 0x00: + addr = data[idx] << 8 | data[idx + 1] + amount = (data[idx + 2] & 0x3F) + 1 + repeat = (data[idx + 2] & 0x40) == 0x40 + vertical = (data[idx + 2] & 0x80) == 0x80 + idx += 3 + for n in range(amount): + self.tiles[addr] = data[idx] + if not repeat: + idx += 1 + addr += 0x20 if vertical else 0x01 + if repeat: + idx += 1 + + def dump(self): + if not self.tiles: + return + low = min(self.tiles.keys()) & 0xFFE0 + high = (max(self.tiles.keys()) | 0x001F) + 1 + print("0x%02x " % (self.__index) + "".join(map(lambda n: "%2X" % (n), range(0x20)))) + for addr in range(low, high, 0x20): + print("%04x " % (addr) + "".join(map(lambda n: ("%02X" % (self.tiles[addr + n])) if addr + n in self.tiles else " ", range(0x20)))) + + def store(self, rom): + # NOTE: This is not a very good encoder, but the background back has so much free space that we really don't care. + # Improvements can be done to find long sequences of bytes and store those as repeated. + result = bytearray() + low = min(self.tiles.keys()) + high = max(self.tiles.keys()) + 1 + while low < high: + if low not in self.tiles: + low += 1 + continue + different_count = 1 + while low + different_count in self.tiles and different_count < 0x40: + different_count += 1 + same_count = 1 + while low + same_count in self.tiles and self.tiles[low] == self.tiles[low + same_count] and same_count < 0x40: + same_count += 1 + if same_count > different_count - 4 and same_count > 2: + result.append(low >> 8) + result.append(low & 0xFF) + result.append((same_count - 1) | 0x40) + result.append(self.tiles[low]) + low += same_count + else: + result.append(low >> 8) + result.append(low & 0xFF) + result.append(different_count - 1) + for n in range(different_count): + result.append(self.tiles[low + n]) + low += different_count + result.append(0x00) + if self.__is_attributes: + rom.background_attributes[self.__index] = result + else: + rom.background_tiles[self.__index] = result diff --git a/worlds/ladx/LADXR/checkMetadata.py b/worlds/ladx/LADXR/checkMetadata.py new file mode 100644 index 000000000000..e8b91c05e80b --- /dev/null +++ b/worlds/ladx/LADXR/checkMetadata.py @@ -0,0 +1,270 @@ +class CheckMetadata: + __slots__ = "name", "area" + def __init__(self, name, area): + self.name = name + self.area = area + + def __repr__(self): + result = "%s - %s" % (self.area, self.name) + return result + + +checkMetadataTable = { + "None": CheckMetadata("Unset Room", "None"), + "0x1F5": CheckMetadata("Boomerang Guy Item", "Toronbo Shores"), #http://artemis251.fobby.net/zelda/maps/underworld1/01F5.GIF + "0x2A3": CheckMetadata("Tarin's Gift", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02A3.GIF + "0x301-0": CheckMetadata("Tunic Fairy Item 1", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0301.GIF + "0x301-1": CheckMetadata("Tunic Fairy Item 2", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0301.GIF + "0x2A2": CheckMetadata("Witch Item", "Koholint Prairie"), #http://artemis251.fobby.net/zelda/maps/underworld2/02A2.GIF + "0x2A1": CheckMetadata("Shop 200 Item", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02A1.GIF + "0x2A7": CheckMetadata("Shop 980 Item", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02A1.GIF + "0x2A1-2": CheckMetadata("Shop 10 Item", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02A1.GIF + "0x113": CheckMetadata("Pit Button Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0113.GIF + "0x115": CheckMetadata("Four Zol Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0115.GIF + "0x10E": CheckMetadata("Spark, Mini-Moldorm Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/010E.GIF + "0x116": CheckMetadata("Hardhat Beetles Key", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0116.GIF + "0x10D": CheckMetadata("Mini-Moldorm Spawn Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/010D.GIF + "0x114": CheckMetadata("Two Stalfos, Two Keese Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0114.GIF + "0x10C": CheckMetadata("Bombable Wall Seashell Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/010C.GIF + "0x103-Owl": CheckMetadata("Spiked Beetle Owl", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0103.GIF + "0x104-Owl": CheckMetadata("Movable Block Owl", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0104.GIF + "0x11D": CheckMetadata("Feather Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/011D.GIF + "0x108": CheckMetadata("Nightmare Key Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0108.GIF + "0x10A": CheckMetadata("Three of a Kind Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/010A.GIF + "0x10A-Owl": CheckMetadata("Three of a Kind Owl", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/010A.GIF + "0x106": CheckMetadata("Moldorm Heart Container", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0106.GIF + "0x102": CheckMetadata("Full Moon Cello", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0102.GIF + "0x136": CheckMetadata("Entrance Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0136.GIF + "0x12E": CheckMetadata("Hardhat Beetle Pit Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/012E.GIF + "0x132": CheckMetadata("Two Stalfos Key", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0132.GIF + "0x137": CheckMetadata("Mask-Mimic Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0137.GIF + "0x133-Owl": CheckMetadata("Switch Owl", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0133.GIF + "0x138": CheckMetadata("First Switch Locked Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0138.GIF + "0x139": CheckMetadata("Button Spawn Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0139.GIF + "0x134": CheckMetadata("Mask-Mimic Key", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0134.GIF + "0x126": CheckMetadata("Vacuum Mouth Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0126.GIF + "0x121": CheckMetadata("Outside Boo Buddies Room Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0121.GIF + "0x129-Owl": CheckMetadata("After Hinox Owl", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0129.GIF + "0x12F-Owl": CheckMetadata("Before First Staircase Owl", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/012F.GIF + "0x120": CheckMetadata("Boo Buddies Room Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0120.GIF + "0x122": CheckMetadata("Second Switch Locked Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0122.GIF + "0x127": CheckMetadata("Enemy Order Room Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0127.GIF + "0x12B": CheckMetadata("Genie Heart Container", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/012B.GIF + "0x12A": CheckMetadata("Conch Horn", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/012A.GIF + "0x153": CheckMetadata("Vacuum Mouth Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0153.GIF + "0x151": CheckMetadata("Two Bombite, Sword Stalfos, Zol Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0151.GIF + "0x14F": CheckMetadata("Four Zol Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/014F.GIF + "0x14E": CheckMetadata("Two Stalfos, Zol Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/014E.GIF + "0x154": CheckMetadata("North Key Room Key", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0154.GIF + "0x154-Owl": CheckMetadata("North Key Room Owl", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0154.GIF + "0x150": CheckMetadata("Sword Stalfos, Keese Switch Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0150.GIF + "0x14C": CheckMetadata("Zol Switch Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/014C.GIF + "0x155": CheckMetadata("West Key Room Key", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0155.GIF + "0x158": CheckMetadata("South Key Room Key", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0158.GIF + "0x14D": CheckMetadata("After Stairs Key", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/014D.GIF + "0x147-Owl": CheckMetadata("Tile Arrow Owl", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0147.GIF + "0x147": CheckMetadata("Tile Arrow Ledge Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0147.GIF + "0x146": CheckMetadata("Boots Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0146.GIF + "0x142": CheckMetadata("Three Zol, Stalfos Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0142.GIF + "0x141": CheckMetadata("Three Bombite Key", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0141.GIF + "0x148": CheckMetadata("Two Zol, Two Pairodd Key", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0148.GIF + "0x144": CheckMetadata("Two Zol, Stalfos Ledge Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0144.GIF + "0x140-Owl": CheckMetadata("Flying Bomb Owl", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0140.GIF + "0x15B": CheckMetadata("Nightmare Door Key", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/015B.GIF + "0x15A": CheckMetadata("Slime Eye Heart Container", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/015A.GIF + "0x159": CheckMetadata("Sea Lily's Bell", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0159.GIF + "0x179": CheckMetadata("Watery Statue Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0179.GIF + "0x16A": CheckMetadata("NW of Boots Pit Ledge Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/016A.GIF + "0x178": CheckMetadata("Two Spiked Beetle, Zol Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0178.GIF + "0x17B": CheckMetadata("Crystal Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/017B.GIF + "0x171": CheckMetadata("Lower Bomb Locked Watery Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0171.GIF + "0x165": CheckMetadata("Upper Bomb Locked Watery Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0165.GIF + "0x175": CheckMetadata("Flipper Locked Before Boots Pit Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0175.GIF + "0x16F-Owl": CheckMetadata("Spiked Beetle Owl", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/016F.GIF + "0x169": CheckMetadata("Pit Key", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0169.GIF + "0x16E": CheckMetadata("Flipper Locked After Boots Pit Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/016E.GIF + "0x16D": CheckMetadata("Blob Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/016D.GIF + "0x168": CheckMetadata("Spark Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0168.GIF + "0x160": CheckMetadata("Flippers Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0160.GIF + "0x176": CheckMetadata("Nightmare Key Ledge Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0176.GIF + "0x166": CheckMetadata("Angler Fish Heart Container", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/01FF.GIF + "0x162": CheckMetadata("Surf Harp", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0162.GIF + "0x1A0": CheckMetadata("Entrance Hookshottable Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/01A0.GIF + "0x19E": CheckMetadata("Spark, Two Iron Mask Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/019E.GIF + "0x181": CheckMetadata("Crystal Key", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0181.GIF + "0x19A-Owl": CheckMetadata("Crystal Owl", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/019A.GIF + "0x19B": CheckMetadata("Flying Bomb Chest South", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/019B.GIF + "0x197": CheckMetadata("Three Iron Mask Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0197.GIF + "0x196": CheckMetadata("Hookshot Note Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0196.GIF + "0x18A-Owl": CheckMetadata("Star Owl", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/018A.GIF + "0x18E": CheckMetadata("Two Stalfos, Star Pit Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/018E.GIF + "0x188": CheckMetadata("Swort Stalfos, Star, Bridge Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0188.GIF + "0x18F": CheckMetadata("Flying Bomb Chest East", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/018F.GIF + "0x180": CheckMetadata("Master Stalfos Item", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0180.GIF + "0x183": CheckMetadata("Three Stalfos Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0183.GIF + "0x186": CheckMetadata("Nightmare Key/Torch Cross Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0186.GIF + "0x185": CheckMetadata("Slime Eel Heart Container", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0185.GIF + "0x182": CheckMetadata("Wind Marimba", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0182.GIF + "0x1CF": CheckMetadata("Mini-Moldorm, Spark Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01CF.GIF + "0x1C9": CheckMetadata("Flying Heart, Statue Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01C9.GIF + "0x1BB-Owl": CheckMetadata("Corridor Owl", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01BB.GIF + "0x1CE": CheckMetadata("L2 Bracelet Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01CE.GIF + "0x1C0": CheckMetadata("Three Wizzrobe, Switch Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01C0.GIF + "0x1B9": CheckMetadata("Stairs Across Statues Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B9.GIF + "0x1B3": CheckMetadata("Switch, Star Above Statues Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B3.GIF + "0x1B4": CheckMetadata("Two Wizzrobe Key", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B4.GIF + "0x1B0": CheckMetadata("Top Left Horse Heads Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B0.GIF + "0x06C": CheckMetadata("Raft Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/overworld/006C.GIF + "0x1BE": CheckMetadata("Water Tektite Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01BE.GIF + "0x1D1": CheckMetadata("Four Wizzrobe Ledge Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01D1.GIF + "0x1D7-Owl": CheckMetadata("Blade Trap Owl", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01D7.GIF + "0x1C3": CheckMetadata("Tile Room Key", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01C3.GIF + "0x1B1": CheckMetadata("Top Right Horse Heads Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B1.GIF + "0x1B6-Owl": CheckMetadata("Pot Owl", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B6.GIF + "0x1B6": CheckMetadata("Pot Locked Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B6.GIF + "0x1BC": CheckMetadata("Facade Heart Container", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01BC.GIF + "0x1B5": CheckMetadata("Coral Triangle", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B5.GIF + "0x210": CheckMetadata("Entrance Key", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0210.GIF + "0x216-Owl": CheckMetadata("Ball Owl", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0216.GIF + "0x212": CheckMetadata("Horse Head, Bubble Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0212.GIF + "0x204-Owl": CheckMetadata("Beamos Owl", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0204.GIF + "0x204": CheckMetadata("Beamos Ledge Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0204.GIF + "0x209": CheckMetadata("Switch Wrapped Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0209.GIF + "0x211": CheckMetadata("Three of a Kind, No Pit Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0211.GIF + "0x21B": CheckMetadata("Hinox Key", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/021B.GIF + "0x201": CheckMetadata("Kirby Ledge Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0201.GIF + "0x21C-Owl": CheckMetadata("Three of a Kind, Pit Owl", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/021C.GIF + "0x21C": CheckMetadata("Three of a Kind, Pit Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/021C.GIF + "0x224": CheckMetadata("Nightmare Key/After Grim Creeper Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0224.GIF + "0x21A": CheckMetadata("Mirror Shield Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/021A.GIF + "0x220": CheckMetadata("Conveyor Beamos Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0220.GIF + "0x223": CheckMetadata("Evil Eagle Heart Container", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/02E8.GIF + "0x22C": CheckMetadata("Organ of Evening Calm", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/022C.GIF + "0x24F": CheckMetadata("Push Block Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/024F.GIF + "0x24D": CheckMetadata("Left of Hinox Zamboni Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/024D.GIF + "0x25C": CheckMetadata("Vacuum Mouth Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/025C.GIF + "0x24C": CheckMetadata("Left Vire Key", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/024C.GIF + "0x255": CheckMetadata("Spark, Pit Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0255.GIF + "0x246": CheckMetadata("Two Torches Room Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0246.GIF + "0x253-Owl": CheckMetadata("Beamos Owl", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0253.GIF + "0x259": CheckMetadata("Right Lava Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0259.GIF + "0x25A": CheckMetadata("Zamboni, Two Zol Key", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/025A.GIF + "0x25F": CheckMetadata("Four Ropes Pot Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/025F.GIF + "0x245-Owl": CheckMetadata("Bombable Blocks Owl", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0245.GIF + "0x23E": CheckMetadata("Gibdos on Cracked Floor Key", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/023E.GIF + "0x235": CheckMetadata("Lava Ledge Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0235.GIF + "0x237": CheckMetadata("Magic Rod Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0237.GIF + "0x240": CheckMetadata("Beamos Blocked Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0240.GIF + "0x23D": CheckMetadata("Dodongo Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/023D.GIF + "0x000": CheckMetadata("Outside Heart Piece", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/overworld/0000.GIF + "0x241": CheckMetadata("Lava Arrow Statue Key", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0241.GIF + "0x241-Owl": CheckMetadata("Lava Arrow Statue Owl", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0241.GIF + "0x23A": CheckMetadata("West of Boss Door Ledge Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/023A.GIF + "0x232": CheckMetadata("Nightmare Key/Big Zamboni Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0232.GIF + "0x234": CheckMetadata("Hot Head Heart Container", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0234.GIF + "0x230": CheckMetadata("Thunder Drum", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0230.GIF + "0x314": CheckMetadata("Lower Small Key", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0314.GIF + "0x308-Owl": CheckMetadata("Upper Key Owl", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0308.GIF + "0x308": CheckMetadata("Upper Small Key", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0308.GIF + "0x30F-Owl": CheckMetadata("Entrance Owl", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/030F.GIF + "0x30F": CheckMetadata("Entrance Chest", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/030F.GIF + "0x311": CheckMetadata("Two Socket Chest", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0311.GIF + "0x302": CheckMetadata("Nightmare Key Chest", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0302.GIF + "0x306": CheckMetadata("Zol Chest", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0306.GIF + "0x307": CheckMetadata("Bullshit Room", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0307.GIF + "0x30A-Owl": CheckMetadata("Puzzowl", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/030A.GIF + "0x2BF": CheckMetadata("Dream Hut East", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02BF.GIF + "0x2BE": CheckMetadata("Dream Hut West", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02BE.GIF + "0x2A4": CheckMetadata("Well Heart Piece", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02A4.GIF + "0x2B1": CheckMetadata("Fishing Game Heart Piece", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02B1.GIF + "0x0A3": CheckMetadata("Bush Field", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/overworld/00A3.GIF + "0x2B2": CheckMetadata("Dog House Dig", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02B2.GIF + "0x0D2": CheckMetadata("Outside D1 Tree Bonk", "Toronbo Shores"), #http://artemis251.fobby.net/zelda/maps/overworld/00D2.GIF + "0x0E5": CheckMetadata("West of Ghost House Chest", "Toronbo Shores"), #http://artemis251.fobby.net/zelda/maps/overworld/00E5.GIF + "0x1E3": CheckMetadata("Ghost House Barrel", "Martha's Bay"), #http://artemis251.fobby.net/zelda/maps/underworld1/01E3.GIF + "0x044": CheckMetadata("Heart Piece of Shame", "Koholint Prairie"), #http://artemis251.fobby.net/zelda/maps/overworld/0044.GIF + "0x071": CheckMetadata("Two Zol, Moblin Chest", "Mysterious Woods"), #http://artemis251.fobby.net/zelda/maps/overworld/0071.GIF + "0x1E1": CheckMetadata("Mad Batter", "Mysterious Woods"), #http://artemis251.fobby.net/zelda/maps/underworld1/01E1.GIF + "0x034": CheckMetadata("Swampy Chest", "Goponga Swamp"), #http://artemis251.fobby.net/zelda/maps/overworld/0034.GIF + "0x041": CheckMetadata("Tail Key Chest", "Mysterious Woods"), #http://artemis251.fobby.net/zelda/maps/overworld/0041.GIF + "0x2BD": CheckMetadata("Cave Crystal Chest", "Mysterious Woods"), #http://artemis251.fobby.net/zelda/maps/underworld2/02BD.GIF + "0x2AB": CheckMetadata("Cave Skull Heart Piece", "Mysterious Woods"), #http://artemis251.fobby.net/zelda/maps/underworld2/02AB.GIF + "0x2B3": CheckMetadata("Hookshot Cave", "Mysterious Woods"), #http://artemis251.fobby.net/zelda/maps/underworld2/02B3.GIF + "0x2AE": CheckMetadata("Write Cave West", "Goponga Swamp"), #http://artemis251.fobby.net/zelda/maps/underworld2/02AE.GIF + "0x011-Owl": CheckMetadata("North of Write Owl", "Goponga Swamp"), #http://artemis251.fobby.net/zelda/maps/overworld/0011.GIF #might come out as "0x11 + "0x2AF": CheckMetadata("Write Cave East", "Goponga Swamp"), #http://artemis251.fobby.net/zelda/maps/underworld2/02AF.GIF + "0x035-Owl": CheckMetadata("Moblin Cave Owl", "Tal Tal Heights"), #http://artemis251.fobby.net/zelda/maps/overworld/0035.GIF + "0x2DF": CheckMetadata("Graveyard Connector", "Koholint Prairie"), #http://artemis251.fobby.net/zelda/maps/underworld2/02DF.GIF + "0x074": CheckMetadata("Ghost Grave Dig", "Koholint Prairie"), #http://artemis251.fobby.net/zelda/maps/overworld/0074.GIF + "0x2E2": CheckMetadata("Moblin Cave", "Tal Tal Heights"), #http://artemis251.fobby.net/zelda/maps/underworld2/02E2.GIF + "0x2CD": CheckMetadata("Cave East of Mabe", "Ukuku Prairie"), #http://artemis251.fobby.net/zelda/maps/underworld2/02CD.GIF + "0x2F4": CheckMetadata("Boots 'n' Bomb Cave Chest", "Ukuku Prairie"), #http://artemis251.fobby.net/zelda/maps/underworld2/02F4.GIF + "0x2E5": CheckMetadata("Boots 'n' Bomb Cave Bombable Wall", "Ukuku Prairie"), #http://artemis251.fobby.net/zelda/maps/underworld2/02E5.GIF + "0x0A5": CheckMetadata("Outside D3 Ledge Dig", "Ukuku Prairie"), #http://artemis251.fobby.net/zelda/maps/overworld/00A5.GIF + "0x0A6": CheckMetadata("Outside D3 Island Bush", "Ukuku Prairie"), #http://artemis251.fobby.net/zelda/maps/overworld/00A6.GIF + "0x08B": CheckMetadata("East of Seashell Mansion Bush", "Ukuku Prairie"), #http://artemis251.fobby.net/zelda/maps/overworld/008B.GIF + "0x0A4": CheckMetadata("East of Mabe Tree Bonk", "Ukuku Prairie"), #http://artemis251.fobby.net/zelda/maps/overworld/00A4.GIF + "0x2E9": CheckMetadata("Seashell Mansion", "Ukuku Prairie"), + "0x1FD": CheckMetadata("Boots Pit", "Kanalet Castle"), #http://artemis251.fobby.net/zelda/maps/underworld1/01FD.GIF + "0x0B9": CheckMetadata("Rock Seashell", "Donut Plains"), #http://artemis251.fobby.net/zelda/maps/overworld/00B9.GIF + "0x0E9": CheckMetadata("Lone Bush", "Martha's Bay"), #http://artemis251.fobby.net/zelda/maps/overworld/00E9.GIF + "0x0F8": CheckMetadata("Island Bush of Destiny", "Martha's Bay"), #http://artemis251.fobby.net/zelda/maps/overworld/00F8.GIF + "0x0A8": CheckMetadata("Donut Plains Ledge Dig", "Donut Plains"), #http://artemis251.fobby.net/zelda/maps/overworld/00A8.GIF + "0x0A8-Owl": CheckMetadata("Donut Plains Ledge Owl", "Donut Plains"), #http://artemis251.fobby.net/zelda/maps/overworld/00A8.GIF + "0x1E0": CheckMetadata("Mad Batter", "Martha's Bay"), #http://artemis251.fobby.net/zelda/maps/underworld1/01E0.GIF + "0x0C6-Owl": CheckMetadata("Slime Key Owl", "Pothole Field"), #http://artemis251.fobby.net/zelda/maps/overworld/00C6.GIF + "0x0C6": CheckMetadata("Slime Key Dig", "Pothole Field"), #http://artemis251.fobby.net/zelda/maps/overworld/00C6.GIF + "0x2C8": CheckMetadata("Under Richard's House", "Pothole Field"), #http://artemis251.fobby.net/zelda/maps/underworld2/02C8.GIF + "0x078": CheckMetadata("In the Moat Heart Piece", "Kanalet Castle"), #http://artemis251.fobby.net/zelda/maps/overworld/0078.GIF + "0x05A": CheckMetadata("Bomberman Meets Whack-a-mole Leaf", "Kanalet Castle"), #http://artemis251.fobby.net/zelda/maps/overworld/005A.GIF + "0x058": CheckMetadata("Crow Rock Leaf", "Kanalet Castle"), #http://artemis251.fobby.net/zelda/maps/overworld/0058.GIF + "0x2D2": CheckMetadata("Darknut, Zol, Bubble Leaf", "Kanalet Castle"), #http://artemis251.fobby.net/zelda/maps/underworld2/02D2.GIF + "0x2C5": CheckMetadata("Bombable Darknut Leaf", "Kanalet Castle"), #http://artemis251.fobby.net/zelda/maps/underworld2/02C5.GIF + "0x2C6": CheckMetadata("Ball and Chain Darknut Leaf", "Kanalet Castle"), #http://artemis251.fobby.net/zelda/maps/underworld2/02C6.GIF + "0x0DA": CheckMetadata("Peninsula Dig", "Martha's Bay"), #http://artemis251.fobby.net/zelda/maps/overworld/00DA.GIF + "0x0DA-Owl": CheckMetadata("Peninsula Owl", "Martha's Bay"), #http://artemis251.fobby.net/zelda/maps/overworld/00DA.GIF + "0x0CF-Owl": CheckMetadata("Desert Owl", "Yarna Desert"), #http://artemis251.fobby.net/zelda/maps/overworld/00CF.GIF + "0x2E6": CheckMetadata("Bomb Arrow Cave", "Yarna Desert"), #http://artemis251.fobby.net/zelda/maps/underworld2/02E6.GIF + "0x1E8": CheckMetadata("Cave Under Lanmola", "Yarna Desert"), #http://artemis251.fobby.net/zelda/maps/underworld1/01E8.GIF + "0x0FF": CheckMetadata("Rock Seashell", "Yarna Desert"), #http://artemis251.fobby.net/zelda/maps/overworld/00FF.GIF + "0x018": CheckMetadata("Access Tunnel Exterior", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/overworld/0018.GIF + "0x2BB": CheckMetadata("Access Tunnel Interior", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/underworld2/02BB.GIF + "0x28A": CheckMetadata("Paphl Cave", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/underworld2/028A.GIF + "0x1F2": CheckMetadata("Damp Cave Heart Piece", "Tal Tal Heights"), #http://artemis251.fobby.net/zelda/maps/underworld1/01F2.GIF + "0x2FC": CheckMetadata("Under Armos Cave", "Southern Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld2/02FC.GIF + "0x08F-Owl": CheckMetadata("Outside Owl", "Southern Face Shrine"), #http://artemis251.fobby.net/zelda/maps/overworld/008F.GIF + "0x05C": CheckMetadata("West", "Rapids Ride"), #http://artemis251.fobby.net/zelda/maps/overworld/005C.GIF + "0x05D": CheckMetadata("East", "Rapids Ride"), #http://artemis251.fobby.net/zelda/maps/overworld/005D.GIF + "0x05D-Owl": CheckMetadata("Owl", "Rapids Ride"), #http://artemis251.fobby.net/zelda/maps/overworld/005D.GIF + "0x01E-Owl": CheckMetadata("Outside D7 Owl", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/overworld/001E.GIF + "0x00C": CheckMetadata("Bridge Rock", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/overworld/000C.GIF + "0x2F2": CheckMetadata("Five Chest Game", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/underworld2/02F2.GIF + "0x01D": CheckMetadata("Outside Five Chest Game", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/overworld/001D.GIF + "0x004": CheckMetadata("Outside Mad Batter", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/overworld/0004.GIF + "0x1E2": CheckMetadata("Mad Batter", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/underworld1/01E2.GIF + "0x2BA": CheckMetadata("Access Tunnel Bombable Heart Piece", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/underworld2/02BA.GIF + "0x0F2": CheckMetadata("Sword on the Beach", "Toronbo Shores"), #http://artemis251.fobby.net/zelda/maps/overworld/00F2.GIF + "0x050": CheckMetadata("Toadstool", "Mysterious Woods"), #http://artemis251.fobby.net/zelda/maps/overworld/0050.GIF + "0x0CE": CheckMetadata("Lanmola", "Yarna Desert"), #http://artemis251.fobby.net/zelda/maps/overworld/00CE.GIF + "0x27F": CheckMetadata("Armos Knight", "Southern Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld2/027F.GIF + "0x27A": CheckMetadata("Bird Key Cave", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/underworld2/027A.GIF + "0x092": CheckMetadata("Ballad of the Wind Fish", "Mabe Village"), + "0x2FD": CheckMetadata("Manbo's Mambo", "Tal Tal Heights"), + "0x2FB": CheckMetadata("Mamu", "Ukuku Prairie"), + "0x1E4": CheckMetadata("Rooster", "Mabe Village"), + + "0x2A0-Trade": CheckMetadata("Trendy Game", "Mabe Village"), + "0x2A6-Trade": CheckMetadata("Papahl's Wife", "Mabe Village"), + "0x2B2-Trade": CheckMetadata("YipYip", "Mabe Village"), + "0x2FE-Trade": CheckMetadata("Banana Sale", "Toronbo Shores"), + "0x07B-Trade": CheckMetadata("Kiki", "Ukuku Prairie"), + "0x087-Trade": CheckMetadata("Honeycomb", "Ukuku Prairie"), + "0x2D7-Trade": CheckMetadata("Bear Cook", "Animal Village"), + "0x019-Trade": CheckMetadata("Papahl", "Tal Tal Heights"), + "0x2D9-Trade": CheckMetadata("Goat", "Animal Village"), + "0x2A8-Trade": CheckMetadata("MrWrite", "Goponga Swamp"), + "0x0CD-Trade": CheckMetadata("Grandma", "Animal Village"), + "0x2F5-Trade": CheckMetadata("Fisher", "Martha's Bay"), + "0x0C9-Trade": CheckMetadata("Mermaid", "Martha's Bay"), + "0x297-Trade": CheckMetadata("Mermaid Statue", "Martha's Bay"), +} diff --git a/worlds/ladx/LADXR/entityData.py b/worlds/ladx/LADXR/entityData.py new file mode 100644 index 000000000000..405a1ea65c84 --- /dev/null +++ b/worlds/ladx/LADXR/entityData.py @@ -0,0 +1,561 @@ +COUNT = 0xFB +NAME = [ + "ARROW", + "BOOMERANG", + "BOMB", + "HOOKSHOT_CHAIN", + "HOOKSHOT_HIT", + "LIFTABLE_ROCK", + "PUSHED_BLOCK", + "CHEST_WITH_ITEM", + "MAGIC_POWDER_SPRINKLE", + "OCTOROCK", + "OCTOROCK_ROCK", + "MOBLIN", + "MOBLIN_ARROW", + "TEKTITE", + "LEEVER", + "ARMOS_STATUE", + "HIDING_GHINI", + "GIANT_GHINI", + "GHINI", + "BROKEN_HEART_CONTAINER", + "MOBLIN_SWORD", + "ANTI_FAIRY", + "SPARK_COUNTER_CLOCKWISE", + "SPARK_CLOCKWISE", + "POLS_VOICE", + "KEESE", + "STALFOS_AGGRESSIVE", + "GEL", + "MINI_GEL", + "DISABLED", + "STALFOS_EVASIVE", + "GIBDO", + "HARDHAT_BEETLE", + "WIZROBE", + "WIZROBE_PROJECTILE", + "LIKE_LIKE", + "IRON_MASK", + "SMALL_EXPLOSION_ENEMY", + "SMALL_EXPLOSION_ENEMY_2", + "SPIKE_TRAP", + "MIMIC", + "MINI_MOLDORM", + "LASER", + "LASER_BEAM", + "SPIKED_BEETLE", + "DROPPABLE_HEART", + "DROPPABLE_RUPEE", + "DROPPABLE_FAIRY", + "KEY_DROP_POINT", + "SWORD", + "32", + "PIECE_OF_POWER", + "GUARDIAN_ACORN", + "HEART_PIECE", + "HEART_CONTAINER", + "DROPPABLE_ARROWS", + "DROPPABLE_BOMBS", + "INSTRUMENT_OF_THE_SIRENS", + "SLEEPY_TOADSTOOL", + "DROPPABLE_MAGIC_POWDER", + "HIDING_SLIME_KEY", + "DROPPABLE_SECRET_SEASHELL", + "MARIN", + "RACOON", + "WITCH", + "OWL_EVENT", + "OWL_STATUE", + "SEASHELL_MANSION_TREES", + "YARNA_TALKING_BONES", + "BOULDERS", + "MOVING_BLOCK_LEFT_TOP", + "MOVING_BLOCK_LEFT_BOTTOM", + "MOVING_BLOCK_BOTTOM_LEFT", + "MOVING_BLOCK_BOTTOM_RIGHT", + "COLOR_DUNGEON_BOOK", + "POT", + "DISABLED", + "SHOP_OWNER", + "4D", + "TRENDY_GAME_OWNER", + "BOO_BUDDY", + "KNIGHT", + "TRACTOR_DEVICE", + "TRACTOR_DEVICE_REVERSE", + "FISHERMAN_FISHING_GAME", + "BOUNCING_BOMBITE", + "TIMER_BOMBITE", + "PAIRODD", + "PAIRODD_PROJECTILE", + "MOLDORM", + "FACADE", + "SLIME_EYE", + "GENIE", + "SLIME_EEL", + "GHOMA", + "MASTER_STALFOS", + "DODONGO_SNAKE", + "WARP", + "HOT_HEAD", + "EVIL_EAGLE", + "SOUTH_FACE_SHRINE_DOOR", + "ANGLER_FISH", + "CRYSTAL_SWITCH", + "67", + "68", + "MOVING_BLOCK_MOVER", + "RAFT_RAFT_OWNER", + "TEXT_DEBUGGER", + "CUCCO", + "BOW_WOW", + "BUTTERFLY", + "DOG", + "KID_70", + "KID_71", + "KID_72", + "KID_73", + "PAPAHLS_WIFE", + "GRANDMA_ULRIRA", + "MR_WRITE", + "GRANDPA_ULRIRA", + "YIP_YIP", + "MADAM_MEOWMEOW", + "CROW", + "CRAZY_TRACY", + "GIANT_GOPONGA_FLOWER", + "GOPONGA_FLOWER_PROJECTILE", + "GOPONGA_FLOWER", + "TURTLE_ROCK_HEAD", + "TELEPHONE", + "ROLLING_BONES", + "ROLLING_BONES_BAR", + "DREAM_SHRINE_BED", + "BIG_FAIRY", + "MR_WRITES_BIRD", + "FLOATING_ITEM", + "DESERT_LANMOLA", + "ARMOS_KNIGHT", + "HINOX", + "TILE_GLINT_SHOWN", + "TILE_GLINT_HIDDEN", + "8C", + "8D", + "CUE_BALL", + "MASKED_MIMIC_GORIYA", + "THREE_OF_A_KIND", + "ANTI_KIRBY", + "SMASHER", + "MAD_BOMBER", + "KANALET_BOMBABLE_WALL", + "RICHARD", + "RICHARD_FROG", + "DIVE_SPOT", + "HORSE_PIECE", + "WATER_TEKTITE", + "FLYING_TILES", + "HIDING_GEL", + "STAR", + "LIFTABLE_STATUE", + "FIREBALL_SHOOTER", + "GOOMBA", + "PEAHAT", + "SNAKE", + "PIRANHA_PLANT", + "SIDE_VIEW_PLATFORM_HORIZONTAL", + "SIDE_VIEW_PLATFORM_VERTICAL", + "SIDE_VIEW_PLATFORM", + "SIDE_VIEW_WEIGHTS", + "SMASHABLE_PILLAR", + "WRECKING_BALL", + "BLOOPER", + "CHEEP_CHEEP_HORIZONTAL", + "CHEEP_CHEEP_VERTICAL", + "CHEEP_CHEEP_JUMPING", + "KIKI_THE_MONKEY", + "WINGED_OCTOROK", + "TRADING_ITEM", + "PINCER", + "HOLE_FILLER", + "BEETLE_SPAWNER", + "HONEYCOMB", + "TARIN", + "BEAR", + "PAPAHL", + "MERMAID", + "FISHERMAN_UNDER_BRIDGE", + "BUZZ_BLOB", + "BOMBER", + "BUSH_CRAWLER", + "GRIM_CREEPER", + "VIRE", + "BLAINO", + "ZOMBIE", + "MAZE_SIGNPOST", + "MARIN_AT_THE_SHORE", + "MARIN_AT_TAL_TAL_HEIGHTS", + "MAMU_AND_FROGS", + "WALRUS", + "URCHIN", + "SAND_CRAB", + "MANBO_AND_FISHES", + "BUNNY_CALLING_MARIN", + "MUSICAL_NOTE", + "MAD_BATTER", + "ZORA", + "FISH", + "BANANAS_SCHULE_SALE", + "MERMAID_STATUE", + "SEASHELL_MANSION", + "ANIMAL_D0", + "ANIMAL_D1", + "ANIMAL_D2", + "BUNNY_D3", + "GHOST", + "ROOSTER", + "SIDE_VIEW_POT", + "THWIMP", + "THWOMP", + "THWOMP_RAMMABLE", + "PODOBOO", + "GIANT_BUBBLE", + "FLYING_ROOSTER_EVENTS", + "BOOK", + "EGG_SONG_EVENT", + "SWORD_BEAM", + "MONKEY", + "WITCH_RAT", + "FLAME_SHOOTER", + "POKEY", + "MOBLIN_KING", + "FLOATING_ITEM_2", + "FINAL_NIGHTMARE", + "KANALET_CASTLE_GATE_SWITCH", + "ENDING_OWL_STAIR_CLIMBING", + "COLOR_SHELL_RED", + "COLOR_SHELL_GREEN", + "COLOR_SHELL_BLUE", + "COLOR_GHOUL_RED", + "COLOR_GHOUL_GREEN", + "COLOR_GHOUL_BLUE", + "ROTOSWITCH_RED", + "ROTOSWITCH_YELLOW", + "ROTOSWITCH_BLUE", + "FLYING_HOPPER_BOMBS", + "HOPPER", + "AVALAUNCH", + "BOUNCING_BOULDER", + "COLOR_GUARDIAN_BLUE", + "COLOR_GUARDIAN_RED", + "GIANT_BUZZ_BLOB", + "HARDHIT_BEETLE", + "PHOTOGRAPHER", +] + +def _moblinSpriteData(room): + if room.room in (0x002, 0x013): # Tal tal heights exception + return (2, 0x9C) # Hooded stalfos + if room.room < 0x100: + x = room.room & 0x0F + y = (room.room >> 4) & 0x0F + if x < 0x04: # Left side is woods and mountain moblins + return (2, 0x7C) # Moblin + if 0x08 <= x <= 0x0B and 4 <= y <= 0x07: # Castle + return (2, 0x92) # Knight + # Everything else is pigs + return (2, 0x83) # Pig + elif room.room < 0x1DF: # Dungeons contain hooded stalfos + return (2, 0x9C) # Hooded stalfos + elif room.room < 0x200: # Caves contain moblins + return (2, 0x7C) # Moblin + elif room.room < 0x276: # Dungeons contain hooded stalfos + return (2, 0x9C) # Hooded stalfos + elif room.room < 0x300: # Caves contain moblins + x = room.room & 0x0F + y = (room.room >> 4) & 0x0F + if 2 <= x <= 6 and 0x0C <= y <= 0x0D: # Castle indoors + return (2, 0x92) # Knight + return (2, 0x7C) # Moblin + else: # Dungeon contains hooded stalfos + return (2, 0x9C) # Hooded stalfos + +_CAVES_B_ROOMS = {0x2B6, 0x2B7, 0x2B8, 0x2B9, 0x285, 0x286, 0x2FD, 0x2F3, 0x2ED, 0x2EE, 0x2EA, 0x2EB, 0x2EC, 0x287, 0x2F1, 0x2F2, 0x2FE, 0x2EF, 0x2BA, 0x2BB, 0x2BC, 0x28D, 0x2F9, 0x2FA, 0x280, 0x281, 0x282, 0x283, 0x284, 0x28C, 0x288, 0x28A, 0x290, 0x291, 0x292, 0x28E, 0x29A, 0x289, 0x28B, 0x297, 0x293, 0x294, 0x295, 0x296, 0x2AB, 0x2AC, 0x298, 0x27A, 0x27B, 0x2E6, 0x2E7, 0x2BD, 0x27C, 0x27D, 0x27E, 0x2F6, 0x2F7, 0x2DE, 0x2DF} + +# For each entity, which sprite slot is used and which value should be used. +SPRITE_DATA = { + 0x09: (2, 0xE3), # OCTOROCK + 0x0B: _moblinSpriteData, # MOBLIN + 0x0D: (1, 0x87), # TEKTITE + 0x0E: (1, 0x81), # LEEVER + 0x0F: (2, 0x78), # ARMOS_STATUE + 0x10: (1, 0x42), # HIDING_GHINI + 0x11: (2, 0x8A), # GIANT_GHINI + 0x12: (1, 0x42), # GHINI + 0x14: _moblinSpriteData, # MOBLIN_SWORD + 0x15: (1, 0x91), # ANTI_FAIRY + 0x16: (1, {0x91, 0x65}), # SPARK_COUNTER_CLOCKWISE + 0x17: (1, {0x91, 0x65}), # SPARK_CLOCKWISE + 0x18: (3, 0x93), # POLS_VOICE + 0x19: lambda room: (2, 0x90) if room.room in _CAVES_B_ROOMS else (0, 0x90), # KEESE + 0x1A: (0, {0x90, 0x77}), # STALFOS_AGGRESSIVE + 0x1B: None, # GEL + 0x1C: (1, 0x91), # MINI_GEL + 0x1E: (0, {0x90, 0x77}), # STALFOS_EVASIVE + 0x1F: lambda room: (0, 0x77) if 0x230 <= room.room <= 0x26B else (0, 0x90, 3, 0x93), # GIBDO + 0x20: lambda room: (2, 0x90) if room.room in _CAVES_B_ROOMS else (0, 0x90), # HARDHAT_BEETLE + 0x21: (2, 0x95), # WIZROBE + 0x23: (3, 0x93), # LIKE_LIKE + 0x24: (2, 0x94, 3, 0x9F), # IRON_MASK + 0x27: (1, 0x91), # SPIKE_TRAP + 0x28: (2, 0x96), # MIMIC + 0x29: (3, 0x98), # MINI_MOLDORM + 0x2A: (3, 0x99), # LASER + 0x2C: lambda room: (2, 0x9B) if 0x15E <= room.room <= 0x17F else (3, 0x9B), # SPIKED_BEETLE + 0x2D: None, # DROPPABLE_HEART + 0x2E: None, # DROPPABLE_RUPEE + 0x2F: None, # DROPPABLE_FAIRY + 0x30: None, # KEY_DROP_POINT + 0x31: None, # SWORD + 0x35: None, # HEART_PIECE + 0x37: None, # DROPPABLE_ARROWS + 0x38: None, # DROPPABLE_BOMBS + 0x39: (2, 0x4F), # INSTRUMENT_OF_THE_SIRENS + 0x3A: (1, 0x8E), # SLEEPY_TOADSTOOL + 0x3B: None, # DROPPABLE_MAGIC_POWDER + 0x3C: None, # HIDING_SLIME_KEY + 0x3D: None, # DROPPABLE_SECRET_SEASHELL + 0x3E: lambda room: (0, 0x8D, 2, 0x8F) if room.room == 0x2A3 else (2, 0xE6), # MARIN + 0x3F: lambda room: (1, 0x8E, 3, 0x6A) if room.room == 0x2A3 else (1, 0x6C, 3, 0xC8), # RACOON + 0x40: (2, 0xA3), # WITCH + 0x41: None, # OWL_EVENT + 0x42: lambda room: (1, 0xD5) if room.room == 0x26F else (1, 0x91), # OWL_STATUE + 0x43: None, # SEASHELL_MANSION_TREES + 0x44: None, # YARNA_TALKING_BONES + 0x45: (1, 0x44), # BOULDERS + 0x46: None, # MOVING_BLOCK_LEFT_TOP + 0x47: None, # MOVING_BLOCK_LEFT_BOTTOM + 0x48: None, # MOVING_BLOCK_BOTTOM_LEFT + 0x49: None, # MOVING_BLOCK_BOTTOM_RIGHT + 0x4A: (1, 0xd5), # COLOR_DUNGEON_BOOK + 0x4C: None, # Used by Bingo board, otherwise unused. + 0x4D: (2, 0x88, 3, 0xC7), # SHOP_OWNER + 0x4F: (2, 0x84, 3, 0x89), # TRENDY_GAME_OWNER + 0x50: (2, 0x97), # BOO_BUDDY + 0x51: (3, 0x9A), # KNIGHT + 0x52: lambda room: (3, {0x7b, 0xa6}) if 0x120 <= room.room <= 0x13F else (0, {0x7b, 0xa6}), # TRACTOR_DEVICE + 0x53: lambda room: (3, {0x7b, 0xa6}) if 0x120 <= room.room <= 0x13F else (0, {0x7b, 0xa6}), # TRACTOR_DEVICE_REVERSE + 0x54: lambda room: (0, 0xA0, 1, 0xA1) if room.room == 0x2B1 else (3, 0x4e), # FISHERMAN_FISHING_GAME + 0x55: (3, 0x9d), # BOUNCING_BOMBITE + 0x56: (3, 0x9d), # TIMER_BOMBITE + 0x57: (3, 0x9e), # PAIRODD + 0x59: (2, 0xb0, 3, 0xb1), # MOLDORM + 0x5A: (0, 0x66, 2, 0xb2, 3, 0xb3), # FACADE + 0x5B: (2, 0xb4, 3, 0xb5), # SLIME_EYE + 0x5C: (2, 0xb6, 3, 0xb7), # GENIE + 0x5D: (2, 0xb8, 3, 0xb9), # SLIME_EEL + 0x5E: (2, 0xa8), # GHOMA + 0x5F: (2, 0x62, 3, 0x63), # MASTER_STALFOS + 0x60: lambda room: (3, 0xaa) if 0x230 <= room.room <= 0x26B else (2, 0xaa), # DODONGO_SNAKE + 0x61: None, # WARP + 0x62: (2, 0xba, 3, 0xbb), # HOT_HEAD + 0x63: (0, 0xbc, 1, 0xbd, 2, 0xbe, 3, 0xbf), # EVIL_EAGLE + 0x65: (0, 0xac, 1, 0xad, 2, 0xae, 3, 0xaf), # ANGLER_FISH + 0x66: (1, 0x91), # CRYSTAL_SWITCH + 0x69: (0, 0x66), # MOVING_BLOCK_MOVER + 0x6A: lambda room: (1, 0x87, 2, 0x84) if room.room >= 0x100 else (1, 0x87), # RAFT_RAFT_OWNER + 0x6C: None, # CUCCU + 0x6D: (3, 0xA4), # BOW_WOW + 0x6E: (1, {0xE5, 0xC4}), # BUTTERFLY + 0x6F: (1, 0xE5), # DOG + 0x70: (3, 0xE7), # KID_70 + 0x71: (3, 0xE7), # KID_71 + 0x72: (3, 0xE7), # KID_72 + 0x73: (3, 0xDC), # KID_73 + 0x74: (2, 0x45), # PAPAHLS_WIFE + 0x75: (2, 0x43), # GRANDMA_ULRIRA + 0x76: lambda room: (3, 0x74) if room.room == 0x2D9 else (3, 0x4b), # MR_WRITE + 0x77: (3, 0x46), # GRANDPA_ULRIRA + 0x78: (3, 0x48), # YIP_YIP + 0x79: (2, 0x47), # MADAM_MEOWMEOW + 0x7A: lambda room: (1, 0xC6) if room.room < 0x040 else (1, 0x42), # CROW + 0x7B: (2, 0x49), # CRAZY_TRACY + 0x7C: (3, 0x40), # GIANT_GOPONGA_FLOWER + 0x7E: (1, 0x4A), # GOPONGA_FLOWER + 0x7F: (3, 0x41), # TURTLE_ROCK_HEAD + 0x80: (1, 0x4C), # TELEPHONE + 0x81: lambda room: (3, 0xAB) if 0x230 <= room.room <= 0x26B else (2, 0xAB), # ROLLING_BONES (sometimes in slot 3?) + 0x82: lambda room: (3, 0xAB) if 0x230 <= room.room <= 0x26B else (2, 0xAB), # ROLLING_BONES_BAR (sometimes in slot 3?) + 0x83: (1, 0x8D), # DREAM_SHRINE_BED + 0x84: (1, 0x4D), # BIG_FAIRY + 0x85: (2, 0x4C), # MR_WRITES_BIRD + 0x86: None, # FLOATING_ITEM + 0x87: (3, 0x52), # DESERT_LANMOLA + 0x88: (3, 0x53), # ARMOS_KNIGHT + 0x89: (2, 0x54), # HINOX + 0x8A: None, # TILE_GLINT_SHOWN + 0x8B: None, # TILE_GLINT_HIDDEN + 0x8E: (2, 0x56), # CUE_BALL + 0x8F: lambda room: (2, 0x86) if room.room == 0x1F5 else (2, 0x58), # MASKED_MIMIC_GORIYA + 0x90: (3, 0x59), # THREE_OF_A_KIND + 0x91: (2, 0x55), # ANTI_KIRBY + 0x92: (2, 0x57), # SMASHER + 0x93: (3, 0x5A), # MAD_BOMBER + 0x94: (2, 0x92), # KANALET_BOMBABLE_WALL + 0x95: (1, 0x5b), # RICHARD + 0x96: (2, 0x5c), # RICHARD_FROG + 0x97: None, # DIVE_SPOT + 0x98: (2, 0x5e), # HORSE_PIECE + 0x99: (3, 0x60), # WATER_TEKTITE + 0x9A: lambda room: (0, 0x66) if 0x200 <= room.room <= 0x22F else (0, 0xa6), # FLYING_TILES + 0x9B: None, # HIDING_GEL + 0x9C: (3, 0x60), # STAR + 0x9D: (0, 0xa6), # LIFTABLE_STATUE + 0x9E: None, # FIREBALL_SHOOTER + 0x9F: (0, 0x5f), # GOOMBA + 0xA0: (0, {0x5f, 0x68}), # PEAHAT + 0xA1: (0, {0x5f, 0x7b}), # SNAKE + 0xA2: (3, 0x64), # PIRANHA_PLANT + 0xA3: (1, 0x65), # SIDE_VIEW_PLATFORM_HORIZONTAL + 0xA4: (1, 0x65), # SIDE_VIEW_PLATFORM_VERTICAL + 0xA5: (1, 0x65), # SIDE_VIEW_PLATFORM + 0xA6: (1, 0x65), # SIDE_VIEW_WEIGHTS + 0xA7: (0, 0x66), # SMASHABLE_PILLAR + 0xA9: (2, 0x5d), # BLOOPER + 0xAA: (2, 0x5d), # CHEEP_CHEEP_HORIZONTAL + 0xAB: (2, 0x5d), # CHEEP_CHEEP_VERTICAL + 0xAC: (2, 0x5d), # CHEEP_CHEEP_JUMPING + 0xAD: (3, 0x67), # KIKI_THE_MONKEY + 0xAE: (1, 0xE3), # WINGED_OCTOROCK + 0xAF: None, # TRADING_ITEM + 0xB0: (2, 0x8B), # PINCER + 0xB1: (0, 0x7b), # HOLE_FILLER (or 0x77) + 0xB2: (3, 0x8C), # BEETLE_SPAWNER + 0xB3: (3, 0x6B), # HONEYCOMB + 0xB4: (1, 0x6C), # TARIN + 0xB5: (3, 0x69), # BEAR + 0xB6: (3, 0x6D), # PAPAHL + 0xB7: (3, 0x71), # MERMAID + 0xB8: (1, 0xa1, 2, 0x75, 3, 0x4e), # FISHERMAN_UNDER_BRIDGE + 0xB9: (2, 0x79), # BUZZ_BLOB + 0xBA: (3, 0x76), # BOMBER + 0xBB: (3, 0x76), # BUSH_CRAWLER + 0xBC: (2, 0xa9), # GRIM_CREEPER + 0xBD: (2, 0x7a), # VIRE + 0xBE: (2, 0xa7), # BLAINO + 0xBF: (2, 0x82), # ZOMBIE + 0xC0: None, # MAZE_SIGNPOST + 0xC1: (2, 0x8F), # MARIN_AT_THE_SHORE + 0xC2: (1, 0x6C, 2, 0x8F), # MARIN_AT_TAL_TAL_HEIGHTS + 0xC3: (1, 0x7d, 2, 0x7e, 3, 0x7F), # MAMU_AND_FROGS + 0xC4: (2, 0x6E, 3, 0x6F), # WALRUS + 0xC5: (1, 0x81), # URCHIN + 0xC6: (1, 0x81), # SAND_CRAB + 0xC7: (0, 0xC0, 1, 0xc1, 2, 0xc2, 3, 0xc3), # MANBO_AND_FISHES + 0xCA: (3, 0xc7), # MAD_BATTER + 0xCB: (1, 0x61), # ZORA + 0xCC: (1, 0x4A), # FISH + 0xCD: lambda room: (1, 0xCC, 2, 0xCD, 3, 0xCE) if room.room == 0x2DD else (1, 0xD1, 2, 0xD2, 3, 0x6A) if room.room == 0x2FE else (3, 0xD4), # BANANAS_SCHULE_SALE + 0xCE: (3, 0x73), # MERMAID_STATUE + 0xCF: (1, 0xC9, 2, 0xCA, 3, 0xCB), # SEASHELL_MANSION + 0xD0: (1, 0xC4), # ANIMAL_D0 + 0xD1: (3, 0xCF), # ANIMAL_D1 + 0xD2: (3, 0xCF), # ANIMAL_D2 + 0xD3: (1, 0xC4), # BUNNY_D3 + 0xD6: (1, 0x65), # SIDE_VIEW_POT + 0xD7: (1, 0x65), # THWIMP + 0xD8: (2, 0xDA, 3, 0xDB), # THWOMP + 0xD9: (1, 0xD9), # THWOMP_RAMMABLE + 0xDA: (3, 0x64), # PODOBOO + 0xDB: (2, 0xDA), # GIANT_BUBBLE + 0xDC: lambda room: (0, 0xDD, 2, 0xDE) if room.room == 0x1E4 else (2, 0xD3, 3, 0xDD) if room.room == 0x29F else (3, 0xDC), # FLYING_ROOSTER_EVENTS + 0xDD: (1, 0xD5), # BOOK + 0xDE: None, # EGG_SONG_EVENT + 0xE0: (3, 0xD4), # MONKEY + 0xE1: (1, 0xDF), # WITCH_RAT + 0xE2: (3, 0xF4), # FLAME_SHOOTER + 0xE3: (3, 0x8C), # POKEY + 0xE4: (1, 0x80, 3, 0xA5), # MOBLIN_KING + 0xE5: None, # FLOATING_ITEM_2 + 0xE6: (0, 0xe8, 1, 0xe9, 2, 0xea, 3, 0xeb), # FINAL_NIGHTMARE + 0xE7: None, # KANALET_CASTLE_GATE_SWITCH + 0xE9: (0, 0x04, 1, 0x05), # COLOR_SHELL_RED + 0xEA: (0, 0x04, 1, 0x05), # COLOR_SHELL_GREEN + 0xEB: (0, 0x04, 1, 0x05), # COLOR_SHELL_BLUE + 0xEC: (2, 0x06), # COLOR_GHOUL_RED + 0xED: (2, 0x06), # COLOR_GHOUL_GREEN + 0xEE: (2, 0x06), # COLOR_GHOUL_BLUE + 0xEF: (3, 0x07), # ROTOSWITCH_RED + 0xF0: (3, 0x07), # ROTOSWITCH_YELLOW + 0xF1: (3, 0x07), # ROTOSWITCH_BLUE + 0xF2: (3, 0x07), # FLYING_HOPPER_BOMBS + 0xF3: (3, 0x07), # HOPPER + 0xF4: (0, 0x08, 1, 0x09, 2, 0x0A), # AVALAUNCH + 0xF6: (0, 0x0E), # COLOR_GUARDIAN_BLUE + 0xF7: (0, 0x0E), # COLOR_GUARDIAN_BLUE + 0xF8: (0, 0x0B, 1, 0x0C, 3, 0x0D), # GIANT_BUZZ_BLOB + 0xF9: (0, 0x11, 2, 0x10), # HARDHIT_BEETLE + 0xFA: lambda room: (0, 0x44) if room.room == 0x2F5 else None, # PHOTOGRAPHER +} + +assert len(NAME) == COUNT + + +class Entity: + def __init__(self, index): + self.index = index + self.group = None + self.physics_flags = None + self.bowwow_eat_flag = None + + +class Group: + def __init__(self, index): + self.index = index + self.health = None + self.link_damage = None + + +class EntityData: + def __init__(self, rom): + groups = rom.banks[0x03][0x01F6:0x01F6+COUNT] + group_count = max(groups) + 1 + group_damage_type = rom.banks[0x03][0x03EC:0x03EC+group_count*16] + damage_per_damage_type = rom.banks[0x03][0x073C:0x073C+8*16] + + self.entities = [] + self.groups = [] + for n in range(group_count): + g = Group(n) + g.health = rom.banks[0x03][0x07BC+n] + g.link_damage = rom.banks[0x03][0x07F1+n] + self.groups.append(g) + for n in range(COUNT): + e = Entity(n) + e.group = self.groups[groups[n]] + e.physics_flags = rom.banks[0x03][0x0000 + n] + e.bowwow_eat_flag = rom.banks[0x14][0x1218+n] + self.entities.append(e) + + #print(sum(bowwow_eatable)) + #for n in range(COUNT): + # if bowwow_eatable[n]: + # print(hex(n), NAME[n]) + for n in range(group_count): + entities = list(map(lambda data: NAME[data[0]], filter(lambda data: data[1] == n, enumerate(groups)))) + #print(hex(n), damage_to_link[n], entities) + dmg = bytearray() + for m in range(16): + dmg.append(damage_per_damage_type[m*8+group_damage_type[n*16+m]]) + import binascii + #print(binascii.hexlify(group_damage_type[n*16:n*16+16])) + #print(binascii.hexlify(dmg)) + + +if __name__ == "__main__": + from rom import ROM + import sys + rom = ROM(sys.argv[1]) + ed = EntityData(rom) + for e in ed.entities: + print(NAME[e.index], e.bowwow_eat_flag) diff --git a/worlds/ladx/LADXR/entranceInfo.py b/worlds/ladx/LADXR/entranceInfo.py new file mode 100644 index 000000000000..de1a247355e3 --- /dev/null +++ b/worlds/ladx/LADXR/entranceInfo.py @@ -0,0 +1,136 @@ + +class EntranceInfo: + def __init__(self, room, alt_room=None, *, type=None, dungeon=None, index=None, instrument_room=None, target=None): + if type is None and dungeon is not None: + type = "dungeon" + assert type is not None, "Missing entrance type" + self.type = type + self.room = room + self.alt_room = alt_room + self.dungeon = dungeon + self.index = index + self.instrument_room = instrument_room + self.target = target + + +ENTRANCE_INFO = { + # Row0-1 + "d8": EntranceInfo(0x10, target=0x25d, dungeon=8, instrument_room=0x230), + "phone_d8": EntranceInfo(0x11, target=0x299, type="dummy"), + "fire_cave_exit": EntranceInfo(0x03, target=0x1ee, type="connector"), + "fire_cave_entrance": EntranceInfo(0x13, target=0x1fe, type="connector"), + "madbatter_taltal": EntranceInfo(0x04, target=0x1e2, type="single"), + "left_taltal_entrance": EntranceInfo(0x15, target=0x2ea, type="connector"), + "obstacle_cave_entrance": EntranceInfo(0x17, target=0x2b6, type="connector"), + "left_to_right_taltalentrance": EntranceInfo(0x07, target=0x2ee, type="connector"), + "obstacle_cave_outside_chest": EntranceInfo(0x18, target=0x2bb, type="connector", index=0), + "obstacle_cave_exit": EntranceInfo(0x18, target=0x2bc, type="connector", index=1), + "papahl_entrance": EntranceInfo(0x19, target=0x289, type="connector"), + "papahl_exit": EntranceInfo(0x0A, target=0x28b, type="connector", index=0), + "rooster_house": EntranceInfo(0x0A, target=0x29f, type="dummy", index=2), + "bird_cave": EntranceInfo(0x0A, target=0x27e, type="single", index=1), + "multichest_left": EntranceInfo(0x1D, target=0x2f9, type="connector", index=0), + "multichest_right": EntranceInfo(0x1D, target=0x2fa, type="connector", index=1), + "multichest_top": EntranceInfo(0x0D, target=0x2f2, type="connector"), + "right_taltal_connector1": EntranceInfo(0x1E, target=0x280, type="connector", index=0), + "right_taltal_connector2": EntranceInfo(0x1F, target=0x282, type="connector", index=0), + "right_taltal_connector3": EntranceInfo(0x1E, target=0x283, type="connector", index=1), + "right_taltal_connector4": EntranceInfo(0x1F, target=0x287, type="connector", index=2), + "right_taltal_connector5": EntranceInfo(0x1F, target=0x28c, type="connector", index=1), + "right_taltal_connector6": EntranceInfo(0x0F, target=0x28e, type="connector"), + "right_fairy": EntranceInfo(0x1F, target=0x1fb, type="dummy", index=3), + "d7": EntranceInfo(0x0E, "Alt0E", target=0x20e, dungeon=7, instrument_room=0x22C), + # Row 2-3 + "writes_cave_left": EntranceInfo(0x20, target=0x2ae, type="connector"), + "writes_cave_right": EntranceInfo(0x21, target=0x2af, type="connector"), + "writes_house": EntranceInfo(0x30, target=0x2a8, type="trade"), + "writes_phone": EntranceInfo(0x31, target=0x29b, type="dummy"), + "d2": EntranceInfo(0x24, target=0x136, dungeon=2, instrument_room=0x12A), + "moblin_cave": EntranceInfo(0x35, target=0x2f0, type="single"), + "photo_house": EntranceInfo(0x37, target=0x2b5, type="dummy"), + "mambo": EntranceInfo(0x2A, target=0x2fd, type="single"), + "d4": EntranceInfo(0x2B, "Alt2B", target=0x17a, dungeon=4, index=0, instrument_room=0x162), + # TODO + # "d4_connector": EntranceInfo(0x2B, "Alt2B", index=1), + # "d4_connector_exit": EntranceInfo(0x2D), + "heartpiece_swim_cave": EntranceInfo(0x2E, target=0x1f2, type="single"), + "raft_return_exit": EntranceInfo(0x2F, target=0x1e7, type="connector"), + "raft_house": EntranceInfo(0x3F, target=0x2b0, type="insanity"), + "raft_return_enter": EntranceInfo(0x8F, target=0x1f7, type="connector"), + # Forest and everything right of it + "hookshot_cave": EntranceInfo(0x42, target=0x2b3, type="single"), + "toadstool_exit": EntranceInfo(0x50, target=0x2ab, type="connector"), + "forest_madbatter": EntranceInfo(0x52, target=0x1e1, type="single"), + "toadstool_entrance": EntranceInfo(0x62, target=0x2bd, type="connector"), + "crazy_tracy": EntranceInfo(0x45, target=0x2ad, type="dummy"), + "witch": EntranceInfo(0x65, target=0x2a2, type="single"), + "graveyard_cave_left": EntranceInfo(0x75, target=0x2de, type="connector"), + "graveyard_cave_right": EntranceInfo(0x76, target=0x2df, type="connector"), + "d0": EntranceInfo(0x77, target=0x312, dungeon=9, index="all", instrument_room=0x301), + # Castle + "castle_jump_cave": EntranceInfo(0x78, target=0x1fd, type="single"), + "castle_main_entrance": EntranceInfo(0x69, target=0x2d3, type="connector"), + "castle_upper_left": EntranceInfo(0x59, target=0x2d5, type="connector", index=0), + "castle_upper_right": EntranceInfo(0x59, target=0x2d6, type="single", index=1), + "castle_secret_exit": EntranceInfo(0x49, target=0x1eb, type="connector"), + "castle_secret_entrance": EntranceInfo(0x4A, target=0x1ec, type="connector"), + "castle_phone": EntranceInfo(0x4B, target=0x2cc, type="dummy"), + # Mabe village + "papahl_house_left": EntranceInfo(0x82, target=0x2a5, type="connector", index=0), + "papahl_house_right": EntranceInfo(0x82, target=0x2a6, type="connector", index=1), + "dream_hut": EntranceInfo(0x83, target=0x2aa, type="single"), + "rooster_grave": EntranceInfo(0x92, target=0x1f4, type="single"), + "shop": EntranceInfo(0x93, target=0x2a1, type="single"), + "madambowwow": EntranceInfo(0xA1, target=0x2a7, type="dummy", index=1), + "kennel": EntranceInfo(0xA1, target=0x2b2, type="single", index=0), + "start_house": EntranceInfo(0xA2, target=0x2a3, type="start"), + "library": EntranceInfo(0xB0, target=0x1fa, type="dummy"), + "ulrira": EntranceInfo(0xB1, target=0x2a9, type="dummy"), + "mabe_phone": EntranceInfo(0xB2, target=0x2cb, type="dummy"), + "trendy_shop": EntranceInfo(0xB3, target=0x2a0, type="trade"), + # Ukuku Prairie + "prairie_left_phone": EntranceInfo(0xA4, target=0x2b4, type="dummy"), + "prairie_left_cave1": EntranceInfo(0x84, target=0x2cd, type="single"), + "prairie_left_cave2": EntranceInfo(0x86, target=0x2f4, type="single"), + "prairie_left_fairy": EntranceInfo(0x87, target=0x1f3, type="dummy"), + "mamu": EntranceInfo(0xD4, target=0x2fb, type="insanity"), + "d3": EntranceInfo(0xB5, target=0x152, dungeon=3, instrument_room=0x159), + "prairie_right_phone": EntranceInfo(0x88, target=0x29c, type="dummy"), + "seashell_mansion": EntranceInfo(0x8A, target=0x2e9, type="single"), + "prairie_right_cave_top": EntranceInfo(0xB8, target=0x292, type="connector", index=1), + "prairie_right_cave_bottom": EntranceInfo(0xC8, target=0x293, type="connector"), + "prairie_right_cave_high": EntranceInfo(0xB8, target=0x295, type="connector", index=0), + "prairie_to_animal_connector": EntranceInfo(0xAA, target=0x2d0, type="connector"), + "animal_to_prairie_connector": EntranceInfo(0xAB, target=0x2d1, type="connector"), + + "d6": EntranceInfo(0x8C, "Alt8C", target=0x1d4, dungeon=6, instrument_room=0x1B5), + "d6_connector_exit": EntranceInfo(0x9C, target=0x1f0, type="connector"), + "d6_connector_entrance": EntranceInfo(0x9D, target=0x1f1, type="connector"), + "armos_fairy": EntranceInfo(0x8D, target=0x1ac, type="dummy"), + "armos_maze_cave": EntranceInfo(0xAE, target=0x2fc, type="single"), + "armos_temple": EntranceInfo(0xAC, target=0x28f, type="single"), + # Beach area + "d1": EntranceInfo(0xD3, target=0x117, dungeon=1, instrument_room=0x102), + "boomerang_cave": EntranceInfo(0xF4, target=0x1f5, type="single", instrument_room="Alt1F5"), # instrument_room is to configure the exit on the alt room layout + "banana_seller": EntranceInfo(0xE3, target=0x2fe, type="trade"), + "ghost_house": EntranceInfo(0xF6, target=0x1e3, type="single"), + + # Lower prairie + "richard_house": EntranceInfo(0xD6, target=0x2c7, type="connector"), + "richard_maze": EntranceInfo(0xC6, target=0x2c9, type="connector"), + "prairie_low_phone": EntranceInfo(0xE8, target=0x29d, type="dummy"), + "prairie_madbatter_connector_entrance": EntranceInfo(0xF9, target=0x1f6, type="connector"), + "prairie_madbatter_connector_exit": EntranceInfo(0xE7, target=0x1e5, type="connector"), + "prairie_madbatter": EntranceInfo(0xE6, target=0x1e0, type="single"), + + "d5": EntranceInfo(0xD9, target=0x1a1, dungeon=5, instrument_room=0x182), + # Animal village + "animal_phone": EntranceInfo(0xDB, target=0x2e3, type="dummy"), + "animal_house1": EntranceInfo(0xCC, target=0x2db, type="dummy", index=0), + "animal_house2": EntranceInfo(0xCC, target=0x2dd, type="dummy", index=1), + "animal_house3": EntranceInfo(0xCD, target=0x2d9, type="trade", index=1), + "animal_house4": EntranceInfo(0xCD, target=0x2da, type="dummy", index=2), + "animal_house5": EntranceInfo(0xDD, target=0x2d7, type="trade"), + "animal_cave": EntranceInfo(0xCD, target=0x2f7, type="single", index=0), + "desert_cave": EntranceInfo(0xCF, target=0x1f9, type="single"), +} diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py new file mode 100644 index 000000000000..0564e276eaa2 --- /dev/null +++ b/worlds/ladx/LADXR/generator.py @@ -0,0 +1,427 @@ +import binascii +import importlib.util +import importlib.machinery +import os + +from .romTables import ROMWithTables +from . import assembler +from . import mapgen +from . import patches +from .patches import overworld as _ +from .patches import dungeon as _ +from .patches import entrances as _ +from .patches import enemies as _ +from .patches import titleScreen as _ +from .patches import aesthetics as _ +from .patches import music as _ +from .patches import core as _ +from .patches import phone as _ +from .patches import photographer as _ +from .patches import owl as _ +from .patches import bank3e as _ +from .patches import bank3f as _ +from .patches import inventory as _ +from .patches import witch as _ +from .patches import tarin as _ +from .patches import fishingMinigame as _ +from .patches import softlock as _ +from .patches import maptweaks as _ +from .patches import chest as _ +from .patches import bomb as _ +from .patches import rooster as _ +from .patches import shop as _ +from .patches import trendy as _ +from .patches import goal as _ +from .patches import hardMode as _ +from .patches import weapons as _ +from .patches import health as _ +from .patches import heartPiece as _ +from .patches import droppedKey as _ +from .patches import goldenLeaf as _ +from .patches import songs as _ +from .patches import bowwow as _ +from .patches import desert as _ +from .patches import reduceRNG as _ +from .patches import madBatter as _ +from .patches import tunicFairy as _ +from .patches import seashell as _ +from .patches import instrument as _ +from .patches import endscreen as _ +from .patches import save as _ +from .patches import bingo as _ +from .patches import multiworld as _ +from .patches import tradeSequence as _ +from . import hints + +from .locations.keyLocation import KeyLocation +from .patches import bank34 + +from ..Options import TrendyGame, Palette + + +# Function to generate a final rom, this patches the rom with all required patches +def generateRom(args, settings, ap_settings, seed, logic, rnd=None, multiworld=None, player_name=None, player_names=[], player_id = 0): + rom = ROMWithTables(args.input_filename) + rom.player_names = player_names + pymods = [] + if args.pymod: + for pymod in args.pymod: + spec = importlib.util.spec_from_loader(pymod, importlib.machinery.SourceFileLoader(pymod, pymod)) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + pymods.append(module) + for pymod in pymods: + pymod.prePatch(rom) + + if settings.gfxmod: + patches.aesthetics.gfxMod(rom, os.path.join("data", "sprites", "ladx", settings.gfxmod)) + + item_list = [item for item in logic.iteminfo_list if not isinstance(item, KeyLocation)] + + assembler.resetConsts() + assembler.const("INV_SIZE", 16) + assembler.const("wHasFlippers", 0xDB3E) + assembler.const("wHasMedicine", 0xDB3F) + assembler.const("wTradeSequenceItem", 0xDB40) # we use it to store flags of which trade items we have + assembler.const("wTradeSequenceItem2", 0xDB7F) # Normally used to store that we have exchanged the trade item, we use it to store flags of which trade items we have + assembler.const("wSeashellsCount", 0xDB41) + assembler.const("wGoldenLeaves", 0xDB42) # New memory location where to store the golden leaf counter + assembler.const("wCollectedTunics", 0xDB6D) # Memory location where to store which tunic options are available + assembler.const("wCustomMessage", 0xC0A0) + + # We store the link info in unused color dungeon flags, so it gets preserved in the savegame. + assembler.const("wLinkSyncSequenceNumber", 0xDDF6) + assembler.const("wLinkStatusBits", 0xDDF7) + assembler.const("wLinkGiveItem", 0xDDF8) + assembler.const("wLinkGiveItemFrom", 0xDDF9) + assembler.const("wLinkSendItemRoomHigh", 0xDDFA) + assembler.const("wLinkSendItemRoomLow", 0xDDFB) + assembler.const("wLinkSendItemTarget", 0xDDFC) + assembler.const("wLinkSendItemItem", 0xDDFD) + + assembler.const("wZolSpawnCount", 0xDE10) + assembler.const("wCuccoSpawnCount", 0xDE11) + assembler.const("wDropBombSpawnCount", 0xDE12) + assembler.const("wLinkSpawnDelay", 0xDE13) + + #assembler.const("HARDWARE_LINK", 1) + assembler.const("HARD_MODE", 1 if settings.hardmode != "none" else 0) + + patches.core.cleanup(rom) + patches.save.singleSaveSlot(rom) + patches.phone.patchPhone(rom) + patches.photographer.fixPhotographer(rom) + patches.core.bugfixWrittingWrongRoomStatus(rom) + patches.core.bugfixBossroomTopPush(rom) + patches.core.bugfixPowderBagSprite(rom) + patches.core.fixEggDeathClearingItems(rom) + patches.core.disablePhotoPrint(rom) + patches.core.easyColorDungeonAccess(rom) + patches.owl.removeOwlEvents(rom) + patches.enemies.fixArmosKnightAsMiniboss(rom) + patches.bank3e.addBank3E(rom, seed, player_id, player_names) + patches.bank3f.addBank3F(rom) + patches.bank34.addBank34(rom, item_list) + patches.core.removeGhost(rom) + patches.core.fixMarinFollower(rom) + patches.core.fixWrongWarp(rom) + patches.core.alwaysAllowSecretBook(rom) + patches.core.injectMainLoop(rom) + + from ..Options import ShuffleSmallKeys, ShuffleNightmareKeys + + if ap_settings["shuffle_small_keys"] != ShuffleSmallKeys.option_original_dungeon or ap_settings["shuffle_nightmare_keys"] != ShuffleNightmareKeys.option_original_dungeon: + patches.inventory.advancedInventorySubscreen(rom) + patches.inventory.moreSlots(rom) + if settings.witch: + patches.witch.updateWitch(rom) + patches.softlock.fixAll(rom) + patches.maptweaks.tweakMap(rom) + patches.chest.fixChests(rom) + patches.shop.fixShop(rom) + patches.rooster.patchRooster(rom) + patches.trendy.fixTrendy(rom) + patches.droppedKey.fixDroppedKey(rom) + patches.madBatter.upgradeMadBatter(rom) + patches.tunicFairy.upgradeTunicFairy(rom) + patches.tarin.updateTarin(rom) + patches.fishingMinigame.updateFinishingMinigame(rom) + patches.health.upgradeHealthContainers(rom) + if settings.owlstatues in ("dungeon", "both"): + patches.owl.upgradeDungeonOwlStatues(rom) + if settings.owlstatues in ("overworld", "both"): + patches.owl.upgradeOverworldOwlStatues(rom) + patches.goldenLeaf.fixGoldenLeaf(rom) + patches.heartPiece.fixHeartPiece(rom) + patches.seashell.fixSeashell(rom) + patches.instrument.fixInstruments(rom) + patches.seashell.upgradeMansion(rom) + patches.songs.upgradeMarin(rom) + patches.songs.upgradeManbo(rom) + patches.songs.upgradeMamu(rom) + if settings.tradequest: + patches.tradeSequence.patchTradeSequence(rom, settings.boomerang) + else: + # Monkey bridge patch, always have the bridge there. + rom.patch(0x00, 0x333D, assembler.ASM("bit 4, e\njr Z, $05"), b"", fill_nop=True) + patches.bowwow.fixBowwow(rom, everywhere=settings.bowwow != 'normal') + if settings.bowwow != 'normal': + patches.bowwow.bowwowMapPatches(rom) + patches.desert.desertAccess(rom) + if settings.overworld == 'dungeondive': + patches.overworld.patchOverworldTilesets(rom) + patches.overworld.createDungeonOnlyOverworld(rom) + elif settings.overworld == 'nodungeons': + patches.dungeon.patchNoDungeons(rom) + elif settings.overworld == 'random': + patches.overworld.patchOverworldTilesets(rom) + mapgen.store_map(rom, logic.world.map) + #if settings.dungeon_items == 'keysy': + # patches.dungeon.removeKeyDoors(rom) + # patches.reduceRNG.slowdownThreeOfAKind(rom) + patches.reduceRNG.fixHorseHeads(rom) + patches.bomb.onlyDropBombsWhenHaveBombs(rom) + # patches.aesthetics.noSwordMusic(rom) + patches.aesthetics.reduceMessageLengths(rom, rnd) + patches.aesthetics.allowColorDungeonSpritesEverywhere(rom) + if settings.music == 'random': + patches.music.randomizeMusic(rom, rnd) + elif settings.music == 'off': + patches.music.noMusic(rom) + if settings.noflash: + patches.aesthetics.removeFlashingLights(rom) + if settings.hardmode == "oracle": + patches.hardMode.oracleMode(rom) + elif settings.hardmode == "hero": + patches.hardMode.heroMode(rom) + elif settings.hardmode == "ohko": + patches.hardMode.oneHitKO(rom) + if settings.superweapons: + patches.weapons.patchSuperWeapons(rom) + if settings.textmode == 'fast': + patches.aesthetics.fastText(rom) + if settings.textmode == 'none': + patches.aesthetics.fastText(rom) + patches.aesthetics.noText(rom) + if not settings.nagmessages: + patches.aesthetics.removeNagMessages(rom) + if settings.lowhpbeep == 'slow': + patches.aesthetics.slowLowHPBeep(rom) + if settings.lowhpbeep == 'none': + patches.aesthetics.removeLowHPBeep(rom) + if 0 <= int(settings.linkspalette): + patches.aesthetics.forceLinksPalette(rom, int(settings.linkspalette)) + if args.romdebugmode: + # The default rom has this build in, just need to set a flag and we get this save. + rom.patch(0, 0x0003, "00", "01") + + # Patch the sword check on the shopkeeper turning around. + if settings.steal == 'never': + rom.patch(4, 0x36F9, "FA4EDB", "3E0000") + elif settings.steal == 'always': + rom.patch(4, 0x36F9, "FA4EDB", "3E0100") + + if settings.hpmode == 'inverted': + patches.health.setStartHealth(rom, 9) + elif settings.hpmode == '1': + patches.health.setStartHealth(rom, 1) + + patches.inventory.songSelectAfterOcarinaSelect(rom) + if settings.quickswap == 'a': + patches.core.quickswap(rom, 1) + elif settings.quickswap == 'b': + patches.core.quickswap(rom, 0) + + # TODO: hints bad + + world_setup = logic.world_setup + + + hints.addHints(rom, rnd, item_list) + + if world_setup.goal == "raft": + patches.goal.setRaftGoal(rom) + elif world_setup.goal in ("bingo", "bingo-full"): + patches.bingo.setBingoGoal(rom, world_setup.bingo_goals, world_setup.goal) + elif world_setup.goal == "seashells": + patches.goal.setSeashellGoal(rom, 20) + else: + patches.goal.setRequiredInstrumentCount(rom, world_setup.goal) + + # Patch the generated logic into the rom + patches.chest.setMultiChest(rom, world_setup.multichest) + if settings.overworld not in {"dungeondive", "random"}: + patches.entrances.changeEntrances(rom, world_setup.entrance_mapping) + for spot in item_list: + if spot.item and spot.item.startswith("*"): + spot.item = spot.item[1:] + mw = None + if spot.item_owner != spot.location_owner: + mw = spot.item_owner + if mw > 255: + # Don't torture the game with higher slot numbers + mw = 255 + spot.patch(rom, spot.item, multiworld=mw) + patches.enemies.changeBosses(rom, world_setup.boss_mapping) + patches.enemies.changeMiniBosses(rom, world_setup.miniboss_mapping) + + if not args.romdebugmode: + patches.core.addFrameCounter(rom, len(item_list)) + + patches.core.warpHome(rom) # Needs to be done after setting the start location. + patches.titleScreen.setRomInfo(rom, binascii.hexlify(seed).decode("ascii").upper(), settings, player_name, player_id) + patches.endscreen.updateEndScreen(rom) + patches.aesthetics.updateSpriteData(rom) + if args.doubletrouble: + patches.enemies.doubleTrouble(rom) + + if ap_settings["trendy_game"] != TrendyGame.option_normal: + + # TODO: if 0 or 4, 5, remove inaccurate conveyor tiles + + from .roomEditor import RoomEditor, Object + room_editor = RoomEditor(rom, 0x2A0) + + if ap_settings["trendy_game"] == TrendyGame.option_easy: + # Set physics flag on all objects + for i in range(0, 6): + rom.banks[0x4][0x6F1E + i -0x4000] = 0x4 + else: + # All levels + # Set physics flag on yoshi + rom.banks[0x4][0x6F21-0x4000] = 0x3 + # Add new conveyor to "push" yoshi (it's only a visual) + room_editor.objects.append(Object(5, 3, 0xD0)) + + if int(ap_settings["trendy_game"]) >= TrendyGame.option_harder: + """ + Data_004_76A0:: + db $FC, $00, $04, $00, $00 + + Data_004_76A5:: + db $00, $04, $00, $FC, $00 + """ + speeds = { + TrendyGame.option_harder: (3, 8), + TrendyGame.option_hardest: (3, 8), + TrendyGame.option_impossible: (3, 16), + } + def speed(): + return rnd.randint(*speeds[ap_settings["trendy_game"]]) + rom.banks[0x4][0x76A0-0x4000] = 0xFF - speed() + rom.banks[0x4][0x76A2-0x4000] = speed() + rom.banks[0x4][0x76A6-0x4000] = speed() + rom.banks[0x4][0x76A8-0x4000] = 0xFF - speed() + if int(ap_settings["trendy_game"]) >= TrendyGame.option_hardest: + rom.banks[0x4][0x76A1-0x4000] = 0xFF - speed() + rom.banks[0x4][0x76A3-0x4000] = speed() + rom.banks[0x4][0x76A5-0x4000] = speed() + rom.banks[0x4][0x76A7-0x4000] = 0xFF - speed() + + room_editor.store(rom) + # This doesn't work, you can set random conveyors, but they aren't used + # for x in range(3, 9): + # for y in range(1, 5): + # room_editor.objects.append(Object(x, y, 0xCF + rnd.randint(0, 3))) + + # Attempt at imitating gb palette, fails + if False: + gb_colors = [ + [0x0f, 0x38, 0x0f], + [0x30, 0x62, 0x30], + [0x8b, 0xac, 0x0f], + [0x9b, 0xbc, 0x0f], + ] + for color in gb_colors: + for channel in range(3): + color[channel] = color[channel] * 31 // 0xbc + + + palette = ap_settings["palette"] + if palette != Palette.option_normal: + ranges = { + # Object palettes + # Overworld palettes + # Dungeon palettes + # Interior palettes + "code/palettes.asm 1": (0x21, 0x1518, 0x34A0), + # Intro/outro(?) + # File select + # S+Q + # Map + "code/palettes.asm 2": (0x21, 0x3536, 0x3FFE), + # Used for transitioning in and out of forest + "backgrounds/palettes.asm": (0x24, 0x3478, 0x3578), + # Haven't yet found menu palette + } + + for name, (bank, start, end) in ranges.items(): + def clamp(x, min, max): + if x < min: + return min + if x > max: + return max + return x + def bin_to_rgb(word): + red = word & 0b11111 + word >>= 5 + green = word & 0b11111 + word >>= 5 + blue = word & 0b11111 + return (red, green, blue) + def rgb_to_bin(r, g, b): + return (b << 10) | (g << 5) | r + + for address in range(start, end, 2): + packed = (rom.banks[bank][address + 1] << 8) | rom.banks[bank][address] + r,g,b = bin_to_rgb(packed) + + # 1 bit + if palette == Palette.option_1bit: + r &= 0b10000 + g &= 0b10000 + b &= 0b10000 + # 2 bit + elif palette == Palette.option_1bit: + r &= 0b11000 + g &= 0b11000 + b &= 0b11000 + # Invert + elif palette == Palette.option_inverted: + r = 31 - r + g = 31 - g + b = 31 - b + # Pink + elif palette == Palette.option_pink: + r = r // 2 + r += 16 + r = int(r) + r = clamp(r, 0, 0x1F) + b = b // 2 + b += 16 + b = int(b) + b = clamp(b, 0, 0x1F) + elif palette == Palette.option_greyscale: + # gray=int(0.299*r+0.587*g+0.114*b) + gray = (r + g + b) // 3 + r = g = b = gray + + packed = rgb_to_bin(r, g, b) + rom.banks[bank][address] = packed & 0xFF + rom.banks[bank][address + 1] = packed >> 8 + + SEED_LOCATION = 0x0134 + SEED_SIZE = 10 + + # TODO: pass this in + # Patch over the title + assert(len(seed) == SEED_SIZE) + gameid = seed + player_id.to_bytes(2, 'big') + rom.patch(0x00, SEED_LOCATION, None, binascii.hexlify(gameid)) + + + for pymod in pymods: + pymod.postPatch(rom) + + + return rom diff --git a/worlds/ladx/LADXR/getGFX.py b/worlds/ladx/LADXR/getGFX.py new file mode 100644 index 000000000000..e7d327ef6503 --- /dev/null +++ b/worlds/ladx/LADXR/getGFX.py @@ -0,0 +1,41 @@ +import requests +import PIL.Image +import re + +url = "https://raw.githubusercontent.com/CrystalSaver/Z4RandomizerBeta2/master/" + +for k, v in requests.get(url + "asset-manifest.json").json()['files'].items(): + m = re.match("static/media/Graphics(.+)\\.bin", k) + assert m is not None + if not k.startswith("static/media/Graphics") or not k.endswith(".bin"): + continue + name = m.group(1) + + data = requests.get(url + v).content + + icon = PIL.Image.new("P", (16, 16)) + buffer = bytearray(b'\x00' * 16 * 8) + for idx in range(0x0C0, 0x0C2): + for y in range(16): + a = data[idx * 32 + y * 2] + b = data[idx * 32 + y * 2 + 1] + for x in range(8): + v = 0 + if a & (0x80 >> x): + v |= 1 + if b & (0x80 >> x): + v |= 2 + buffer[x+y*8] = v + tile = PIL.Image.frombytes('P', (8, 16), bytes(buffer)) + x = (idx % 16) * 8 + icon.paste(tile, (x, 0)) + pal = icon.getpalette() + assert pal is not None + pal[0:3] = [150, 150, 255] + pal[3:6] = [0, 0, 0] + pal[6:9] = [59, 180, 112] + pal[9:12] = [251, 221, 197] + icon.putpalette(pal) + icon = icon.resize((32, 32)) + icon.save("gfx/%s.bin.png" % (name)) + open("gfx/%s.bin" % (name), "wb").write(data) diff --git a/worlds/ladx/LADXR/hints.py b/worlds/ladx/LADXR/hints.py new file mode 100644 index 000000000000..f7ef78a3e8bc --- /dev/null +++ b/worlds/ladx/LADXR/hints.py @@ -0,0 +1,66 @@ +from .locations.items import * +from .utils import formatText + + +hint_text_ids = [ + # Overworld owl statues + 0x1B6, 0x1B7, 0x1B8, 0x1B9, 0x1BA, 0x1BB, 0x1BC, 0x1BD, 0x1BE, 0x22D, + + 0x288, 0x280, # D1 + 0x28A, 0x289, 0x281, # D2 + 0x282, 0x28C, 0x28B, # D3 + 0x283, # D4 + 0x28D, 0x284, # D5 + 0x285, 0x28F, 0x28E, # D6 + 0x291, 0x290, 0x286, # D7 + 0x293, 0x287, 0x292, # D8 + 0x263, # D0 + + # Hint books + 0x267, # color dungeon + 0x201, # Pre open: 0x200 + 0x203, # Pre open: 0x202 + 0x205, # Pre open: 0x204 + 0x207, # Pre open: 0x206 + 0x209, # Pre open: 0x208 + 0x20B, # Pre open: 0x20A +] + +hint_items = (POWER_BRACELET, SHIELD, BOW, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, OCARINA, FEATHER, SHOVEL, + MAGIC_POWDER, SWORD, FLIPPERS, TAIL_KEY, ANGLER_KEY, FACE_KEY, + BIRD_KEY, SLIME_KEY, GOLD_LEAF, BOOMERANG, BOWWOW) + +hints = [ + "{0} is at {1}", + "If you want {0} start looking in {1}", + "{1} holds {0}", + "They say that {0} is at {1}", + "You might want to look in {1} for a secret", +] +useless_hint = [ + ("Egg", "Mt. Tamaranch"), + ("Marin", "Mabe Village"), + ("Marin", "Mabe Village"), + ("Witch", "Koholint Prairie"), + ("Mermaid", "Martha's Bay"), + ("Nothing", "Tabahl Wasteland"), + ("Animals", "Animal Village"), + ("Sand", "Yarna Desert"), +] + + +def addHints(rom, rnd, spots): + spots = list(sorted(filter(lambda spot: spot.item in hint_items, spots), key=lambda spot: spot.nameId)) + text_ids = hint_text_ids.copy() + rnd.shuffle(text_ids) + for text_id in text_ids: + if len(spots) > 0: + spot_index = rnd.randint(0, len(spots) - 1) + spot = spots.pop(spot_index) + hint = rnd.choice(hints).format("{%s}" % (spot.item), spot.metadata.area) + else: + hint = rnd.choice(hints).format(*rnd.choice(useless_hint)) + rom.texts[text_id] = formatText(hint) + + for text_id in range(0x200, 0x20C, 2): + rom.texts[text_id] = formatText("Read this book?", ask="YES NO") diff --git a/worlds/ladx/LADXR/itempool.py b/worlds/ladx/LADXR/itempool.py new file mode 100644 index 000000000000..50314883378a --- /dev/null +++ b/worlds/ladx/LADXR/itempool.py @@ -0,0 +1,278 @@ +from .locations.items import * + + +DEFAULT_ITEM_POOL = { + SWORD: 2, + FEATHER: 1, + HOOKSHOT: 1, + BOW: 1, + BOMB: 1, + MAGIC_POWDER: 1, + MAGIC_ROD: 1, + OCARINA: 1, + PEGASUS_BOOTS: 1, + POWER_BRACELET: 2, + SHIELD: 2, + SHOVEL: 1, + ROOSTER: 1, + TOADSTOOL: 1, + + TAIL_KEY: 1, SLIME_KEY: 1, ANGLER_KEY: 1, FACE_KEY: 1, BIRD_KEY: 1, + GOLD_LEAF: 5, + + FLIPPERS: 1, + BOWWOW: 1, + SONG1: 1, SONG2: 1, SONG3: 1, + + BLUE_TUNIC: 1, RED_TUNIC: 1, + MAX_ARROWS_UPGRADE: 1, MAX_BOMBS_UPGRADE: 1, MAX_POWDER_UPGRADE: 1, + + HEART_CONTAINER: 8, + HEART_PIECE: 12, + + RUPEES_100: 3, + RUPEES_20: 6, + RUPEES_200: 3, + RUPEES_50: 19, + + SEASHELL: 24, + MEDICINE: 3, + GEL: 4, + MESSAGE: 1, + + COMPASS1: 1, COMPASS2: 1, COMPASS3: 1, COMPASS4: 1, COMPASS5: 1, COMPASS6: 1, COMPASS7: 1, COMPASS8: 1, COMPASS9: 1, + KEY1: 3, KEY2: 5, KEY3: 9, KEY4: 5, KEY5: 3, KEY6: 3, KEY7: 3, KEY8: 7, KEY9: 3, + MAP1: 1, MAP2: 1, MAP3: 1, MAP4: 1, MAP5: 1, MAP6: 1, MAP7: 1, MAP8: 1, MAP9: 1, + NIGHTMARE_KEY1: 1, NIGHTMARE_KEY2: 1, NIGHTMARE_KEY3: 1, NIGHTMARE_KEY4: 1, NIGHTMARE_KEY5: 1, NIGHTMARE_KEY6: 1, NIGHTMARE_KEY7: 1, NIGHTMARE_KEY8: 1, NIGHTMARE_KEY9: 1, + STONE_BEAK1: 1, STONE_BEAK2: 1, STONE_BEAK3: 1, STONE_BEAK4: 1, STONE_BEAK5: 1, STONE_BEAK6: 1, STONE_BEAK7: 1, STONE_BEAK8: 1, STONE_BEAK9: 1, + + INSTRUMENT1: 1, INSTRUMENT2: 1, INSTRUMENT3: 1, INSTRUMENT4: 1, INSTRUMENT5: 1, INSTRUMENT6: 1, INSTRUMENT7: 1, INSTRUMENT8: 1, + + TRADING_ITEM_YOSHI_DOLL: 1, + TRADING_ITEM_RIBBON: 1, + TRADING_ITEM_DOG_FOOD: 1, + TRADING_ITEM_BANANAS: 1, + TRADING_ITEM_STICK: 1, + TRADING_ITEM_HONEYCOMB: 1, + TRADING_ITEM_PINEAPPLE: 1, + TRADING_ITEM_HIBISCUS: 1, + TRADING_ITEM_LETTER: 1, + TRADING_ITEM_BROOM: 1, + TRADING_ITEM_FISHING_HOOK: 1, + TRADING_ITEM_NECKLACE: 1, + TRADING_ITEM_SCALE: 1, + TRADING_ITEM_MAGNIFYING_GLASS: 1, + + "MEDICINE2": 1, "RAFT": 1, "ANGLER_KEYHOLE": 1, "CASTLE_BUTTON": 1 +} + + +class ItemPool: + def __init__(self, logic, settings, rnd): + self.__pool = {} + self.__setup(logic, settings) + self.__randomizeRupees(settings, rnd) + + def add(self, item, count=1): + self.__pool[item] = self.__pool.get(item, 0) + count + + def remove(self, item, count=1): + self.__pool[item] = self.__pool.get(item, 0) - count + if self.__pool[item] == 0: + del self.__pool[item] + + def get(self, item): + return self.__pool.get(item, 0) + + def count(self): + total = 0 + for count in self.__pool.values(): + total += count + return total + + def removeRupees(self, count): + for n in range(count): + self.removeRupee() + + def removeRupee(self): + for item in (RUPEES_20, RUPEES_50, RUPEES_200, RUPEES_500): + if self.get(item) > 0: + self.remove(item) + return + raise RuntimeError("Wanted to remove more rupees from the pool then we have") + + def __setup(self, logic, settings): + default_item_pool = DEFAULT_ITEM_POOL + if settings.overworld == "random": + default_item_pool = logic.world.map.get_item_pool() + for item, count in default_item_pool.items(): + self.add(item, count) + if settings.boomerang != 'default' and settings.overworld != "random": + self.add(BOOMERANG) + if settings.owlstatues == 'both': + self.add(RUPEES_20, 9 + 24) + elif settings.owlstatues == 'dungeon': + self.add(RUPEES_20, 24) + elif settings.owlstatues == 'overworld': + self.add(RUPEES_20, 9) + + if settings.bowwow == 'always': + # Bowwow mode takes a sword from the pool to give as bowwow. So we need to fix that. + self.add(SWORD) + self.remove(BOWWOW) + elif settings.bowwow == 'swordless': + # Bowwow mode takes a sword from the pool to give as bowwow, we need to remove all swords and Bowwow except for 1 + self.add(RUPEES_20, self.get(BOWWOW) + self.get(SWORD) - 1) + self.remove(SWORD, self.get(SWORD) - 1) + self.remove(BOWWOW, self.get(BOWWOW)) + if settings.hpmode == 'inverted': + self.add(BAD_HEART_CONTAINER, self.get(HEART_CONTAINER)) + self.remove(HEART_CONTAINER, self.get(HEART_CONTAINER)) + elif settings.hpmode == 'low': + self.add(HEART_PIECE, self.get(HEART_CONTAINER)) + self.remove(HEART_CONTAINER, self.get(HEART_CONTAINER)) + elif settings.hpmode == 'extralow': + self.add(RUPEES_20, self.get(HEART_CONTAINER)) + self.remove(HEART_CONTAINER, self.get(HEART_CONTAINER)) + + if settings.itempool == 'casual': + self.add(FLIPPERS) + self.add(FEATHER) + self.add(HOOKSHOT) + self.add(BOW) + self.add(BOMB) + self.add(MAGIC_POWDER) + self.add(MAGIC_ROD) + self.add(OCARINA) + self.add(PEGASUS_BOOTS) + self.add(POWER_BRACELET) + self.add(SHOVEL) + self.add(RUPEES_200, 2) + self.removeRupees(13) + + for n in range(9): + self.remove("MAP%d" % (n + 1)) + self.remove("COMPASS%d" % (n + 1)) + self.add("KEY%d" % (n + 1)) + self.add("NIGHTMARE_KEY%d" % (n +1)) + elif settings.itempool == 'pain': + self.add(BAD_HEART_CONTAINER, 12) + self.remove(BLUE_TUNIC) + self.remove(MEDICINE, 2) + self.remove(HEART_PIECE, 4) + self.removeRupees(5) + elif settings.itempool == 'keyup': + for n in range(9): + self.remove("MAP%d" % (n + 1)) + self.remove("COMPASS%d" % (n + 1)) + self.add("KEY%d" % (n +1)) + self.add("NIGHTMARE_KEY%d" % (n +1)) + if settings.owlstatues in ("none", "overworld"): + for n in range(9): + self.remove("STONE_BEAK%d" % (n + 1)) + self.add("KEY%d" % (n +1)) + + # if settings.dungeon_items == 'keysy': + # for n in range(9): + # for amount, item_name in ((9, "KEY"), (1, "NIGHTMARE_KEY")): + # item_name = "%s%d" % (item_name, n + 1) + # if item_name in self.__pool: + # self.add(RUPEES_20, self.__pool[item_name]) + # self.remove(item_name, self.__pool[item_name]) + # self.add(item_name, amount) + + if settings.goal == "seashells": + for n in range(8): + self.remove("INSTRUMENT%d" % (n + 1)) + self.add(SEASHELL, 8) + + if settings.overworld == "dungeondive": + self.remove(SWORD) + self.remove(MAX_ARROWS_UPGRADE) + self.remove(MAX_BOMBS_UPGRADE) + self.remove(MAX_POWDER_UPGRADE) + self.remove(SEASHELL, 24) + self.remove(TAIL_KEY) + self.remove(SLIME_KEY) + self.remove(ANGLER_KEY) + self.remove(FACE_KEY) + self.remove(BIRD_KEY) + self.remove(GOLD_LEAF, 5) + self.remove(SONG2) + self.remove(SONG3) + self.remove(HEART_PIECE, 8) + self.remove(RUPEES_50, 9) + self.remove(RUPEES_20, 2) + self.remove(MEDICINE, 3) + self.remove(MESSAGE) + self.remove(BOWWOW) + self.remove(ROOSTER) + self.remove(GEL, 2) + self.remove("MEDICINE2") + self.remove("RAFT") + self.remove("ANGLER_KEYHOLE") + self.remove("CASTLE_BUTTON") + self.remove(TRADING_ITEM_YOSHI_DOLL) + self.remove(TRADING_ITEM_RIBBON) + self.remove(TRADING_ITEM_DOG_FOOD) + self.remove(TRADING_ITEM_BANANAS) + self.remove(TRADING_ITEM_STICK) + self.remove(TRADING_ITEM_HONEYCOMB) + self.remove(TRADING_ITEM_PINEAPPLE) + self.remove(TRADING_ITEM_HIBISCUS) + self.remove(TRADING_ITEM_LETTER) + self.remove(TRADING_ITEM_BROOM) + self.remove(TRADING_ITEM_FISHING_HOOK) + self.remove(TRADING_ITEM_NECKLACE) + self.remove(TRADING_ITEM_SCALE) + self.remove(TRADING_ITEM_MAGNIFYING_GLASS) + elif not settings.rooster: + self.remove(ROOSTER) + self.add(RUPEES_50) + + if settings.overworld == "nodungeons": + for n in range(9): + for item_name in {KEY, NIGHTMARE_KEY, MAP, COMPASS, STONE_BEAK}: + self.remove(f"{item_name}{n+1}", self.get(f"{item_name}{n+1}")) + self.remove(BLUE_TUNIC) + self.remove(RED_TUNIC) + self.remove(SEASHELL, 2) + self.remove(RUPEES_20, 6) + self.remove(RUPEES_50, 17) + self.remove(MEDICINE, 3) + self.remove(GEL, 4) + self.remove(MESSAGE, 1) + self.remove(BOMB, 1) + self.remove(RUPEES_100, 3) + self.add(RUPEES_500, 3) + + # # In multiworld, put a bit more rupees in the seed, this helps with generation (2nd shop item) + # # As we cheat and can place rupees for the wrong player. + # if settings.multiworld: + # rupees20 = self.__pool.get(RUPEES_20, 0) + # self.add(RUPEES_50, rupees20 // 2) + # self.remove(RUPEES_20, rupees20 // 2) + # rupees50 = self.__pool.get(RUPEES_50, 0) + # self.add(RUPEES_200, rupees50 // 5) + # self.remove(RUPEES_50, rupees50 // 5) + + def __randomizeRupees(self, options, rnd): + # Remove rupees from the item pool and replace them with other items to create more variety + rupee_item = [] + rupee_item_count = [] + for k, v in self.__pool.items(): + if k in {RUPEES_20, RUPEES_50} and v > 0: + rupee_item.append(k) + rupee_item_count.append(v) + rupee_chests = sum(v for k, v in self.__pool.items() if k.startswith("RUPEES_")) + for n in range(rupee_chests // 5): + new_item = rnd.choices((BOMB, SINGLE_ARROW, ARROWS_10, MAGIC_POWDER, MEDICINE), (10, 5, 10, 10, 1))[0] + while True: + remove_item = rnd.choices(rupee_item, rupee_item_count)[0] + if self.get(remove_item) > 0: + break + self.add(new_item) + self.remove(remove_item) + + def toDict(self): + return self.__pool.copy() diff --git a/worlds/ladx/LADXR/locations/all.py b/worlds/ladx/LADXR/locations/all.py new file mode 100644 index 000000000000..a61734605143 --- /dev/null +++ b/worlds/ladx/LADXR/locations/all.py @@ -0,0 +1,26 @@ +from .beachSword import BeachSword +from .chest import Chest, DungeonChest +from .droppedKey import DroppedKey +from .seashell import Seashell, SeashellMansion +from .heartContainer import HeartContainer +from .owlStatue import OwlStatue +from .madBatter import MadBatter +from .shop import ShopItem +from .startItem import StartItem +from .toadstool import Toadstool +from .witch import Witch +from .goldLeaf import GoldLeaf, SlimeKey +from .boomerangGuy import BoomerangGuy +from .anglerKey import AnglerKey +from .hookshot import HookshotDrop +from .faceKey import FaceKey +from .birdKey import BirdKey +from .heartPiece import HeartPiece +from .tunicFairy import TunicFairy +from .song import Song +from .instrument import Instrument +from .fishingMinigame import FishingMinigame +from .keyLocation import KeyLocation +from .tradeSequence import TradeSequenceItem + +from .items import * diff --git a/worlds/ladx/LADXR/locations/anglerKey.py b/worlds/ladx/LADXR/locations/anglerKey.py new file mode 100644 index 000000000000..79382de8625d --- /dev/null +++ b/worlds/ladx/LADXR/locations/anglerKey.py @@ -0,0 +1,6 @@ +from .droppedKey import DroppedKey + + +class AnglerKey(DroppedKey): + def __init__(self): + super().__init__(0x0CE) \ No newline at end of file diff --git a/worlds/ladx/LADXR/locations/beachSword.py b/worlds/ladx/LADXR/locations/beachSword.py new file mode 100644 index 000000000000..51fc4388e325 --- /dev/null +++ b/worlds/ladx/LADXR/locations/beachSword.py @@ -0,0 +1,32 @@ +from .droppedKey import DroppedKey +from .items import * +from ..roomEditor import RoomEditor +from ..assembler import ASM +from typing import Optional +from ..rom import ROM + + +class BeachSword(DroppedKey): + def __init__(self) -> None: + super().__init__(0x0F2) + + def patch(self, rom: ROM, option: str, *, multiworld: Optional[int] = None) -> None: + if option != SWORD or multiworld is not None: + # Set the heart piece data + super().patch(rom, option, multiworld=multiworld) + + # Patch the room to contain a heart piece instead of the sword on the beach + re = RoomEditor(rom, 0x0F2) + re.removeEntities(0x31) # remove sword + re.addEntity(5, 5, 0x35) # add heart piece + re.store(rom) + + # Prevent shield drops from the like-like from turning into swords. + rom.patch(0x03, 0x1B9C, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True) + rom.patch(0x03, 0x244D, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True) + + def read(self, rom: ROM) -> str: + re = RoomEditor(rom, 0x0F2) + if re.hasEntity(0x31): + return SWORD + return super().read(rom) diff --git a/worlds/ladx/LADXR/locations/birdKey.py b/worlds/ladx/LADXR/locations/birdKey.py new file mode 100644 index 000000000000..12418c61aa46 --- /dev/null +++ b/worlds/ladx/LADXR/locations/birdKey.py @@ -0,0 +1,23 @@ +from .droppedKey import DroppedKey +from ..roomEditor import RoomEditor +from ..assembler import ASM + + +class BirdKey(DroppedKey): + def __init__(self): + super().__init__(0x27A) + + def patch(self, rom, option, *, multiworld=None): + super().patch(rom, option, multiworld=multiworld) + + re = RoomEditor(rom, self.room) + + # Make the bird key accessible without the rooster + re.removeObject(1, 6) + re.removeObject(2, 6) + re.removeObject(3, 5) + re.removeObject(3, 6) + re.moveObject(1, 5, 2, 6) + re.moveObject(2, 5, 3, 6) + re.addEntity(3, 5, 0x9D) + re.store(rom) diff --git a/worlds/ladx/LADXR/locations/boomerangGuy.py b/worlds/ladx/LADXR/locations/boomerangGuy.py new file mode 100644 index 000000000000..92d76cebdf5d --- /dev/null +++ b/worlds/ladx/LADXR/locations/boomerangGuy.py @@ -0,0 +1,94 @@ +from .itemInfo import ItemInfo +from .constants import * +from ..assembler import ASM +from ..utils import formatText + + +class BoomerangGuy(ItemInfo): + OPTIONS = [BOOMERANG, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, FEATHER, SHOVEL] + + def __init__(self): + super().__init__(0x1F5) + self.setting = 'trade' + + def configure(self, options): + self.MULTIWORLD = False + + self.setting = options.boomerang + if self.setting == 'gift': + self.MULTIWORLD = True + + # Cannot trade: + # SWORD, BOMB, SHIELD, POWER_BRACELET, OCARINA, MAGIC_POWDER, BOW + # Checks for these are at $46A2, and potentially we could remove those. + # But SHIELD, BOMB and MAGIC_POWDER would most likely break things. + # SWORD and POWER_BRACELET would most likely introduce the lv0 shield/bracelet issue + def patch(self, rom, option, *, multiworld=None): + # Always have the boomerang trade guy enabled (normally you need the magnifier) + rom.patch(0x19, 0x05EC, ASM("ld a, [wTradeSequenceItem]\ncp $0E"), ASM("ld a, $0E\ncp $0E"), fill_nop=True) # show the guy + rom.patch(0x00, 0x3199, ASM("ld a, [wTradeSequenceItem]\ncp $0E"), ASM("ld a, $0E\ncp $0E"), fill_nop=True) # load the proper room layout + rom.patch(0x19, 0x05F4, ASM("ld a, [wTradeSequenceItem2]\nand a"), ASM("xor a"), fill_nop=True) + + if self.setting == 'trade': + inv = INVENTORY_MAP[option] + # Patch the check if you traded back the boomerang (so traded twice) + rom.patch(0x19, 0x063F, ASM("cp $0D"), ASM("cp $%s" % (inv))) + # Item to give by "default" (aka, boomerang) + rom.patch(0x19, 0x06C1, ASM("ld a, $0D"), ASM("ld a, $%s" % (inv))) + # Check if inventory slot is boomerang to give back item in this slot + rom.patch(0x19, 0x06FC, ASM("cp $0D"), ASM("cp $%s" % (inv))) + # Put the boomerang ID in the inventory of the boomerang guy (aka, traded back) + rom.patch(0x19, 0x0710, ASM("ld a, $0D"), ASM("ld a, $%s" % (inv))) + + rom.texts[0x222] = formatText("Okay, let's do it!") + rom.texts[0x224] = formatText("You got the {%s} in exchange for the item you had." % (option)) + rom.texts[0x225] = formatText("Give me back my {%s}, I beg you! I'll return the item you gave me" % (option), ask="Okay Not Now") + rom.texts[0x226] = formatText("The item came back to you. You returned the other item.") + else: + # Patch the inventory trade to give an specific item instead + rom.texts[0x221] = formatText("I found a good item washed up on the beach... Want to have it?", ask="Okay No") + rom.patch(0x19, 0x069C, 0x06C6, ASM(""" + ; Mark trade as done + ld a, $06 + ld [$DB7D], a + + ld a, [$472B] + ldh [$F1], a + ld a, $06 + rst 8 + + ld a, $0D + """), fill_nop=True) + # Show the right item above link + rom.patch(0x19, 0x0786, 0x0793, ASM(""" + ld a, [$472B] + ldh [$F1], a + ld a, $01 + rst 8 + """), fill_nop=True) + # Give the proper message for this item + rom.patch(0x19, 0x075A, 0x076A, ASM(""" + ld a, [$472B] + ldh [$F1], a + ld a, $0A + rst 8 + """), fill_nop=True) + rom.patch(0x19, 0x072B, "00", "%02X" % (CHEST_ITEMS[option])) + + # Ignore the trade back. + rom.texts[0x225] = formatText("It's a secret to everybody.") + rom.patch(0x19, 0x0668, ASM("ld a, [$DB7D]"), ASM("ret"), fill_nop=True) + + if multiworld is not None: + rom.banks[0x3E][0x3300 + self.room] = multiworld + + def read(self, rom): + if rom.banks[0x19][0x06C5] == 0x00: + for k, v in CHEST_ITEMS.items(): + if v == rom.banks[0x19][0x072B]: + return k + else: + for k, v in INVENTORY_MAP.items(): + if int(v, 16) == rom.banks[0x19][0x0640]: + return k + raise ValueError() diff --git a/worlds/ladx/LADXR/locations/chest.py b/worlds/ladx/LADXR/locations/chest.py new file mode 100644 index 000000000000..578201bc1eb3 --- /dev/null +++ b/worlds/ladx/LADXR/locations/chest.py @@ -0,0 +1,50 @@ +from .itemInfo import ItemInfo +from .constants import * +from ..assembler import ASM + + +class Chest(ItemInfo): + def __init__(self, room): + super().__init__(room) + self.addr = room + 0x560 + + def patch(self, rom, option, *, multiworld=None): + rom.banks[0x14][self.addr] = CHEST_ITEMS[option] + + if self.room == 0x1B6: + # Patch the code that gives the nightmare key when you throw the pot at the chest in dungeon 6 + # As this is hardcoded for a specific chest type + rom.patch(3, 0x145D, ASM("ld a, $19"), ASM("ld a, $%02x" % (CHEST_ITEMS[option]))) + if multiworld is not None: + rom.banks[0x3E][0x3300 + self.room] = multiworld + + def read(self, rom): + value = rom.banks[0x14][self.addr] + for k, v in CHEST_ITEMS.items(): + if v == value: + return k + raise ValueError("Could not find chest contents in ROM (0x%02x)" % (value)) + + def __repr__(self): + return "%s:%03x" % (self.__class__.__name__, self.room) + + +class DungeonChest(Chest): + def patch(self, rom, option, *, multiworld=None): + if (option.startswith(MAP) and option != MAP) \ + or (option.startswith(COMPASS) and option != COMPASS) \ + or (option.startswith(STONE_BEAK) and option != STONE_BEAK) \ + or (option.startswith(NIGHTMARE_KEY) and option != NIGHTMARE_KEY) \ + or (option.startswith(KEY) and option != KEY): + if self._location.dungeon == int(option[-1]) and multiworld is None: + option = option[:-1] + super().patch(rom, option, multiworld=multiworld) + + def read(self, rom): + result = super().read(rom) + if result in [MAP, COMPASS, STONE_BEAK, NIGHTMARE_KEY, KEY]: + return "%s%d" % (result, self._location.dungeon) + return result + + def __repr__(self): + return "%s:%03x:%d" % (self.__class__.__name__, self.room, self._location.dungeon) diff --git a/worlds/ladx/LADXR/locations/constants.py b/worlds/ladx/LADXR/locations/constants.py new file mode 100644 index 000000000000..7bb8df5b3515 --- /dev/null +++ b/worlds/ladx/LADXR/locations/constants.py @@ -0,0 +1,131 @@ +from .items import * + +INVENTORY_MAP = { + SWORD: "01", + BOMB: "02", + POWER_BRACELET: "03", + SHIELD: "04", + BOW: "05", + HOOKSHOT: "06", + MAGIC_ROD: "07", + PEGASUS_BOOTS: "08", + OCARINA: "09", + FEATHER: "0A", + SHOVEL: "0B", + MAGIC_POWDER: "0C", + BOOMERANG: "0D", + TOADSTOOL: "0E", +} +CHEST_ITEMS = { + POWER_BRACELET: 0x00, + SHIELD: 0x01, + BOW: 0x02, + HOOKSHOT: 0x03, + MAGIC_ROD: 0x04, + PEGASUS_BOOTS: 0x05, + OCARINA: 0x06, + FEATHER: 0x07, SHOVEL: 0x08, MAGIC_POWDER: 0x09, BOMB: 0x0A, SWORD: 0x0B, FLIPPERS: 0x0C, + MAGNIFYING_LENS: 0x0D, MEDICINE: 0x10, + TAIL_KEY: 0x11, ANGLER_KEY: 0x12, FACE_KEY: 0x13, BIRD_KEY: 0x14, GOLD_LEAF: 0x15, + RUPEES_50: 0x1B, RUPEES_20: 0x1C, RUPEES_100: 0x1D, RUPEES_200: 0x1E, RUPEES_500: 0x1F, + SEASHELL: 0x20, MESSAGE: 0x21, GEL: 0x22, + MAP: 0x16, COMPASS: 0x17, STONE_BEAK: 0x18, NIGHTMARE_KEY: 0x19, KEY: 0x1A, + ROOSTER: 0x96, + + BOOMERANG: 0x0E, + SLIME_KEY: 0x0F, + + KEY1: 0x23, + KEY2: 0x24, + KEY3: 0x25, + KEY4: 0x26, + KEY5: 0x27, + KEY6: 0x28, + KEY7: 0x29, + KEY8: 0x2A, + KEY9: 0x2B, + + MAP1: 0x2C, + MAP2: 0x2D, + MAP3: 0x2E, + MAP4: 0x2F, + MAP5: 0x30, + MAP6: 0x31, + MAP7: 0x32, + MAP8: 0x33, + MAP9: 0x34, + + COMPASS1: 0x35, + COMPASS2: 0x36, + COMPASS3: 0x37, + COMPASS4: 0x38, + COMPASS5: 0x39, + COMPASS6: 0x3A, + COMPASS7: 0x3B, + COMPASS8: 0x3C, + COMPASS9: 0x3D, + + STONE_BEAK1: 0x3E, + STONE_BEAK2: 0x3F, + STONE_BEAK3: 0x40, + STONE_BEAK4: 0x41, + STONE_BEAK5: 0x42, + STONE_BEAK6: 0x43, + STONE_BEAK7: 0x44, + STONE_BEAK8: 0x45, + STONE_BEAK9: 0x46, + + NIGHTMARE_KEY1: 0x47, + NIGHTMARE_KEY2: 0x48, + NIGHTMARE_KEY3: 0x49, + NIGHTMARE_KEY4: 0x4A, + NIGHTMARE_KEY5: 0x4B, + NIGHTMARE_KEY6: 0x4C, + NIGHTMARE_KEY7: 0x4D, + NIGHTMARE_KEY8: 0x4E, + NIGHTMARE_KEY9: 0x4F, + + TOADSTOOL: 0x50, + + HEART_PIECE: 0x80, + BOWWOW: 0x81, + ARROWS_10: 0x82, + SINGLE_ARROW: 0x83, + + MAX_POWDER_UPGRADE: 0x84, + MAX_BOMBS_UPGRADE: 0x85, + MAX_ARROWS_UPGRADE: 0x86, + + RED_TUNIC: 0x87, + BLUE_TUNIC: 0x88, + HEART_CONTAINER: 0x89, + BAD_HEART_CONTAINER: 0x8A, + + SONG1: 0x8B, + SONG2: 0x8C, + SONG3: 0x8D, + + INSTRUMENT1: 0x8E, + INSTRUMENT2: 0x8F, + INSTRUMENT3: 0x90, + INSTRUMENT4: 0x91, + INSTRUMENT5: 0x92, + INSTRUMENT6: 0x93, + INSTRUMENT7: 0x94, + INSTRUMENT8: 0x95, + + TRADING_ITEM_YOSHI_DOLL: 0x97, + TRADING_ITEM_RIBBON: 0x98, + TRADING_ITEM_DOG_FOOD: 0x99, + TRADING_ITEM_BANANAS: 0x9A, + TRADING_ITEM_STICK: 0x9B, + TRADING_ITEM_HONEYCOMB: 0x9C, + TRADING_ITEM_PINEAPPLE: 0x9D, + TRADING_ITEM_HIBISCUS: 0x9E, + TRADING_ITEM_LETTER: 0x9F, + TRADING_ITEM_BROOM: 0xA0, + TRADING_ITEM_FISHING_HOOK: 0xA1, + TRADING_ITEM_NECKLACE: 0xA2, + TRADING_ITEM_SCALE: 0xA3, + TRADING_ITEM_MAGNIFYING_GLASS: 0xA4, +} diff --git a/worlds/ladx/LADXR/locations/droppedKey.py b/worlds/ladx/LADXR/locations/droppedKey.py new file mode 100644 index 000000000000..baa093bb3892 --- /dev/null +++ b/worlds/ladx/LADXR/locations/droppedKey.py @@ -0,0 +1,57 @@ +from .itemInfo import ItemInfo +from .constants import * +patched_already = {} + +class DroppedKey(ItemInfo): + default_item = None + + def __init__(self, room=None): + extra = None + if room == 0x169: # Room in D4 where the key drops down the hole into the sidescroller + extra = 0x017C + elif room == 0x166: # D4 boss, also place the item in out real boss room. + extra = 0x01ff + elif room == 0x223: # D7 boss, also place the item in our real boss room. + extra = 0x02E8 + elif room == 0x092: # Marins song + extra = 0x00DC + elif room == 0x0CE: + extra = 0x01F8 + super().__init__(room, extra) + def patch(self, rom, option, *, multiworld=None): + if (option.startswith(MAP) and option != MAP) or (option.startswith(COMPASS) and option != COMPASS) or option.startswith(STONE_BEAK) or (option.startswith(NIGHTMARE_KEY) and option != NIGHTMARE_KEY )or (option.startswith(KEY) and option != KEY): + if option[-1] == 'P': + print(option) + if self._location.dungeon == int(option[-1]) and multiworld is None and self.room not in {0x166, 0x223}: + option = option[:-1] + rom.banks[0x3E][self.room + 0x3800] = CHEST_ITEMS[option] + #assert room not in patched_already, f"{self} {patched_already[room]}" + #patched_already[room] = self + + + if self.extra: + assert(not self.default_item) + rom.banks[0x3E][self.extra + 0x3800] = CHEST_ITEMS[option] + + if multiworld is not None: + rom.banks[0x3E][0x3300 + self.room] = multiworld + + if self.extra: + rom.banks[0x3E][0x3300 + self.extra] = multiworld + + def read(self, rom): + assert self._location is not None, hex(self.room) + value = rom.banks[0x3E][self.room + 0x3800] + for k, v in CHEST_ITEMS.items(): + if v == value: + if k in [MAP, COMPASS, STONE_BEAK, NIGHTMARE_KEY, KEY]: + assert self._location.dungeon is not None, "Dungeon item outside of dungeon? %r" % (self) + return "%s%d" % (k, self._location.dungeon) + return k + raise ValueError("Could not find chest contents in ROM (0x%02x)" % (value)) + + def __repr__(self): + if self._location and self._location.dungeon: + return "%s:%03x:%d" % (self.__class__.__name__, self.room, self._location.dungeon) + else: + return "%s:%03x" % (self.__class__.__name__, self.room) diff --git a/worlds/ladx/LADXR/locations/faceKey.py b/worlds/ladx/LADXR/locations/faceKey.py new file mode 100644 index 000000000000..1585535f6354 --- /dev/null +++ b/worlds/ladx/LADXR/locations/faceKey.py @@ -0,0 +1,6 @@ +from .droppedKey import DroppedKey + + +class FaceKey(DroppedKey): + def __init__(self): + super().__init__(0x27F) diff --git a/worlds/ladx/LADXR/locations/fishingMinigame.py b/worlds/ladx/LADXR/locations/fishingMinigame.py new file mode 100644 index 000000000000..0caaf7fddbef --- /dev/null +++ b/worlds/ladx/LADXR/locations/fishingMinigame.py @@ -0,0 +1,13 @@ +from .droppedKey import DroppedKey +from .constants import * + + +class FishingMinigame(DroppedKey): + def __init__(self): + super().__init__(0x2B1) + + def configure(self, options): + if options.heartpiece: + super().configure(options) + else: + self.OPTIONS = [HEART_PIECE] diff --git a/worlds/ladx/LADXR/locations/goldLeaf.py b/worlds/ladx/LADXR/locations/goldLeaf.py new file mode 100644 index 000000000000..10ebab42cc38 --- /dev/null +++ b/worlds/ladx/LADXR/locations/goldLeaf.py @@ -0,0 +1,12 @@ +from .droppedKey import DroppedKey + + +class GoldLeaf(DroppedKey): + pass # Golden leaves are patched to work exactly like dropped keys + + +class SlimeKey(DroppedKey): + # The slime key is secretly a golden leaf and just normally uses logic depended on the room number. + # As we patched it to act like a dropped key, we can just be a dropped key in the right room + def __init__(self): + super().__init__(0x0C6) diff --git a/worlds/ladx/LADXR/locations/heartContainer.py b/worlds/ladx/LADXR/locations/heartContainer.py new file mode 100644 index 000000000000..e1a8d77569af --- /dev/null +++ b/worlds/ladx/LADXR/locations/heartContainer.py @@ -0,0 +1,15 @@ +from .droppedKey import DroppedKey +from .items import * + + +class HeartContainer(DroppedKey): + # Due to the patches a heartContainers acts like a dropped key. + def configure(self, options): + if options.heartcontainers or options.hpmode == 'extralow': + super().configure(options) + elif options.hpmode == 'inverted': + self.OPTIONS = [BAD_HEART_CONTAINER] + elif options.hpmode == 'low': + self.OPTIONS = [HEART_PIECE] + else: + self.OPTIONS = [HEART_CONTAINER] diff --git a/worlds/ladx/LADXR/locations/heartPiece.py b/worlds/ladx/LADXR/locations/heartPiece.py new file mode 100644 index 000000000000..b6dddf0b7b8a --- /dev/null +++ b/worlds/ladx/LADXR/locations/heartPiece.py @@ -0,0 +1,12 @@ +from .droppedKey import DroppedKey +from .items import * + + +class HeartPiece(DroppedKey): + # Due to the patches a heartPiece acts like a dropped key. + + def configure(self, options): + if options.heartpiece: + super().configure(options) + else: + self.OPTIONS = [HEART_PIECE] diff --git a/worlds/ladx/LADXR/locations/hookshot.py b/worlds/ladx/LADXR/locations/hookshot.py new file mode 100644 index 000000000000..1e2d24584ab6 --- /dev/null +++ b/worlds/ladx/LADXR/locations/hookshot.py @@ -0,0 +1,18 @@ +from .droppedKey import DroppedKey + + +""" +The hookshot is dropped by the master stalfos. +The master stalfos drops a "key" with, and modifies a bunch of properties: + + ld a, $30 ; $7EE1: $3E $30 + call SpawnNewEntity_trampoline ; $7EE3: $CD $86 $3B + +And then the dropped key handles the rest with room number specific code. +As we patched the dropped key, this requires no extra handling. +""" + + +class HookshotDrop(DroppedKey): + def __init__(self): + super().__init__(0x180) diff --git a/worlds/ladx/LADXR/locations/instrument.py b/worlds/ladx/LADXR/locations/instrument.py new file mode 100644 index 000000000000..2eadb31c6584 --- /dev/null +++ b/worlds/ladx/LADXR/locations/instrument.py @@ -0,0 +1,9 @@ +from .droppedKey import DroppedKey + + +class Instrument(DroppedKey): + # Thanks to patches, an instrument is just a dropped key as far as the randomizer is concerned. + + def configure(self, options): + if not options.instruments and not options.goal == "seashells": + self.OPTIONS = ["INSTRUMENT%d" % (self._location.dungeon)] diff --git a/worlds/ladx/LADXR/locations/itemInfo.py b/worlds/ladx/LADXR/locations/itemInfo.py new file mode 100644 index 000000000000..dcd4205f4cd9 --- /dev/null +++ b/worlds/ladx/LADXR/locations/itemInfo.py @@ -0,0 +1,43 @@ +import typing +from ..checkMetadata import checkMetadataTable +from .constants import * + + +class ItemInfo: + MULTIWORLD = True + + def __init__(self, room=None, extra=None): + self.item = None + self._location = None + self.room = room + self.extra = extra + self.metadata = checkMetadataTable.get(self.nameId, checkMetadataTable["None"]) + self.forced_item = None + self.custom_item_name = None + + self.event = None + @property + def location(self): + return self._location + + def setLocation(self, location): + self._location = location + + def getOptions(self): + return self.OPTIONS + + def configure(self, options): + pass + + def read(self, rom): + raise NotImplementedError() + + def patch(self, rom, option, *, multiworld=None): + raise NotImplementedError() + + def __repr__(self): + return self.__class__.__name__ + + @property + def nameId(self): + return "0x%03X" % self.room if self.room is not None else "None" diff --git a/worlds/ladx/LADXR/locations/items.py b/worlds/ladx/LADXR/locations/items.py new file mode 100644 index 000000000000..50186ef2a34c --- /dev/null +++ b/worlds/ladx/LADXR/locations/items.py @@ -0,0 +1,127 @@ +POWER_BRACELET = "POWER_BRACELET" +SHIELD = "SHIELD" +BOW = "BOW" +HOOKSHOT = "HOOKSHOT" +MAGIC_ROD = "MAGIC_ROD" +PEGASUS_BOOTS = "PEGASUS_BOOTS" +OCARINA = "OCARINA" +FEATHER = "FEATHER" +SHOVEL = "SHOVEL" +MAGIC_POWDER = "MAGIC_POWDER" +BOMB = "BOMB" +SWORD = "SWORD" +FLIPPERS = "FLIPPERS" +MAGNIFYING_LENS = "MAGNIFYING_LENS" +MEDICINE = "MEDICINE" +TAIL_KEY = "TAIL_KEY" +ANGLER_KEY = "ANGLER_KEY" +FACE_KEY = "FACE_KEY" +BIRD_KEY = "BIRD_KEY" +SLIME_KEY = "SLIME_KEY" +GOLD_LEAF = "GOLD_LEAF" +RUPEES_50 = "RUPEES_50" +RUPEES_20 = "RUPEES_20" +RUPEES_100 = "RUPEES_100" +RUPEES_200 = "RUPEES_200" +RUPEES_500 = "RUPEES_500" +SEASHELL = "SEASHELL" +MESSAGE = "MESSAGE" +GEL = "GEL" +BOOMERANG = "BOOMERANG" +HEART_PIECE = "HEART_PIECE" +BOWWOW = "BOWWOW" +ARROWS_10 = "ARROWS_10" +SINGLE_ARROW = "SINGLE_ARROW" +ROOSTER = "ROOSTER" + +MAX_POWDER_UPGRADE = "MAX_POWDER_UPGRADE" +MAX_BOMBS_UPGRADE = "MAX_BOMBS_UPGRADE" +MAX_ARROWS_UPGRADE = "MAX_ARROWS_UPGRADE" + +RED_TUNIC = "RED_TUNIC" +BLUE_TUNIC = "BLUE_TUNIC" +HEART_CONTAINER = "HEART_CONTAINER" +BAD_HEART_CONTAINER = "BAD_HEART_CONTAINER" + +TOADSTOOL = "TOADSTOOL" + +KEY = "KEY" +KEY1 = "KEY1" +KEY2 = "KEY2" +KEY3 = "KEY3" +KEY4 = "KEY4" +KEY5 = "KEY5" +KEY6 = "KEY6" +KEY7 = "KEY7" +KEY8 = "KEY8" +KEY9 = "KEY9" + +NIGHTMARE_KEY = "NIGHTMARE_KEY" +NIGHTMARE_KEY1 = "NIGHTMARE_KEY1" +NIGHTMARE_KEY2 = "NIGHTMARE_KEY2" +NIGHTMARE_KEY3 = "NIGHTMARE_KEY3" +NIGHTMARE_KEY4 = "NIGHTMARE_KEY4" +NIGHTMARE_KEY5 = "NIGHTMARE_KEY5" +NIGHTMARE_KEY6 = "NIGHTMARE_KEY6" +NIGHTMARE_KEY7 = "NIGHTMARE_KEY7" +NIGHTMARE_KEY8 = "NIGHTMARE_KEY8" +NIGHTMARE_KEY9 = "NIGHTMARE_KEY9" + +MAP = "MAP" +MAP1 = "MAP1" +MAP2 = "MAP2" +MAP3 = "MAP3" +MAP4 = "MAP4" +MAP5 = "MAP5" +MAP6 = "MAP6" +MAP7 = "MAP7" +MAP8 = "MAP8" +MAP9 = "MAP9" +COMPASS = "COMPASS" +COMPASS1 = "COMPASS1" +COMPASS2 = "COMPASS2" +COMPASS3 = "COMPASS3" +COMPASS4 = "COMPASS4" +COMPASS5 = "COMPASS5" +COMPASS6 = "COMPASS6" +COMPASS7 = "COMPASS7" +COMPASS8 = "COMPASS8" +COMPASS9 = "COMPASS9" +STONE_BEAK = "STONE_BEAK" +STONE_BEAK1 = "STONE_BEAK1" +STONE_BEAK2 = "STONE_BEAK2" +STONE_BEAK3 = "STONE_BEAK3" +STONE_BEAK4 = "STONE_BEAK4" +STONE_BEAK5 = "STONE_BEAK5" +STONE_BEAK6 = "STONE_BEAK6" +STONE_BEAK7 = "STONE_BEAK7" +STONE_BEAK8 = "STONE_BEAK8" +STONE_BEAK9 = "STONE_BEAK9" + +SONG1 = "SONG1" +SONG2 = "SONG2" +SONG3 = "SONG3" + +INSTRUMENT1 = "INSTRUMENT1" +INSTRUMENT2 = "INSTRUMENT2" +INSTRUMENT3 = "INSTRUMENT3" +INSTRUMENT4 = "INSTRUMENT4" +INSTRUMENT5 = "INSTRUMENT5" +INSTRUMENT6 = "INSTRUMENT6" +INSTRUMENT7 = "INSTRUMENT7" +INSTRUMENT8 = "INSTRUMENT8" + +TRADING_ITEM_YOSHI_DOLL = "TRADING_ITEM_YOSHI_DOLL" +TRADING_ITEM_RIBBON = "TRADING_ITEM_RIBBON" +TRADING_ITEM_DOG_FOOD = "TRADING_ITEM_DOG_FOOD" +TRADING_ITEM_BANANAS = "TRADING_ITEM_BANANAS" +TRADING_ITEM_STICK = "TRADING_ITEM_STICK" +TRADING_ITEM_HONEYCOMB = "TRADING_ITEM_HONEYCOMB" +TRADING_ITEM_PINEAPPLE = "TRADING_ITEM_PINEAPPLE" +TRADING_ITEM_HIBISCUS = "TRADING_ITEM_HIBISCUS" +TRADING_ITEM_LETTER = "TRADING_ITEM_LETTER" +TRADING_ITEM_BROOM = "TRADING_ITEM_BROOM" +TRADING_ITEM_FISHING_HOOK = "TRADING_ITEM_FISHING_HOOK" +TRADING_ITEM_NECKLACE = "TRADING_ITEM_NECKLACE" +TRADING_ITEM_SCALE = "TRADING_ITEM_SCALE" +TRADING_ITEM_MAGNIFYING_GLASS = "TRADING_ITEM_MAGNIFYING_GLASS" diff --git a/worlds/ladx/LADXR/locations/keyLocation.py b/worlds/ladx/LADXR/locations/keyLocation.py new file mode 100644 index 000000000000..675bfe0f9061 --- /dev/null +++ b/worlds/ladx/LADXR/locations/keyLocation.py @@ -0,0 +1,18 @@ +from .itemInfo import ItemInfo + + +class KeyLocation(ItemInfo): + OPTIONS = [] + + def __init__(self, key): + super().__init__() + self.event = key + + def patch(self, rom, option, *, multiworld=None): + pass + + def read(self, rom): + return self.OPTIONS[0] + + def configure(self, options): + pass diff --git a/worlds/ladx/LADXR/locations/madBatter.py b/worlds/ladx/LADXR/locations/madBatter.py new file mode 100644 index 000000000000..33ce971422ac --- /dev/null +++ b/worlds/ladx/LADXR/locations/madBatter.py @@ -0,0 +1,23 @@ +from .itemInfo import ItemInfo +from .constants import * + + +class MadBatter(ItemInfo): + def configure(self, options): + return + + def patch(self, rom, option, *, multiworld=None): + rom.banks[0x18][0x0F90 + (self.room & 0x0F)] = CHEST_ITEMS[option] + if multiworld is not None: + rom.banks[0x3E][0x3300 + self.room] = multiworld + + def read(self, rom): + assert self._location is not None, hex(self.room) + value = rom.banks[0x18][0x0F90 + (self.room & 0x0F)] + for k, v in CHEST_ITEMS.items(): + if v == value: + return k + raise ValueError("Could not find mad batter contents in ROM (0x%02x)" % (value)) + + def __repr__(self): + return "%s:%03x" % (self.__class__.__name__, self.room) diff --git a/worlds/ladx/LADXR/locations/owlStatue.py b/worlds/ladx/LADXR/locations/owlStatue.py new file mode 100644 index 000000000000..1e62519319db --- /dev/null +++ b/worlds/ladx/LADXR/locations/owlStatue.py @@ -0,0 +1,41 @@ +from .itemInfo import ItemInfo +from .constants import * + + +class OwlStatue(ItemInfo): + def configure(self, options): + if options.owlstatues == "both": + return + if options.owlstatues == "dungeon" and self.room >= 0x100: + return + if options.owlstatues == "overworld" and self.room < 0x100: + return + raise RuntimeError("Tried to configure an owlstatue that was not enabled") + self.OPTIONS = [RUPEES_20] + + def patch(self, rom, option, *, multiworld=None): + if option.startswith(MAP) or option.startswith(COMPASS) or option.startswith(STONE_BEAK) or option.startswith(NIGHTMARE_KEY) or option.startswith(KEY): + if self._location.dungeon == int(option[-1]) and multiworld is not None: + option = option[:-1] + rom.banks[0x3E][self.room + 0x3B16] = CHEST_ITEMS[option] + + def read(self, rom): + assert self._location is not None, hex(self.room) + value = rom.banks[0x3E][self.room + 0x3B16] + for k, v in CHEST_ITEMS.items(): + if v == value: + if k in [MAP, COMPASS, STONE_BEAK, NIGHTMARE_KEY, KEY]: + assert self._location.dungeon is not None, "Dungeon item outside of dungeon? %r" % (self) + return "%s%d" % (k, self._location.dungeon) + return k + raise ValueError("Could not find owl statue contents in ROM (0x%02x)" % (value)) + + def __repr__(self): + if self._location and self._location.dungeon: + return "%s:%03x:%d" % (self.__class__.__name__, self.room, self._location.dungeon) + else: + return "%s:%03x" % (self.__class__.__name__, self.room) + + @property + def nameId(self): + return "0x%03X-Owl" % self.room diff --git a/worlds/ladx/LADXR/locations/seashell.py b/worlds/ladx/LADXR/locations/seashell.py new file mode 100644 index 000000000000..5b30cf7e2444 --- /dev/null +++ b/worlds/ladx/LADXR/locations/seashell.py @@ -0,0 +1,14 @@ +from .droppedKey import DroppedKey +from .items import * + + +class Seashell(DroppedKey): + # Thanks to patches, a seashell is just a dropped key as far as the randomizer is concerned. + + def configure(self, options): + if not options.seashells: + self.OPTIONS = [SEASHELL] + + +class SeashellMansion(DroppedKey): + pass diff --git a/worlds/ladx/LADXR/locations/shop.py b/worlds/ladx/LADXR/locations/shop.py new file mode 100644 index 000000000000..e3e05d941a0b --- /dev/null +++ b/worlds/ladx/LADXR/locations/shop.py @@ -0,0 +1,42 @@ +from .itemInfo import ItemInfo +from .constants import * +from ..utils import formatText +from ..assembler import ASM + + +class ShopItem(ItemInfo): + def __init__(self, index): + self.__index = index + # pass in the alternate index for shop 2 + # The "real" room is at 0x2A1, but we store the second item data as if link were in 0x2A7 + room = 0x2A1 + if index == 1: + room = 0x2A7 + super().__init__(room) + + def patch(self, rom, option, *, multiworld=None): + mw_text = "" + if multiworld: + mw_text = f" for player {rom.player_names[multiworld - 1]}" + + if self.__index == 0: + # Old index, maybe not needed any more + rom.patch(0x04, 0x37C5, "08", "%02X" % (CHEST_ITEMS[option])) + rom.texts[0x030] = formatText(f"Deluxe {{%s}} 200 {{RUPEES}}{mw_text}!" % (option), ask="Buy No Way") + rom.banks[0x3E][0x3800 + 0x2A1] = CHEST_ITEMS[option] + if multiworld: + rom.banks[0x3E][0x3300 + 0x2A1] = multiworld + elif self.__index == 1: + rom.patch(0x04, 0x37C6, "02", "%02X" % (CHEST_ITEMS[option])) + rom.texts[0x02C] = formatText(f"{{%s}} Only 980 {{RUPEES}}{mw_text}!" % (option), ask="Buy No Way") + + rom.banks[0x3E][0x3800 + 0x2A7] = CHEST_ITEMS[option] + if multiworld: + rom.banks[0x3E][0x3300 + 0x2A7] = multiworld + + def read(self, rom): + value = rom.banks[0x04][0x37C5 + self.__index] + for k, v in CHEST_ITEMS.items(): + if v == value: + return k + raise ValueError("Could not find shop item contents in ROM (0x%02x)" % (value)) \ No newline at end of file diff --git a/worlds/ladx/LADXR/locations/song.py b/worlds/ladx/LADXR/locations/song.py new file mode 100644 index 000000000000..25937dcef42c --- /dev/null +++ b/worlds/ladx/LADXR/locations/song.py @@ -0,0 +1,5 @@ +from .droppedKey import DroppedKey + + +class Song(DroppedKey): + pass diff --git a/worlds/ladx/LADXR/locations/startItem.py b/worlds/ladx/LADXR/locations/startItem.py new file mode 100644 index 000000000000..95dd6ba54abd --- /dev/null +++ b/worlds/ladx/LADXR/locations/startItem.py @@ -0,0 +1,38 @@ +from .itemInfo import ItemInfo +from .constants import * +from .droppedKey import DroppedKey +from ..assembler import ASM +from ..utils import formatText +from ..roomEditor import RoomEditor + + +class StartItem(DroppedKey): + # We need to give something here that we can use to progress. + # FEATHER + OPTIONS = [SWORD, SHIELD, POWER_BRACELET, OCARINA, BOOMERANG, MAGIC_ROD, TAIL_KEY, SHOVEL, HOOKSHOT, PEGASUS_BOOTS, MAGIC_POWDER, BOMB] + + MULTIWORLD = False + + def __init__(self): + super().__init__(0x2A3) + self.give_bowwow = False + + def configure(self, options): + if options.bowwow != 'normal': + # When we have bowwow mode, we pretend to be a sword for logic reasons + self.OPTIONS = [SWORD] + self.give_bowwow = True + if options.randomstartlocation and options.entranceshuffle != 'none': + self.OPTIONS.append(FLIPPERS) + + def patch(self, rom, option, *, multiworld=None): + assert multiworld is None + + if self.give_bowwow: + option = BOWWOW + rom.texts[0xC8] = formatText("Got BowWow!") + + if option != SHIELD: + rom.patch(5, 0x0CDA, ASM("ld a, $22"), ASM("ld a, $00")) # do not change links sprite into the one with a shield + + super().patch(rom, option) diff --git a/worlds/ladx/LADXR/locations/toadstool.py b/worlds/ladx/LADXR/locations/toadstool.py new file mode 100644 index 000000000000..381b023ec82c --- /dev/null +++ b/worlds/ladx/LADXR/locations/toadstool.py @@ -0,0 +1,18 @@ +from .droppedKey import DroppedKey +from .items import * + + +class Toadstool(DroppedKey): + def __init__(self): + super().__init__(0x050) + + def configure(self, options): + if not options.witch: + self.OPTIONS = [TOADSTOOL] + else: + super().configure(options) + + def read(self, rom): + if len(self.OPTIONS) == 1: + return TOADSTOOL + return super().read(rom) diff --git a/worlds/ladx/LADXR/locations/tradeSequence.py b/worlds/ladx/LADXR/locations/tradeSequence.py new file mode 100644 index 000000000000..1587bb5e13b1 --- /dev/null +++ b/worlds/ladx/LADXR/locations/tradeSequence.py @@ -0,0 +1,55 @@ +from .itemInfo import ItemInfo +from .constants import * +from .droppedKey import DroppedKey + +TradeRequirements = { + TRADING_ITEM_YOSHI_DOLL: None, + TRADING_ITEM_RIBBON: TRADING_ITEM_YOSHI_DOLL, + TRADING_ITEM_DOG_FOOD: TRADING_ITEM_RIBBON, + TRADING_ITEM_BANANAS: TRADING_ITEM_DOG_FOOD, + TRADING_ITEM_STICK: TRADING_ITEM_BANANAS, + TRADING_ITEM_HONEYCOMB: TRADING_ITEM_STICK, + TRADING_ITEM_PINEAPPLE: TRADING_ITEM_HONEYCOMB, + TRADING_ITEM_HIBISCUS: TRADING_ITEM_PINEAPPLE, + TRADING_ITEM_LETTER: TRADING_ITEM_HIBISCUS, + TRADING_ITEM_BROOM: TRADING_ITEM_LETTER, + TRADING_ITEM_FISHING_HOOK: TRADING_ITEM_BROOM, + TRADING_ITEM_NECKLACE: TRADING_ITEM_FISHING_HOOK, + TRADING_ITEM_SCALE: TRADING_ITEM_NECKLACE, + TRADING_ITEM_MAGNIFYING_GLASS: TRADING_ITEM_SCALE, +} +class TradeSequenceItem(DroppedKey): + def __init__(self, room, default_item): + self.unadjusted_room = room + if room == 0x2B2: + # Offset room for trade items to avoid collisions + roomLo = room & 0xFF + roomHi = room ^ roomLo + roomLo = (roomLo + 2) & 0xFF + room = roomHi | roomLo + super().__init__(room) + self.default_item = default_item + + def configure(self, options): + if not options.tradequest: + self.OPTIONS = [self.default_item] + super().configure(options) + + #def patch(self, rom, option, *, multiworld=None): + # rom.banks[0x3E][self.room + 0x3B16] = CHEST_ITEMS[option] + + def read(self, rom): + assert(False) + assert self._location is not None, hex(self.room) + value = rom.banks[0x3E][self.room + 0x3B16] + for k, v in CHEST_ITEMS.items(): + if v == value: + return k + raise ValueError("Could not find owl statue contents in ROM (0x%02x)" % (value)) + + def __repr__(self): + return "%s:%03x" % (self.__class__.__name__, self.room) + + @property + def nameId(self): + return "0x%03X-Trade" % self.unadjusted_room diff --git a/worlds/ladx/LADXR/locations/tunicFairy.py b/worlds/ladx/LADXR/locations/tunicFairy.py new file mode 100644 index 000000000000..84fc9ca7356f --- /dev/null +++ b/worlds/ladx/LADXR/locations/tunicFairy.py @@ -0,0 +1,27 @@ +from .itemInfo import ItemInfo +from .constants import * + + +class TunicFairy(ItemInfo): + + def __init__(self, index): + self.index = index + super().__init__(0x301) + + def patch(self, rom, option, *, multiworld=None): + # Old index, maybe not needed anymore + rom.banks[0x36][0x11BF + self.index] = CHEST_ITEMS[option] + rom.banks[0x3e][0x3800 + 0x301 + self.index*3] = CHEST_ITEMS[option] + if multiworld: + rom.banks[0x3e][0x3300 + 0x301 + self.index*3] = multiworld + + def read(self, rom): + value = rom.banks[0x36][0x11BF + self.index] + for k, v in CHEST_ITEMS.items(): + if v == value: + return k + raise ValueError("Could not find tunic fairy contents in ROM (0x%02x)" % (value)) + + @property + def nameId(self): + return "0x%03X-%s" % (self.room, self.index) diff --git a/worlds/ladx/LADXR/locations/witch.py b/worlds/ladx/LADXR/locations/witch.py new file mode 100644 index 000000000000..6435a30e32e1 --- /dev/null +++ b/worlds/ladx/LADXR/locations/witch.py @@ -0,0 +1,31 @@ +from .constants import * +from .itemInfo import ItemInfo + + +class Witch(ItemInfo): + def __init__(self): + super().__init__(0x2A2) + + def configure(self, options): + if not options.witch: + self.OPTIONS = [MAGIC_POWDER] + + def patch(self, rom, option, *, multiworld=None): + if multiworld or option != MAGIC_POWDER: + + rom.banks[0x3E][self.room + 0x3800] = CHEST_ITEMS[option] + if multiworld is not None: + rom.banks[0x3E][0x3300 + self.room] = multiworld + else: + rom.banks[0x3E][0x3300 + self.room] = 0 + + #rom.patch(0x05, 0x08D5, "09", "%02x" % (CHEST_ITEMS[option])) + + def read(self, rom): + if rom.banks[0x05][0x08EF] != 0x00: + return MAGIC_POWDER + value = rom.banks[0x05][0x08D5] + for k, v in CHEST_ITEMS.items(): + if v == value: + return k + raise ValueError("Could not find witch contents in ROM (0x%02x)" % (value)) diff --git a/worlds/ladx/LADXR/logic/__init__.py b/worlds/ladx/LADXR/logic/__init__.py new file mode 100644 index 000000000000..11a0acfd01f8 --- /dev/null +++ b/worlds/ladx/LADXR/logic/__init__.py @@ -0,0 +1,284 @@ +from . import overworld +from . import dungeon1 +from . import dungeon2 +from . import dungeon3 +from . import dungeon4 +from . import dungeon5 +from . import dungeon6 +from . import dungeon7 +from . import dungeon8 +from . import dungeonColor +from .requirements import AND, OR, COUNT, COUNTS, FOUND, RequirementsSettings +from .location import Location +from ..locations.items import * +from ..locations.keyLocation import KeyLocation +from ..worldSetup import WorldSetup +from .. import itempool +from .. import mapgen + + +class Logic: + def __init__(self, configuration_options, *, world_setup): + self.world_setup = world_setup + r = RequirementsSettings(configuration_options) + + if configuration_options.overworld == "dungeondive": + world = overworld.DungeonDiveOverworld(configuration_options, r) + elif configuration_options.overworld == "random": + world = mapgen.LogicGenerator(configuration_options, world_setup, r, world_setup.map) + else: + world = overworld.World(configuration_options, world_setup, r) + + if configuration_options.overworld == "nodungeons": + world.updateIndoorLocation("d1", dungeon1.NoDungeon1(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d2", dungeon2.NoDungeon2(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d3", dungeon3.NoDungeon3(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d4", dungeon4.NoDungeon4(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d5", dungeon5.NoDungeon5(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d6", dungeon6.NoDungeon6(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d7", dungeon7.NoDungeon7(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d8", dungeon8.NoDungeon8(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d0", dungeonColor.NoDungeonColor(configuration_options, world_setup, r).entrance) + elif configuration_options.overworld != "random": + world.updateIndoorLocation("d1", dungeon1.Dungeon1(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d2", dungeon2.Dungeon2(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d3", dungeon3.Dungeon3(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d4", dungeon4.Dungeon4(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d5", dungeon5.Dungeon5(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d6", dungeon6.Dungeon6(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d7", dungeon7.Dungeon7(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d8", dungeon8.Dungeon8(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d0", dungeonColor.DungeonColor(configuration_options, world_setup, r).entrance) + + if configuration_options.overworld != "random": + for k in world.overworld_entrance.keys(): + assert k in world_setup.entrance_mapping, k + for k in world_setup.entrance_mapping.keys(): + assert k in world.overworld_entrance, k + + for entrance, indoor in world_setup.entrance_mapping.items(): + exterior = world.overworld_entrance[entrance] + if world.indoor_location[indoor] is not None: + exterior.location.connect(world.indoor_location[indoor], exterior.requirement) + if exterior.enterIsSet(): + exterior.location.connect(world.indoor_location[indoor], exterior.one_way_enter_requirement, one_way=True) + if exterior.exitIsSet(): + world.indoor_location[indoor].connect(exterior.location, exterior.one_way_exit_requirement, one_way=True) + + egg_trigger = AND(OCARINA, SONG1) + if configuration_options.logic == 'glitched' or configuration_options.logic == 'hell': + egg_trigger = OR(AND(OCARINA, SONG1), BOMB) + + if world_setup.goal == "seashells": + world.nightmare.connect(world.egg, COUNT(SEASHELL, 20)) + elif world_setup.goal in ("raft", "bingo", "bingo-full"): + world.nightmare.connect(world.egg, egg_trigger) + else: + goal = int(world_setup.goal) + if goal < 0: + world.nightmare.connect(world.egg, None) + elif goal == 0: + world.nightmare.connect(world.egg, egg_trigger) + elif goal == 8: + world.nightmare.connect(world.egg, AND(egg_trigger, INSTRUMENT1, INSTRUMENT2, INSTRUMENT3, INSTRUMENT4, INSTRUMENT5, INSTRUMENT6, INSTRUMENT7, INSTRUMENT8)) + else: + world.nightmare.connect(world.egg, AND(egg_trigger, COUNTS([INSTRUMENT1, INSTRUMENT2, INSTRUMENT3, INSTRUMENT4, INSTRUMENT5, INSTRUMENT6, INSTRUMENT7, INSTRUMENT8], goal))) + + # if configuration_options.dungeon_items == 'keysy': + # for n in range(9): + # for count in range(9): + # world.start.add(KeyLocation("KEY%d" % (n + 1))) + # world.start.add(KeyLocation("NIGHTMARE_KEY%d" % (n + 1))) + + self.world = world + self.start = world.start + self.windfish = world.windfish + self.location_list = [] + self.iteminfo_list = [] + + self.__location_set = set() + self.__recursiveFindAll(self.start) + del self.__location_set + + for ii in self.iteminfo_list: + ii.configure(configuration_options) + + def dumpFlatRequirements(self): + def __rec(location, req): + if hasattr(location, "flat_requirements"): + new_flat_requirements = requirements.mergeFlat(location.flat_requirements, requirements.flatten(req)) + if new_flat_requirements == location.flat_requirements: + return + location.flat_requirements = new_flat_requirements + else: + location.flat_requirements = requirements.flatten(req) + for connection, requirement in location.simple_connections: + __rec(connection, AND(req, requirement) if req else requirement) + for connection, requirement in location.gated_connections: + __rec(connection, AND(req, requirement) if req else requirement) + __rec(self.start, None) + for ii in self.iteminfo_list: + print(ii) + for fr in ii._location.flat_requirements: + print(" " + ", ".join(sorted(map(str, fr)))) + + def __recursiveFindAll(self, location): + if location in self.__location_set: + return + self.location_list.append(location) + self.__location_set.add(location) + for ii in location.items: + self.iteminfo_list.append(ii) + for connection, requirement in location.simple_connections: + self.__recursiveFindAll(connection) + for connection, requirement in location.gated_connections: + self.__recursiveFindAll(connection) + + +class MultiworldLogic: + def __init__(self, settings, rnd=None, *, world_setups=None): + assert rnd or world_setups + self.worlds = [] + self.start = Location() + self.location_list = [self.start] + self.iteminfo_list = [] + + for n in range(settings.multiworld): + options = settings.multiworld_settings[n] + world = None + if world_setups: + world = Logic(options, world_setup=world_setups[n]) + else: + for cnt in range(1000): # Try the world setup in case entrance randomization generates unsolvable logic + world_setup = WorldSetup() + world_setup.randomize(options, rnd) + world = Logic(options, world_setup=world_setup) + if options.entranceshuffle not in ("advanced", "expert", "insanity") or len(world.iteminfo_list) == sum(itempool.ItemPool(options, rnd).toDict().values()): + break + + for ii in world.iteminfo_list: + ii.world = n + + req_done_set = set() + for loc in world.location_list: + loc.simple_connections = [(target, addWorldIdToRequirements(req_done_set, n, req)) for target, req in loc.simple_connections] + loc.gated_connections = [(target, addWorldIdToRequirements(req_done_set, n, req)) for target, req in loc.gated_connections] + loc.items = [MultiworldItemInfoWrapper(n, options, ii) for ii in loc.items] + self.iteminfo_list += loc.items + + self.worlds.append(world) + self.start.simple_connections += world.start.simple_connections + self.start.gated_connections += world.start.gated_connections + self.start.items += world.start.items + world.start.items.clear() + self.location_list += world.location_list + + self.entranceMapping = None + + +class MultiworldMetadataWrapper: + def __init__(self, world, metadata): + self.world = world + self.metadata = metadata + + @property + def name(self): + return self.metadata.name + + @property + def area(self): + return "P%d %s" % (self.world + 1, self.metadata.area) + + +class MultiworldItemInfoWrapper: + def __init__(self, world, configuration_options, target): + self.world = world + self.world_count = configuration_options.multiworld + self.target = target + self.dungeon_items = configuration_options.dungeon_items + self.MULTIWORLD_OPTIONS = None + self.item = None + + @property + def nameId(self): + return self.target.nameId + + @property + def forced_item(self): + if self.target.forced_item is None: + return None + if "_W" in self.target.forced_item: + return self.target.forced_item + return "%s_W%d" % (self.target.forced_item, self.world) + + @property + def room(self): + return self.target.room + + @property + def metadata(self): + return MultiworldMetadataWrapper(self.world, self.target.metadata) + + @property + def MULTIWORLD(self): + return self.target.MULTIWORLD + + def read(self, rom): + world = rom.banks[0x3E][0x3300 + self.target.room] if self.target.MULTIWORLD else self.world + return "%s_W%d" % (self.target.read(rom), world) + + def getOptions(self): + if self.MULTIWORLD_OPTIONS is None: + options = self.target.getOptions() + if self.target.MULTIWORLD and len(options) > 1: + self.MULTIWORLD_OPTIONS = [] + for n in range(self.world_count): + self.MULTIWORLD_OPTIONS += ["%s_W%d" % (t, n) for t in options if n == self.world or self.canMultiworld(t)] + else: + self.MULTIWORLD_OPTIONS = ["%s_W%d" % (t, self.world) for t in options] + return self.MULTIWORLD_OPTIONS + + def patch(self, rom, option): + idx = option.rfind("_W") + world = int(option[idx+2:]) + option = option[:idx] + if not self.target.MULTIWORLD: + assert self.world == world + self.target.patch(rom, option) + else: + self.target.patch(rom, option, multiworld=world) + + # Return true if the item is allowed to be placed in any world, or false if it is + # world specific for this check. + def canMultiworld(self, option): + if self.dungeon_items in {'', 'smallkeys'}: + if option.startswith("MAP"): + return False + if option.startswith("COMPASS"): + return False + if option.startswith("STONE_BEAK"): + return False + if self.dungeon_items in {'', 'localkeys'}: + if option.startswith("KEY"): + return False + if self.dungeon_items in {'', 'localkeys', 'localnightmarekey', 'smallkeys'}: + if option.startswith("NIGHTMARE_KEY"): + return False + return True + + @property + def location(self): + return self.target.location + + def __repr__(self): + return "W%d:%s" % (self.world, repr(self.target)) + + +def addWorldIdToRequirements(req_done_set, world, req): + if req is None: + return None + if isinstance(req, str): + return "%s_W%d" % (req, world) + if req in req_done_set: + return req + return req.copyWithModifiedItemNames(lambda item: "%s_W%d" % (item, world)) diff --git a/worlds/ladx/LADXR/logic/dungeon1.py b/worlds/ladx/LADXR/logic/dungeon1.py new file mode 100644 index 000000000000..82321a1c0d65 --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon1.py @@ -0,0 +1,46 @@ +from .requirements import * +from .location import Location +from ..locations.all import * + + +class Dungeon1: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=1) + entrance.add(DungeonChest(0x113), DungeonChest(0x115), DungeonChest(0x10E)) + Location(dungeon=1).add(DroppedKey(0x116)).connect(entrance, OR(BOMB, r.push_hardhat)) # hardhat beetles (can kill with bomb) + Location(dungeon=1).add(DungeonChest(0x10D)).connect(entrance, OR(r.attack_hookshot_powder, SHIELD)) # moldorm spawn chest + stalfos_keese_room = Location(dungeon=1).add(DungeonChest(0x114)).connect(entrance, r.attack_hookshot) # 2 stalfos 2 keese room + Location(dungeon=1).add(DungeonChest(0x10C)).connect(entrance, BOMB) # hidden seashell room + dungeon1_upper_left = Location(dungeon=1).connect(entrance, AND(KEY1, FOUND(KEY1, 3))) + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=1).add(OwlStatue(0x103), OwlStatue(0x104)).connect(dungeon1_upper_left, STONE_BEAK1) + feather_chest = Location(dungeon=1).add(DungeonChest(0x11D)).connect(dungeon1_upper_left, SHIELD) # feather location, behind spike enemies. can shield bump into pit (only shield works) + boss_key = Location(dungeon=1).add(DungeonChest(0x108)).connect(entrance, AND(FEATHER, KEY1, FOUND(KEY1, 3))) # boss key + dungeon1_right_side = Location(dungeon=1).connect(entrance, AND(KEY1, FOUND(KEY1, 3))) + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=1).add(OwlStatue(0x10A)).connect(dungeon1_right_side, STONE_BEAK1) + Location(dungeon=1).add(DungeonChest(0x10A)).connect(dungeon1_right_side, OR(r.attack_hookshot, SHIELD)) # three of a kind, shield stops the suit from changing + dungeon1_miniboss = Location(dungeon=1).connect(dungeon1_right_side, AND(r.miniboss_requirements[world_setup.miniboss_mapping[0]], FEATHER)) + dungeon1_boss = Location(dungeon=1).connect(dungeon1_miniboss, NIGHTMARE_KEY1) + Location(dungeon=1).add(HeartContainer(0x106), Instrument(0x102)).connect(dungeon1_boss, r.boss_requirements[world_setup.boss_mapping[0]]) + + if options.logic not in ('normal', 'casual'): + stalfos_keese_room.connect(entrance, r.attack_hookshot_powder) # stalfos jump away when you press a button. + + if options.logic == 'glitched' or options.logic == 'hell': + boss_key.connect(entrance, FEATHER) # super jump + dungeon1_miniboss.connect(dungeon1_right_side, r.miniboss_requirements[world_setup.miniboss_mapping[0]]) # damage boost or buffer pause over the pit to cross or mushroom + + if options.logic == 'hell': + feather_chest.connect(dungeon1_upper_left, SWORD) # keep slashing the spiked beetles until they keep moving 1 pixel close towards you and the pit, to get them to fall + boss_key.connect(entrance, FOUND(KEY1,3)) # damage boost off the hardhat to cross the pit + + self.entrance = entrance + + +class NoDungeon1: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=1) + Location(dungeon=1).add(HeartContainer(0x106), Instrument(0x102)).connect(entrance, r.boss_requirements[ + world_setup.boss_mapping[0]]) + self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon2.py b/worlds/ladx/LADXR/logic/dungeon2.py new file mode 100644 index 000000000000..3bb95edbc8bd --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon2.py @@ -0,0 +1,62 @@ +from .requirements import * +from .location import Location +from ..locations.all import * + + +class Dungeon2: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=2) + Location(dungeon=2).add(DungeonChest(0x136)).connect(entrance, POWER_BRACELET) # chest at entrance + dungeon2_l2 = Location(dungeon=2).connect(entrance, AND(KEY2, FOUND(KEY2, 5))) # towards map chest + dungeon2_map_chest = Location(dungeon=2).add(DungeonChest(0x12E)).connect(dungeon2_l2, AND(r.attack_hookshot_powder, OR(FEATHER, HOOKSHOT))) # map chest + dungeon2_r2 = Location(dungeon=2).connect(entrance, r.fire) + Location(dungeon=2).add(DroppedKey(0x132)).connect(dungeon2_r2, r.attack_skeleton) + Location(dungeon=2).add(DungeonChest(0x137)).connect(dungeon2_r2, AND(KEY2, FOUND(KEY2, 5), OR(r.rear_attack, r.rear_attack_range))) # compass chest + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=2).add(OwlStatue(0x133)).connect(dungeon2_r2, STONE_BEAK2) + dungeon2_r3 = Location(dungeon=2).add(DungeonChest(0x138)).connect(dungeon2_r2, r.attack_hookshot) # first chest with key, can hookshot the switch in previous room + dungeon2_r4 = Location(dungeon=2).add(DungeonChest(0x139)).connect(dungeon2_r3, FEATHER) # button spawn chest + if options.logic == "casual": + shyguy_key_drop = Location(dungeon=2).add(DroppedKey(0x134)).connect(dungeon2_r3, AND(FEATHER, OR(r.rear_attack, r.rear_attack_range))) # shyguy drop key + else: + shyguy_key_drop = Location(dungeon=2).add(DroppedKey(0x134)).connect(dungeon2_r3, OR(r.rear_attack, AND(FEATHER, r.rear_attack_range))) # shyguy drop key + dungeon2_r5 = Location(dungeon=2).connect(dungeon2_r4, AND(KEY2, FOUND(KEY2, 3))) # push two blocks together room with owl statue + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=2).add(OwlStatue(0x12F)).connect(dungeon2_r5, STONE_BEAK2) # owl statue is before miniboss + miniboss = Location(dungeon=2).add(DungeonChest(0x126)).add(DungeonChest(0x121)).connect(dungeon2_r5, AND(FEATHER, r.miniboss_requirements[world_setup.miniboss_mapping[1]])) # post hinox + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=2).add(OwlStatue(0x129)).connect(miniboss, STONE_BEAK2) # owl statue after the miniboss + + dungeon2_ghosts_room = Location(dungeon=2).connect(miniboss, AND(KEY2, FOUND(KEY2, 5))) + dungeon2_ghosts_chest = Location(dungeon=2).add(DungeonChest(0x120)).connect(dungeon2_ghosts_room, OR(r.fire, BOW)) # bracelet chest + dungeon2_r6 = Location(dungeon=2).add(DungeonChest(0x122)).connect(miniboss, POWER_BRACELET) + dungeon2_boss_key = Location(dungeon=2).add(DungeonChest(0x127)).connect(dungeon2_r6, AND(r.attack_hookshot_powder, OR(BOW, BOMB, MAGIC_ROD, AND(OCARINA, SONG1), POWER_BRACELET))) + dungeon2_pre_stairs_boss = Location(dungeon=2).connect(dungeon2_r6, AND(POWER_BRACELET, KEY2, FOUND(KEY2, 5))) + dungeon2_post_stairs_boss = Location(dungeon=2).connect(dungeon2_pre_stairs_boss, POWER_BRACELET) + dungeon2_pre_boss = Location(dungeon=2).connect(dungeon2_post_stairs_boss, FEATHER) + # If we can get here, we have everything for the boss. So this is also the goal room. + dungeon2_boss = Location(dungeon=2).add(HeartContainer(0x12B), Instrument(0x12a)).connect(dungeon2_pre_boss, AND(NIGHTMARE_KEY2, r.boss_requirements[world_setup.boss_mapping[1]])) + + if options.logic == 'glitched' or options.logic == 'hell': + dungeon2_ghosts_chest.connect(dungeon2_ghosts_room, SWORD) # use sword to spawn ghosts on other side of the room so they run away (logically irrelevant because of torches at start) + dungeon2_r6.connect(miniboss, FEATHER) # superjump to staircase next to hinox. + + if options.logic == 'hell': + dungeon2_map_chest.connect(dungeon2_l2, AND(r.attack_hookshot_powder, PEGASUS_BOOTS)) # use boots to jump over the pits + dungeon2_r4.connect(dungeon2_r3, OR(PEGASUS_BOOTS, HOOKSHOT)) # can use both pegasus boots bonks or hookshot spam to cross the pit room + dungeon2_r4.connect(shyguy_key_drop, r.rear_attack_range, one_way=True) # adjust for alternate requirements for dungeon2_r4 + miniboss.connect(dungeon2_r5, AND(PEGASUS_BOOTS, r.miniboss_requirements[world_setup.miniboss_mapping[1]])) # use boots to dash over the spikes in the 2d section + dungeon2_pre_stairs_boss.connect(dungeon2_r6, AND(HOOKSHOT, OR(BOW, BOMB, MAGIC_ROD, AND(OCARINA, SONG1)), FOUND(KEY2, 5))) # hookshot clip through the pot using both pol's voice + dungeon2_post_stairs_boss.connect(dungeon2_pre_stairs_boss, OR(BOMB, AND(PEGASUS_BOOTS, FEATHER))) # use a bomb to lower the last platform, or boots + feather to cross over top (only relevant in hell logic) + dungeon2_pre_boss.connect(dungeon2_post_stairs_boss, AND(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk off bottom wall + hookshot spam across the two 1 tile pits vertically + + self.entrance = entrance + + +class NoDungeon2: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=2) + Location(dungeon=2).add(DungeonChest(0x136)).connect(entrance, POWER_BRACELET) # chest at entrance + Location(dungeon=2).add(HeartContainer(0x12B), Instrument(0x12a)).connect(entrance, r.boss_requirements[ + world_setup.boss_mapping[1]]) + self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon3.py b/worlds/ladx/LADXR/logic/dungeon3.py new file mode 100644 index 000000000000..e65c7da0bafc --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon3.py @@ -0,0 +1,89 @@ +from .requirements import * +from .location import Location +from ..locations.all import * + + +class Dungeon3: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=3) + dungeon3_reverse_eye = Location(dungeon=3).add(DungeonChest(0x153)).connect(entrance, PEGASUS_BOOTS) # Right side reverse eye + area2 = Location(dungeon=3).connect(entrance, POWER_BRACELET) + Location(dungeon=3).add(DungeonChest(0x151)).connect(area2, r.attack_hookshot_powder) # First chest with key + area2.add(DungeonChest(0x14F)) # Second chest with slime + area3 = Location(dungeon=3).connect(area2, OR(r.attack_hookshot_powder, PEGASUS_BOOTS)) # need to kill slimes to continue or pass through left path + dungeon3_zol_stalfos = Location(dungeon=3).add(DungeonChest(0x14E)).connect(area3, AND(PEGASUS_BOOTS, r.attack_skeleton)) # 3th chest requires killing the slime behind the crystal pillars + + # now we can go 4 directions, + area_up = Location(dungeon=3).connect(area3, AND(KEY3, FOUND(KEY3, 8))) + dungeon3_north_key_drop = Location(dungeon=3).add(DroppedKey(0x154)).connect(area_up, r.attack_skeleton) # north key drop + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=3).add(OwlStatue(0x154)).connect(area_up, STONE_BEAK3) + dungeon3_raised_blocks_north = Location(dungeon=3).add(DungeonChest(0x14C)) # chest locked behind raised blocks near staircase + dungeon3_raised_blocks_east = Location(dungeon=3).add(DungeonChest(0x150)) # chest locked behind raised blocks next to slime chest + area_up.connect(dungeon3_raised_blocks_north, r.attack_hookshot, one_way=True) # hit switch to reach north chest + area_up.connect(dungeon3_raised_blocks_east, r.attack_hookshot, one_way=True) # hit switch to reach east chest + + area_left = Location(dungeon=3).connect(area3, AND(KEY3, FOUND(KEY3, 8))) + area_left_key_drop = Location(dungeon=3).add(DroppedKey(0x155)).connect(area_left, r.attack_hookshot) # west key drop (no longer requires feather to get across hole), can use boomerang to knock owls into pit + + area_down = Location(dungeon=3).connect(area3, AND(KEY3, FOUND(KEY3, 8))) + dungeon3_south_key_drop = Location(dungeon=3).add(DroppedKey(0x158)).connect(area_down, r.attack_hookshot) # south keydrop, can use boomerang to knock owls into pit + + area_right = Location(dungeon=3).connect(area3, AND(KEY3, FOUND(KEY3, 4))) # We enter the top part of the map here. + Location(dungeon=3).add(DroppedKey(0x14D)).connect(area_right, r.attack_hookshot_powder) # key after the stairs. + + dungeon3_nightmare_key_chest = Location(dungeon=3).add(DungeonChest(0x147)).connect(area_right, AND(BOMB, FEATHER, PEGASUS_BOOTS)) # nightmare key chest + dungeon3_post_dodongo_chest = Location(dungeon=3).add(DungeonChest(0x146)).connect(area_right, AND(r.attack_hookshot_powder, r.miniboss_requirements[world_setup.miniboss_mapping[2]])) # boots after the miniboss + compass_chest = Location(dungeon=3).add(DungeonChest(0x142)).connect(area_right, OR(SWORD, BOMB, AND(SHIELD, r.attack_hookshot_powder))) # bomb only activates with sword, bomb or shield + dungeon3_3_bombite_room = Location(dungeon=3).add(DroppedKey(0x141)).connect(compass_chest, BOMB) # 3 bombite room + Location(dungeon=3).add(DroppedKey(0x148)).connect(area_right, r.attack_no_boomerang) # 2 zol 2 owl drop key + Location(dungeon=3).add(DungeonChest(0x144)).connect(area_right, r.attack_skeleton) # map chest + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=3).add(OwlStatue(0x140), OwlStatue(0x147)).connect(area_right, STONE_BEAK3) + + towards_boss1 = Location(dungeon=3).connect(area_right, AND(KEY3, FOUND(KEY3, 5))) + towards_boss2 = Location(dungeon=3).connect(towards_boss1, AND(KEY3, FOUND(KEY3, 6))) + towards_boss3 = Location(dungeon=3).connect(towards_boss2, AND(KEY3, FOUND(KEY3, 7))) + towards_boss4 = Location(dungeon=3).connect(towards_boss3, AND(KEY3, FOUND(KEY3, 8))) + + # Just the whole area before the boss, requirements for the boss itself and the rooms before it are the same. + pre_boss = Location(dungeon=3).connect(towards_boss4, AND(r.attack_no_boomerang, FEATHER, PEGASUS_BOOTS)) + pre_boss.add(DroppedKey(0x15B)) + + boss = Location(dungeon=3).add(HeartContainer(0x15A), Instrument(0x159)).connect(pre_boss, AND(NIGHTMARE_KEY3, r.boss_requirements[world_setup.boss_mapping[2]])) + + if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': + dungeon3_3_bombite_room.connect(area_right, BOOMERANG) # 3 bombite room from the left side, grab item with boomerang + dungeon3_reverse_eye.connect(entrance, HOOKSHOT) # hookshot the chest to get to the right side + dungeon3_north_key_drop.connect(area_up, POWER_BRACELET) # use pots to kill the enemies + dungeon3_south_key_drop.connect(area_down, POWER_BRACELET) # use pots to kill enemies + + if options.logic == 'glitched' or options.logic == 'hell': + area2.connect(dungeon3_raised_blocks_east, AND(r.attack_hookshot_powder, FEATHER), one_way=True) # use superjump to get over the bottom left block + area3.connect(dungeon3_raised_blocks_north, AND(OR(PEGASUS_BOOTS, HOOKSHOT), FEATHER), one_way=True) # use shagjump (unclipped superjump next to movable block) from north wall to get on the blocks. Instead of boots can also get to that area with a hookshot clip past the movable block + area3.connect(dungeon3_zol_stalfos, HOOKSHOT, one_way=True) # hookshot clip through the northern push block next to raised blocks chest to get to the zol + dungeon3_nightmare_key_chest.connect(area_right, AND(FEATHER, BOMB)) # superjump to right side 3 gap via top wall and jump the 2 gap + dungeon3_post_dodongo_chest.connect(area_right, AND(FEATHER, FOUND(KEY3, 6))) # superjump from keyblock path. use 2 keys to open enough blocks TODO: nag messages to skip a key + + if options.logic == 'hell': + area2.connect(dungeon3_raised_blocks_east, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD)), one_way=True) # use boots superhop to get over the bottom left block + area3.connect(dungeon3_raised_blocks_north, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD)), one_way=True) # use boots superhop off top wall or left wall to get on raised blocks + area_up.connect(dungeon3_zol_stalfos, AND(FEATHER, OR(BOW, MAGIC_ROD, SWORD)), one_way=True) # use superjump near top blocks chest to get to zol without boots, keep wall clip on right wall to get a clip on left wall or use obstacles + area_left_key_drop.connect(area_left, SHIELD) # knock everything into the pit including the teleporting owls + dungeon3_south_key_drop.connect(area_down, SHIELD) # knock everything into the pit including the teleporting owls + dungeon3_nightmare_key_chest.connect(area_right, AND(FEATHER, SHIELD)) # superjump into jumping stalfos and shield bump to right ledge + dungeon3_nightmare_key_chest.connect(area_right, AND(BOMB, PEGASUS_BOOTS, HOOKSHOT)) # boots bonk across the pits with pit buffering and hookshot to the chest + compass_chest.connect(dungeon3_3_bombite_room, OR(BOW, MAGIC_ROD, AND(OR(FEATHER, PEGASUS_BOOTS), OR(SWORD, MAGIC_POWDER))), one_way=True) # 3 bombite room from the left side, use a bombite to blow open the wall without bombs + pre_boss.connect(towards_boss4, AND(r.attack_no_boomerang, FEATHER, POWER_BRACELET)) # use bracelet super bounce glitch to pass through first part underground section + pre_boss.connect(towards_boss4, AND(r.attack_no_boomerang, PEGASUS_BOOTS, "MEDICINE2")) # use medicine invulnerability to pass through the 2d section with a boots bonk to reach the staircase + + self.entrance = entrance + + +class NoDungeon3: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=3) + Location(dungeon=3).add(HeartContainer(0x15A), Instrument(0x159)).connect(entrance, AND(POWER_BRACELET, r.boss_requirements[ + world_setup.boss_mapping[2]])) + + self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon4.py b/worlds/ladx/LADXR/logic/dungeon4.py new file mode 100644 index 000000000000..7d71c89f0c86 --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon4.py @@ -0,0 +1,81 @@ +from .requirements import * +from .location import Location +from ..locations.all import * + + +class Dungeon4: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=4) + entrance.add(DungeonChest(0x179)) # stone slab chest + entrance.add(DungeonChest(0x16A)) # map chest + right_of_entrance = Location(dungeon=4).add(DungeonChest(0x178)).connect(entrance, AND(SHIELD, r.attack_hookshot_powder)) # 1 zol 2 spike beetles 1 spark chest + Location(dungeon=4).add(DungeonChest(0x17B)).connect(right_of_entrance, AND(SHIELD, SWORD)) # room with key chest + rightside_crossroads = Location(dungeon=4).connect(entrance, AND(FEATHER, PEGASUS_BOOTS)) # 2 key chests on the right. + pushable_block_chest = Location(dungeon=4).add(DungeonChest(0x171)).connect(rightside_crossroads, BOMB) # lower chest + puddle_crack_block_chest = Location(dungeon=4).add(DungeonChest(0x165)).connect(rightside_crossroads, OR(BOMB, FLIPPERS)) # top right chest + + double_locked_room = Location(dungeon=4).connect(right_of_entrance, AND(KEY4, FOUND(KEY4, 5)), one_way=True) + right_of_entrance.connect(double_locked_room, KEY4, one_way=True) + after_double_lock = Location(dungeon=4).connect(double_locked_room, AND(KEY4, FOUND(KEY4, 4), OR(FEATHER, FLIPPERS)), one_way=True) + double_locked_room.connect(after_double_lock, AND(KEY4, FOUND(KEY4, 2), OR(FEATHER, FLIPPERS)), one_way=True) + + dungeon4_puddle_before_crossroads = Location(dungeon=4).add(DungeonChest(0x175)).connect(after_double_lock, FLIPPERS) + north_crossroads = Location(dungeon=4).connect(after_double_lock, AND(FEATHER, PEGASUS_BOOTS)) + before_miniboss = Location(dungeon=4).connect(north_crossroads, AND(KEY4, FOUND(KEY4, 3))) + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=4).add(OwlStatue(0x16F)).connect(before_miniboss, STONE_BEAK4) + sidescroller_key = Location(dungeon=4).add(DroppedKey(0x169)).connect(before_miniboss, AND(r.attack_hookshot_powder, FLIPPERS)) # key that drops in the hole and needs swim to get + center_puddle_chest = Location(dungeon=4).add(DungeonChest(0x16E)).connect(before_miniboss, FLIPPERS) # chest with 50 rupees + left_water_area = Location(dungeon=4).connect(before_miniboss, OR(FEATHER, FLIPPERS)) # area left with zol chest and 5 symbol puzzle (water area) + left_water_area.add(DungeonChest(0x16D)) # gel chest + left_water_area.add(DungeonChest(0x168)) # key chest near the puzzle + miniboss = Location(dungeon=4).connect(before_miniboss, AND(KEY4, FOUND(KEY4, 5), r.miniboss_requirements[world_setup.miniboss_mapping[3]])) + terrace_zols_chest = Location(dungeon=4).connect(before_miniboss, FLIPPERS) # flippers to move around miniboss through 5 tile room + miniboss = Location(dungeon=4).connect(terrace_zols_chest, POWER_BRACELET, one_way=True) # reach flippers chest through the miniboss room + terrace_zols_chest.add(DungeonChest(0x160)) # flippers chest + terrace_zols_chest.connect(left_water_area, r.attack_hookshot_powder, one_way=True) # can move from flippers chest south to push the block to left area + + to_the_nightmare_key = Location(dungeon=4).connect(left_water_area, AND(FEATHER, OR(FLIPPERS, PEGASUS_BOOTS))) # 5 symbol puzzle (does not need flippers with boots + feather) + to_the_nightmare_key.add(DungeonChest(0x176)) + + before_boss = Location(dungeon=4).connect(before_miniboss, AND(r.attack_hookshot, FLIPPERS, KEY4, FOUND(KEY4, 5))) + boss = Location(dungeon=4).add(HeartContainer(0x166), Instrument(0x162)).connect(before_boss, AND(NIGHTMARE_KEY4, r.boss_requirements[world_setup.boss_mapping[3]])) + + if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': + sidescroller_key.connect(before_miniboss, AND(FEATHER, BOOMERANG)) # grab the key jumping over the water and boomerang downwards + sidescroller_key.connect(before_miniboss, AND(POWER_BRACELET, FLIPPERS)) # kill the zols with the pots in the room to spawn the key + rightside_crossroads.connect(entrance, FEATHER) # jump across the corners + puddle_crack_block_chest.connect(rightside_crossroads, FEATHER) # jump around the bombable block + north_crossroads.connect(entrance, FEATHER) # jump across the corners + after_double_lock.connect(entrance, FEATHER) # jump across the corners + dungeon4_puddle_before_crossroads.connect(after_double_lock, FEATHER) # With a tight jump feather is enough to cross the puddle without flippers + center_puddle_chest.connect(before_miniboss, FEATHER) # With a tight jump feather is enough to cross the puddle without flippers + miniboss = Location(dungeon=4).connect(terrace_zols_chest, None, one_way=True) # reach flippers chest through the miniboss room without pulling the lever + to_the_nightmare_key.connect(left_water_area, FEATHER) # With a tight jump feather is enough to reach the top left switch without flippers, or use flippers for puzzle and boots to get through 2d section + before_boss.connect(left_water_area, FEATHER) # jump to the bottom right corner of boss door room + + if options.logic == 'glitched' or options.logic == 'hell': + pushable_block_chest.connect(rightside_crossroads, FLIPPERS) # sideways block push to skip bombs + sidescroller_key.connect(before_miniboss, AND(FEATHER, OR(r.attack_hookshot_powder, POWER_BRACELET))) # superjump into the hole to grab the key while falling into the water + miniboss.connect(before_miniboss, FEATHER) # use jesus jump to transition over the water left of miniboss + + if options.logic == 'hell': + rightside_crossroads.connect(entrance, AND(PEGASUS_BOOTS, HOOKSHOT)) # pit buffer into the wall of the first pit, then boots bonk across the center, hookshot to get to the rightmost pit to a second villa buffer on the rightmost pit + pushable_block_chest.connect(rightside_crossroads, OR(PEGASUS_BOOTS, FEATHER)) # use feather to water clip into the top right corner of the bombable block, and sideways block push to gain access. Can boots bonk of top right wall, then water buffer to top of chest and boots bonk to water buffer next to chest + after_double_lock.connect(double_locked_room, AND(FOUND(KEY4, 4), PEGASUS_BOOTS), one_way=True) # use boots bonks to cross the water gaps + north_crossroads.connect(entrance, AND(PEGASUS_BOOTS, HOOKSHOT)) # pit buffer into wall of the first pit, then boots bonk towards the top and hookshot spam to get across (easier with Piece of Power) + after_double_lock.connect(entrance, PEGASUS_BOOTS) # boots bonk + pit buffer to the bottom + dungeon4_puddle_before_crossroads.connect(after_double_lock, AND(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk across the water bottom wall to the bottom left corner, then hookshot up + to_the_nightmare_key.connect(left_water_area, AND(FLIPPERS, PEGASUS_BOOTS)) # Use flippers for puzzle and boots bonk to get through 2d section + before_boss.connect(left_water_area, PEGASUS_BOOTS) # boots bonk across bottom wall then boots bonk to the platform before boss door + + self.entrance = entrance + + +class NoDungeon4: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=4) + Location(dungeon=4).add(HeartContainer(0x166), Instrument(0x162)).connect(entrance, r.boss_requirements[ + world_setup.boss_mapping[3]]) + + self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon5.py b/worlds/ladx/LADXR/logic/dungeon5.py new file mode 100644 index 000000000000..b8e013066c50 --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon5.py @@ -0,0 +1,89 @@ +from .requirements import * +from .location import Location +from ..locations.all import * + + +class Dungeon5: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=5) + start_hookshot_chest = Location(dungeon=5).add(DungeonChest(0x1A0)).connect(entrance, HOOKSHOT) + compass = Location(dungeon=5).add(DungeonChest(0x19E)).connect(entrance, r.attack_hookshot_powder) + fourth_stalfos_area = Location(dungeon=5).add(DroppedKey(0x181)).connect(compass, AND(SWORD, FEATHER)) # crystal rocks can only be broken by sword + + area2 = Location(dungeon=5).connect(entrance, KEY5) + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=5).add(OwlStatue(0x19A)).connect(area2, STONE_BEAK5) + Location(dungeon=5).add(DungeonChest(0x19B)).connect(area2, r.attack_hookshot_powder) # map chest + blade_trap_chest = Location(dungeon=5).add(DungeonChest(0x197)).connect(area2, HOOKSHOT) # key chest on the left + post_gohma = Location(dungeon=5).connect(area2, AND(HOOKSHOT, r.miniboss_requirements[world_setup.miniboss_mapping[4]], KEY5, FOUND(KEY5,2))) # staircase after gohma + staircase_before_boss = Location(dungeon=5).connect(post_gohma, AND(HOOKSHOT, FEATHER)) # bottom right section pits room before boss door. Path via gohma + after_keyblock_boss = Location(dungeon=5).connect(staircase_before_boss, AND(KEY5, FOUND(KEY5, 3))) # top right section pits room before boss door + after_stalfos = Location(dungeon=5).add(DungeonChest(0x196)).connect(area2, AND(SWORD, BOMB)) # Need to defeat master stalfos once for this empty chest; l2 sword beams kill but obscure + if options.owlstatues == "both" or options.owlstatues == "dungeon": + butterfly_owl = Location(dungeon=5).add(OwlStatue(0x18A)).connect(after_stalfos, AND(FEATHER, STONE_BEAK5)) + else: + butterfly_owl = None + after_stalfos.connect(staircase_before_boss, AND(FEATHER, r.attack_hookshot_powder), one_way=True) # pathway from stalfos to staircase: past butterfly room and push the block + north_of_crossroads = Location(dungeon=5).connect(after_stalfos, FEATHER) + first_bridge_chest = Location(dungeon=5).add(DungeonChest(0x18E)).connect(north_of_crossroads, OR(HOOKSHOT, AND(FEATHER, PEGASUS_BOOTS))) # south of bridge + north_bridge_chest = Location(dungeon=5).add(DungeonChest(0x188)).connect(north_of_crossroads, HOOKSHOT) # north bridge chest 50 rupees + east_bridge_chest = Location(dungeon=5).add(DungeonChest(0x18F)).connect(north_of_crossroads, HOOKSHOT) # east bridge chest small key + third_arena = Location(dungeon=5).connect(north_of_crossroads, AND(SWORD, BOMB)) # can beat 3rd m.stalfos + stone_tablet = Location(dungeon=5).add(DungeonChest(0x183)).connect(north_of_crossroads, AND(POWER_BRACELET, r.attack_skeleton)) # stone tablet + boss_key = Location(dungeon=5).add(DungeonChest(0x186)).connect(after_stalfos, AND(FLIPPERS, HOOKSHOT)) # nightmare key + before_boss = Location(dungeon=5).connect(after_keyblock_boss, HOOKSHOT) + boss = Location(dungeon=5).add(HeartContainer(0x185), Instrument(0x182)).connect(before_boss, AND(r.boss_requirements[world_setup.boss_mapping[4]], NIGHTMARE_KEY5)) + + # When we can reach the stone tablet chest, we can also reach the final location of master stalfos + m_stalfos_drop = Location(dungeon=5).add(HookshotDrop()).connect(third_arena, AND(FEATHER, SWORD, BOMB)) # can reach fourth arena from entrance with feather and sword + + if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': + blade_trap_chest.connect(area2, AND(FEATHER, r.attack_hookshot_powder)) # jump past the blade traps + boss_key.connect(after_stalfos, AND(FLIPPERS, FEATHER, PEGASUS_BOOTS)) # boots jump across + after_stalfos.connect(after_keyblock_boss, AND(FEATHER, r.attack_hookshot_powder)) # circumvent stalfos by going past gohma and backwards from boss door + if butterfly_owl: + butterfly_owl.connect(after_stalfos, AND(PEGASUS_BOOTS, STONE_BEAK5)) # boots charge + bonk to cross 2d bridge + after_stalfos.connect(staircase_before_boss, AND(PEGASUS_BOOTS, r.attack_hookshot_powder), one_way=True) # pathway from stalfos to staircase: boots charge + bonk to cross bridge, past butterfly room and push the block + staircase_before_boss.connect(post_gohma, AND(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk in 2d section to skip feather + north_of_crossroads.connect(after_stalfos, HOOKSHOT) # hookshot to the right block to cross pits + first_bridge_chest.connect(north_of_crossroads, FEATHER) # tight jump from bottom wall clipped to make it over the pits + after_keyblock_boss.connect(after_stalfos, AND(FEATHER, r.attack_hookshot_powder)) # jump from bottom left to top right, skipping the keyblock + before_boss.connect(after_stalfos, AND(FEATHER, PEGASUS_BOOTS, r.attack_hookshot_powder)) # cross pits room from bottom left to top left with boots jump + + if options.logic == 'glitched' or options.logic == 'hell': + start_hookshot_chest.connect(entrance, FEATHER) # 1 pit buffer to clip bottom wall and jump across the pits + post_gohma.connect(area2, HOOKSHOT) # glitch through the blocks/pots with hookshot. Zoomerang can be used but has no logical implications because of 2d section requiring hookshot + north_bridge_chest.connect(north_of_crossroads, FEATHER) # 1 pit buffer to clip bottom wall and jump across the pits + east_bridge_chest.connect(first_bridge_chest, FEATHER) # 1 pit buffer to clip bottom wall and jump across the pits + #after_stalfos.connect(staircase_before_boss, AND(FEATHER, OR(SWORD, BOW, MAGIC_ROD))) # use the keyblock to get a wall clip in right wall to perform a superjump over the pushable block TODO: nagmessages + after_stalfos.connect(staircase_before_boss, AND(PEGASUS_BOOTS, FEATHER, OR(SWORD, BOW, MAGIC_ROD))) # charge a boots dash in bottom right corner to the right, jump before hitting the wall and use weapon to the left side before hitting the wall + + if options.logic == 'hell': + start_hookshot_chest.connect(entrance, PEGASUS_BOOTS) # use pit buffer to clip into the bottom wall and boots bonk off the wall again + fourth_stalfos_area.connect(compass, AND(PEGASUS_BOOTS, SWORD)) # do an incredibly hard boots bonk setup to get across the hanging platforms in the 2d section + blade_trap_chest.connect(area2, AND(PEGASUS_BOOTS, r.attack_hookshot_powder)) # boots bonk + pit buffer past the blade traps + post_gohma.connect(area2, AND(PEGASUS_BOOTS, FEATHER, POWER_BRACELET, r.attack_hookshot_powder)) # use boots jump in room with 2 zols + flying arrows to pit buffer above pot, then jump across. Sideways block push + pick up pots to reach post_gohma + staircase_before_boss.connect(post_gohma, AND(PEGASUS_BOOTS, FEATHER)) # to pass 2d section, tight jump on left screen: hug left wall on little platform, then dash right off platform and jump while in midair to bonk against right wall + after_stalfos.connect(staircase_before_boss, AND(FEATHER, SWORD)) # unclipped superjump in bottom right corner of staircase before boss room, jumping left over the pushable block. reverse is push block + after_stalfos.connect(area2, SWORD) # knock master stalfos down 255 times (about 23 minutes) + north_bridge_chest.connect(north_of_crossroads, PEGASUS_BOOTS) # boots bonk across the pits with pit buffering + first_bridge_chest.connect(north_of_crossroads, PEGASUS_BOOTS) # get to first chest via the north chest with pit buffering + east_bridge_chest.connect(first_bridge_chest, PEGASUS_BOOTS) # boots bonk across the pits with pit buffering + third_arena.connect(north_of_crossroads, SWORD) # can beat 3rd m.stalfos with 255 sword spins + m_stalfos_drop.connect(third_arena, AND(FEATHER, SWORD)) # beat master stalfos by knocking it down 255 times x 4 (takes about 1.5h total) + m_stalfos_drop.connect(third_arena, AND(PEGASUS_BOOTS, SWORD)) # can reach fourth arena from entrance with pegasus boots and sword + boss_key.connect(after_stalfos, FLIPPERS) # pit buffer across + if butterfly_owl: + after_keyblock_boss.connect(butterfly_owl, STONE_BEAK5, one_way=True) # pit buffer from top right to bottom in right pits room + before_boss.connect(after_stalfos, AND(FEATHER, SWORD)) # cross pits room from bottom left to top left by unclipped superjump on bottom wall on top of side wall, then jump across + + self.entrance = entrance + + +class NoDungeon5: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=5) + Location(dungeon=5).add(HeartContainer(0x185), Instrument(0x182)).connect(entrance, r.boss_requirements[ + world_setup.boss_mapping[4]]) + + self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon6.py b/worlds/ladx/LADXR/logic/dungeon6.py new file mode 100644 index 000000000000..7e51349e3a5e --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon6.py @@ -0,0 +1,65 @@ +from .requirements import * +from .location import Location +from ..locations.all import * + + +class Dungeon6: + def __init__(self, options, world_setup, r, *, raft_game_chest=True): + entrance = Location(dungeon=6) + Location(dungeon=6).add(DungeonChest(0x1CF)).connect(entrance, OR(BOMB, BOW, MAGIC_ROD, COUNT(POWER_BRACELET, 2))) # 50 rupees + Location(dungeon=6).add(DungeonChest(0x1C9)).connect(entrance, COUNT(POWER_BRACELET, 2)) # 100 rupees start + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=6).add(OwlStatue(0x1BB)).connect(entrance, STONE_BEAK6) + + # Power bracelet chest + bracelet_chest = Location(dungeon=6).add(DungeonChest(0x1CE)).connect(entrance, AND(BOMB, FEATHER)) + + # left side + Location(dungeon=6).add(DungeonChest(0x1C0)).connect(entrance, AND(POWER_BRACELET, OR(BOMB, BOW, MAGIC_ROD))) # 3 wizrobes raised blocks dont need to hit the switch + left_side = Location(dungeon=6).add(DungeonChest(0x1B9)).add(DungeonChest(0x1B3)).connect(entrance, AND(POWER_BRACELET, OR(BOMB, BOOMERANG))) + Location(dungeon=6).add(DroppedKey(0x1B4)).connect(left_side, OR(BOMB, BOW, MAGIC_ROD)) # 2 wizrobe drop key + top_left = Location(dungeon=6).add(DungeonChest(0x1B0)).connect(left_side, COUNT(POWER_BRACELET, 2)) # top left chest horseheads + if raft_game_chest: + Location().add(Chest(0x06C)).connect(top_left, POWER_BRACELET) # seashell chest in raft game + + # right side + to_miniboss = Location(dungeon=6).connect(entrance, KEY6) + miniboss = Location(dungeon=6).connect(to_miniboss, AND(BOMB, r.miniboss_requirements[world_setup.miniboss_mapping[5]])) + lower_right_side = Location(dungeon=6).add(DungeonChest(0x1BE)).connect(entrance, AND(OR(BOMB, BOW, MAGIC_ROD), COUNT(POWER_BRACELET, 2))) # waterway key + medicine_chest = Location(dungeon=6).add(DungeonChest(0x1D1)).connect(lower_right_side, FEATHER) # ledge chest medicine + if options.owlstatues == "both" or options.owlstatues == "dungeon": + lower_right_owl = Location(dungeon=6).add(OwlStatue(0x1D7)).connect(lower_right_side, AND(POWER_BRACELET, STONE_BEAK6)) + + center_1 = Location(dungeon=6).add(DroppedKey(0x1C3)).connect(miniboss, AND(COUNT(POWER_BRACELET, 2), FEATHER)) # tile room key drop + center_2_and_upper_right_side = Location(dungeon=6).add(DungeonChest(0x1B1)).connect(center_1, KEY6) # top right chest horseheads + boss_key = Location(dungeon=6).add(DungeonChest(0x1B6)).connect(center_2_and_upper_right_side, AND(KEY6, HOOKSHOT)) + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=6).add(OwlStatue(0x1B6)).connect(boss_key, STONE_BEAK6) + + boss = Location(dungeon=6).add(HeartContainer(0x1BC), Instrument(0x1b5)).connect(center_1, AND(NIGHTMARE_KEY6, r.boss_requirements[world_setup.boss_mapping[5]])) + + if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': + bracelet_chest.connect(entrance, BOMB) # get through 2d section by "fake" jumping to the ladders + center_1.connect(miniboss, AND(COUNT(POWER_BRACELET, 2), PEGASUS_BOOTS)) # use a boots dash to get over the platforms + + if options.logic == 'glitched' or options.logic == 'hell': + entrance.connect(left_side, AND(POWER_BRACELET, FEATHER), one_way=True) # path from entrance to left_side: use superjumps to pass raised blocks + lower_right_side.connect(center_2_and_upper_right_side, AND(FEATHER, OR(SWORD, BOW, MAGIC_ROD)), one_way=True) # path from lower_right_side to center_2: superjump from waterway towards dodongos. superjump next to corner block, so weapons added + center_2_and_upper_right_side.connect(center_1, AND(POWER_BRACELET, FEATHER), one_way=True) # going backwards from dodongos, use a shaq jump to pass by keyblock at tile room + boss_key.connect(lower_right_side, FEATHER) # superjump from waterway to the left. POWER_BRACELET is implied from lower_right_side + + if options.logic == 'hell': + entrance.connect(left_side, AND(POWER_BRACELET, PEGASUS_BOOTS, OR(BOW, MAGIC_ROD)), one_way=True) # can boots superhop off the top right corner in 3 wizrobe raised blocks room + medicine_chest.connect(lower_right_side, AND(PEGASUS_BOOTS, OR(MAGIC_ROD, BOW))) # can boots superhop off the top wall with bow or magic rod + center_1.connect(miniboss, AND(COUNT(POWER_BRACELET, 2))) # use a double damage boost from the sparks to get across (first one is free, second one needs to buffer while in midair for spark to get close enough) + lower_right_side.connect(center_2_and_upper_right_side, FEATHER, one_way=True) # path from lower_right_side to center_2: superjump from waterway towards dodongos. superjump next to corner block is super tight to get enough horizontal distance + + self.entrance = entrance + + +class NoDungeon6: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=6) + Location(dungeon=6).add(HeartContainer(0x1BC), Instrument(0x1b5)).connect(entrance, r.boss_requirements[ + world_setup.boss_mapping[5]]) + self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon7.py b/worlds/ladx/LADXR/logic/dungeon7.py new file mode 100644 index 000000000000..594b4d083ca7 --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon7.py @@ -0,0 +1,65 @@ +from .requirements import * +from .location import Location +from ..locations.all import * + + +class Dungeon7: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=7) + first_key = Location(dungeon=7).add(DroppedKey(0x210)).connect(entrance, r.attack_hookshot_powder) + topright_pillar_area = Location(dungeon=7).connect(entrance, KEY7) + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=7).add(OwlStatue(0x216)).connect(topright_pillar_area, STONE_BEAK7) + topright_pillar = Location(dungeon=7).add(DungeonChest(0x212)).connect(topright_pillar_area, POWER_BRACELET) # map chest + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=7).add(OwlStatue(0x204)).connect(topright_pillar_area, STONE_BEAK7) + topright_pillar_area.add(DungeonChest(0x209)) # stone slab chest can be reached by dropping down a hole + three_of_a_kind_north = Location(dungeon=7).add(DungeonChest(0x211)).connect(topright_pillar_area, OR(r.attack_hookshot, AND(FEATHER, SHIELD))) # compass chest; path without feather with hitting switch by falling on the raised blocks. No bracelet because ball does not reset + bottomleftF2_area = Location(dungeon=7).connect(topright_pillar_area, r.attack_hookshot) # area with hinox, be able to hit a switch to reach that area + topleftF1_chest = Location(dungeon=7).add(DungeonChest(0x201)) # top left chest on F1 + bottomleftF2_area.connect(topleftF1_chest, None, one_way = True) # drop down in left most holes of hinox room or tile room + Location(dungeon=7).add(DroppedKey(0x21B)).connect(bottomleftF2_area, r.attack_hookshot) # hinox drop key + # Most of the dungeon can be accessed at this point. + if options.owlstatues == "both" or options.owlstatues == "dungeon": + bottomleft_owl = Location(dungeon=7).add(OwlStatue(0x21C)).connect(bottomleftF2_area, AND(BOMB, STONE_BEAK7)) + nightmare_key = Location(dungeon=7).add(DungeonChest(0x224)).connect(bottomleftF2_area, r.miniboss_requirements[world_setup.miniboss_mapping[6]]) # nightmare key after the miniboss + mirror_shield_chest = Location(dungeon=7).add(DungeonChest(0x21A)).connect(bottomleftF2_area, r.attack_hookshot) # mirror shield chest, need to be able to hit a switch to reach or + bottomleftF2_area.connect(mirror_shield_chest, AND(KEY7, FOUND(KEY7, 3)), one_way = True) # reach mirror shield chest from hinox area by opening keyblock + toprightF1_chest = Location(dungeon=7).add(DungeonChest(0x204)).connect(bottomleftF2_area, r.attack_hookshot) # chest on the F1 right ledge. Added attack_hookshot since switch needs to be hit to get back up + final_pillar_area = Location(dungeon=7).add(DungeonChest(0x21C)).connect(bottomleftF2_area, AND(BOMB, HOOKSHOT)) # chest that needs to spawn to get to the last pillar + final_pillar = Location(dungeon=7).connect(final_pillar_area, POWER_BRACELET) # decouple chest from pillar + + beamos_horseheads_area = Location(dungeon=7).connect(final_pillar, NIGHTMARE_KEY7) # area behind boss door + beamos_horseheads = Location(dungeon=7).add(DungeonChest(0x220)).connect(beamos_horseheads_area, POWER_BRACELET) # 100 rupee chest / medicine chest (DX) behind boss door + pre_boss = Location(dungeon=7).connect(beamos_horseheads_area, HOOKSHOT) # raised plateau before boss staircase + boss = Location(dungeon=7).add(HeartContainer(0x223), Instrument(0x22c)).connect(pre_boss, r.boss_requirements[world_setup.boss_mapping[6]]) + + if options.logic == 'glitched' or options.logic == 'hell': + topright_pillar_area.connect(entrance, AND(FEATHER, SWORD)) # superjump in the center to get on raised blocks, superjump in switch room to right side to walk down. center superjump has to be low so sword added + toprightF1_chest.connect(topright_pillar_area, FEATHER) # superjump from F1 switch room + topleftF2_area = Location(dungeon=7).connect(topright_pillar_area, FEATHER) # superjump in top left pillar room over the blocks from right to left, to reach tile room + topleftF2_area.connect(topleftF1_chest, None, one_way = True) # fall down tile room holes on left side to reach top left chest on ground floor + topleftF1_chest.connect(bottomleftF2_area, AND(PEGASUS_BOOTS, FEATHER), one_way = True) # without hitting the switch, jump on raised blocks at f1 pegs chest (0x209), and boots jump to stairs to reach hinox area + final_pillar_area.connect(bottomleftF2_area, OR(r.attack_hookshot, POWER_BRACELET, AND(FEATHER, SHIELD))) # sideways block push to get to the chest and pillar, kill requirement for 3 of a kind enemies to access chest. Assumes you do not get ball stuck on raised pegs for bracelet path + if options.owlstatues == "both" or options.owlstatues == "dungeon": + bottomleft_owl.connect(bottomleftF2_area, STONE_BEAK7) # sideways block push to get to the owl statue + final_pillar.connect(bottomleftF2_area, BOMB) # bomb trigger pillar + pre_boss.connect(final_pillar, FEATHER) # superjump on top of goomba to extend superjump to boss door plateau + pre_boss.connect(beamos_horseheads_area, None, one_way=True) # can drop down from raised plateau to beamos horseheads area + + if options.logic == 'hell': + topright_pillar_area.connect(entrance, FEATHER) # superjump in the center to get on raised blocks, has to be low + topright_pillar_area.connect(entrance, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD))) # boots superhop in the center to get on raised blocks + toprightF1_chest.connect(topright_pillar_area, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD))) # boots superhop from F1 switch room + pre_boss.connect(final_pillar, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD))) # boots superhop on top of goomba to extend superhop to boss door plateau + + self.entrance = entrance + + +class NoDungeon7: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=7) + boss = Location(dungeon=7).add(HeartContainer(0x223), Instrument(0x22c)).connect(entrance, r.boss_requirements[ + world_setup.boss_mapping[6]]) + + self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon8.py b/worlds/ladx/LADXR/logic/dungeon8.py new file mode 100644 index 000000000000..4444ecbb1419 --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon8.py @@ -0,0 +1,107 @@ +from .requirements import * +from .location import Location +from ..locations.all import * + + +class Dungeon8: + def __init__(self, options, world_setup, r, *, back_entrance_heartpiece=True): + entrance = Location(dungeon=8) + entrance_up = Location(dungeon=8).connect(entrance, FEATHER) + entrance_left = Location(dungeon=8).connect(entrance, r.attack_hookshot_no_bomb) # past hinox + + # left side + entrance_left.add(DungeonChest(0x24D)) # zamboni room chest + Location(dungeon=8).add(DungeonChest(0x25C)).connect(entrance_left, r.attack_hookshot) # eye magnet chest + vire_drop_key = Location(dungeon=8).add(DroppedKey(0x24C)).connect(entrance_left, r.attack_hookshot_no_bomb) # vire drop key + sparks_chest = Location(dungeon=8).add(DungeonChest(0x255)).connect(entrance_left, OR(HOOKSHOT, FEATHER)) # chest before lvl1 miniboss + Location(dungeon=8).add(DungeonChest(0x246)).connect(entrance_left, MAGIC_ROD) # key chest that spawns after creating fire + + # right side + if options.owlstatues == "both" or options.owlstatues == "dungeon": + bottomright_owl = Location(dungeon=8).add(OwlStatue(0x253)).connect(entrance, AND(STONE_BEAK8, FEATHER, POWER_BRACELET)) # Two ways to reach this owl statue, but both require the same (except that one route requires bombs as well) + else: + bottomright_owl = None + slime_chest = Location(dungeon=8).add(DungeonChest(0x259)).connect(entrance, OR(FEATHER, AND(r.attack_hookshot, POWER_BRACELET))) # chest with slime + bottom_right = Location(dungeon=8).add(DroppedKey(0x25A)).connect(entrance, AND(FEATHER, OR(BOMB, AND(r.attack_hookshot_powder, POWER_BRACELET)))) # zamboni key drop; bombs for entrance up through switch room, weapon + bracelet for NW zamboni staircase to bottom right past smasher + bottomright_pot_chest = Location(dungeon=8).add(DungeonChest(0x25F)).connect(bottom_right, POWER_BRACELET) # 4 ropes pot room chest + + map_chest = Location(dungeon=8).add(DungeonChest(0x24F)).connect(entrance_up, None) # use the zamboni to get to the push blocks + lower_center = Location(dungeon=8).connect(entrance_up, KEY8) + upper_center = Location(dungeon=8).connect(lower_center, AND(KEY8, FOUND(KEY8, 2))) + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=8).add(OwlStatue(0x245)).connect(upper_center, STONE_BEAK8) + Location(dungeon=8).add(DroppedKey(0x23E)).connect(upper_center, r.attack_skeleton) # 2 gibdos cracked floor; technically possible to use pits to kill but dumb + medicine_chest = Location(dungeon=8).add(DungeonChest(0x235)).connect(upper_center, AND(FEATHER, HOOKSHOT)) # medicine chest + + middle_center_1 = Location(dungeon=8).connect(upper_center, BOMB) + middle_center_2 = Location(dungeon=8).connect(middle_center_1, AND(KEY8, FOUND(KEY8, 4))) + middle_center_3 = Location(dungeon=8).connect(middle_center_2, KEY8) + miniboss_entrance = Location(dungeon=8).connect(middle_center_3, AND(HOOKSHOT, KEY8, FOUND(KEY8, 7))) # hookshot to get across to keyblock, 7 to fix keylock issues if keys are used on other keyblocks + miniboss = Location(dungeon=8).connect(miniboss_entrance, AND(FEATHER, r.miniboss_requirements[world_setup.miniboss_mapping[7]])) # feather for 2d section, sword to kill + miniboss.add(DungeonChest(0x237)) # fire rod chest + + up_left = Location(dungeon=8).connect(upper_center, AND(r.attack_hookshot_powder, AND(KEY8, FOUND(KEY8, 4)))) + entrance_up.connect(up_left, AND(FEATHER, MAGIC_ROD), one_way=True) # alternate path with fire rod through 2d section to nightmare key + up_left.add(DungeonChest(0x240)) # beamos blocked chest + up_left.connect(entrance_left, None, one_way=True) # path from up_left to entrance_left by dropping of the ledge in torch room + Location(dungeon=8).add(DungeonChest(0x23D)).connect(up_left, BOMB) # dodongo chest + up_left.connect(upper_center, None, one_way=True) # use the outside path of the dungeon to get to the right side + if back_entrance_heartpiece: + Location().add(HeartPiece(0x000)).connect(up_left, None) # Outside the dungeon on the platform + Location(dungeon=8).add(DroppedKey(0x241)).connect(up_left, BOW) # lava statue + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=8).add(OwlStatue(0x241)).connect(up_left, STONE_BEAK8) + Location(dungeon=8).add(DungeonChest(0x23A)).connect(up_left, HOOKSHOT) # ledge chest left of boss door + + top_left_stairs = Location(dungeon=8).connect(entrance_up, AND(FEATHER, MAGIC_ROD)) + top_left_stairs.connect(up_left, None, one_way=True) # jump down from the staircase to the right + nightmare_key = Location(dungeon=8).add(DungeonChest(0x232)).connect(top_left_stairs, AND(FEATHER, SWORD, KEY8, FOUND(KEY8, 7))) + + # Bombing from the center dark rooms to the left so you can access more keys. + # The south walls of center dark room can be bombed from lower_center too with bomb and feather for center dark room access from the south, allowing even more access. Not sure if this should be logic since "obscure" + middle_center_2.connect(up_left, AND(BOMB, FEATHER), one_way=True) # does this even skip a key? both middle_center_2 and up_left come from upper_center with 1 extra key + + bossdoor = Location(dungeon=8).connect(entrance_up, AND(FEATHER, MAGIC_ROD)) + boss = Location(dungeon=8).add(HeartContainer(0x234), Instrument(0x230)).connect(bossdoor, AND(NIGHTMARE_KEY8, r.boss_requirements[world_setup.boss_mapping[7]])) + + if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': + entrance_left.connect(entrance, BOMB) # use bombs to kill vire and hinox + vire_drop_key.connect(entrance_left, BOMB) # use bombs to kill rolling bones and vire + bottom_right.connect(slime_chest, FEATHER) # diagonal jump over the pits to reach rolling rock / zamboni + up_left.connect(lower_center, AND(BOMB, FEATHER)) # blow up hidden walls from peahat room -> dark room -> eye statue room + slime_chest.connect(entrance, AND(r.attack_hookshot_powder, POWER_BRACELET)) # kill vire with powder or bombs + + if options.logic == 'glitched' or options.logic == 'hell': + sparks_chest.connect(entrance_left, OR(r.attack_hookshot, FEATHER, PEGASUS_BOOTS)) # 1 pit buffer across the pit. Add requirements for all the options to get to this area + lower_center.connect(entrance_up, None) # sideways block push in peahat room to get past keyblock + miniboss_entrance.connect(lower_center, AND(BOMB, FEATHER, HOOKSHOT)) # blow up hidden wall for darkroom, use feather + hookshot to clip past keyblock in front of stairs + miniboss_entrance.connect(lower_center, AND(BOMB, FEATHER, FOUND(KEY8, 7))) # same as above, but without clipping past the keyblock + up_left.connect(lower_center, FEATHER) # use jesus jump in refill room left of peahats to clip bottom wall and push bottom block left, to get a place to super jump + up_left.connect(upper_center, FEATHER) # from up left you can jesus jump / lava swim around the key door next to the boss. + top_left_stairs.connect(up_left, AND(FEATHER, SWORD)) # superjump + medicine_chest.connect(upper_center, FEATHER) # jesus super jump + up_left.connect(bossdoor, FEATHER, one_way=True) # superjump off the bottom or right wall to jump over to the boss door + + if options.logic == 'hell': + if bottomright_owl: + bottomright_owl.connect(entrance, AND(SWORD, POWER_BRACELET, PEGASUS_BOOTS, STONE_BEAK8)) # underground section past mimics, boots bonking across the gap to the ladder + bottomright_pot_chest.connect(entrance, AND(SWORD, POWER_BRACELET, PEGASUS_BOOTS)) # underground section past mimics, boots bonking across the gap to the ladder + entrance.connect(bottomright_pot_chest, AND(FEATHER, SWORD), one_way=True) # use NW zamboni staircase backwards, subpixel manip for superjump past the pots + medicine_chest.connect(upper_center, AND(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk + lava buffer to the bottom wall, then bonk onto the middle section + miniboss.connect(miniboss_entrance, AND(PEGASUS_BOOTS, r.miniboss_requirements[world_setup.miniboss_mapping[7]])) # get through 2d section with boots bonks + top_left_stairs.connect(map_chest, AND(PEGASUS_BOOTS, MAGIC_ROD)) # boots bonk + lava buffer from map chest to entrance_up, then boots bonk through 2d section + nightmare_key.connect(top_left_stairs, AND(PEGASUS_BOOTS, SWORD, FOUND(KEY8, 7))) # use a boots bonk to cross the 2d section + the lava in cueball room + bottom_right.connect(entrance_up, AND(POWER_BRACELET, PEGASUS_BOOTS), one_way=True) # take staircase to NW zamboni room, boots bonk onto the lava and water buffer all the way down to push the zamboni + bossdoor.connect(entrance_up, AND(PEGASUS_BOOTS, MAGIC_ROD)) # boots bonk through 2d section + + self.entrance = entrance + + +class NoDungeon8: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=8) + boss = Location(dungeon=8).add(HeartContainer(0x234)).connect(entrance, r.boss_requirements[ + world_setup.boss_mapping[7]]) + instrument = Location(dungeon=8).add(Instrument(0x230)).connect(boss, FEATHER) # jump over the lava to get to the instrument + + self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeonColor.py b/worlds/ladx/LADXR/logic/dungeonColor.py new file mode 100644 index 000000000000..aa58c0bafa91 --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeonColor.py @@ -0,0 +1,49 @@ +from .requirements import * +from .location import Location +from ..locations.all import * + + +class DungeonColor: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=9) + room2 = Location(dungeon=9).connect(entrance, r.attack_hookshot_powder) + room2.add(DungeonChest(0x314)) # key + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=9).add(OwlStatue(0x308), OwlStatue(0x30F)).connect(room2, STONE_BEAK9) + room2_weapon = Location(dungeon=9).connect(room2, r.attack_hookshot) + room2_weapon.add(DungeonChest(0x311)) # stone beak + room2_lights = Location(dungeon=9).connect(room2, OR(r.attack_hookshot, SHIELD)) + room2_lights.add(DungeonChest(0x30F)) # compass chest + room2_lights.add(DroppedKey(0x308)) + + Location(dungeon=9).connect(room2, AND(KEY9, FOUND(KEY9, 3), r.miniboss_requirements[world_setup.miniboss_mapping["c2"]])).add(DungeonChest(0x302)) # nightmare key after slime mini boss + room3 = Location(dungeon=9).connect(room2, AND(KEY9, FOUND(KEY9, 2), r.miniboss_requirements[world_setup.miniboss_mapping["c1"]])) # After the miniboss + room4 = Location(dungeon=9).connect(room3, POWER_BRACELET) # need to lift a pot to reveal button + room4.add(DungeonChest(0x306)) # map + room4karakoro = Location(dungeon=9).add(DroppedKey(0x307)).connect(room4, r.attack_hookshot) # require item to knock Karakoro enemies into shell + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=9).add(OwlStatue(0x30A)).connect(room4, STONE_BEAK9) + room5 = Location(dungeon=9).connect(room4, OR(r.attack_hookshot, SHIELD)) # lights room + room6 = Location(dungeon=9).connect(room5, AND(KEY9, FOUND(KEY9, 3))) # room with switch and nightmare door + pre_boss = Location(dungeon=9).connect(room6, OR(r.attack_hookshot, AND(PEGASUS_BOOTS, FEATHER))) # before the boss, require item to hit switch or jump past raised blocks + boss = Location(dungeon=9).connect(pre_boss, AND(NIGHTMARE_KEY9, r.boss_requirements[world_setup.boss_mapping[8]])) + boss.add(TunicFairy(0), TunicFairy(1)) + + if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': + room2.connect(entrance, POWER_BRACELET) # throw pots at enemies + pre_boss.connect(room6, FEATHER) # before the boss, jump past raised blocks without boots + + if options.logic == 'hell': + room2_weapon.connect(room2, SHIELD) # shield bump karakoro into the holes + room4karakoro.connect(room4, SHIELD) # shield bump karakoro into the holes + + self.entrance = entrance + + +class NoDungeonColor: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=9) + boss = Location(dungeon=9).connect(entrance, r.boss_requirements[world_setup.boss_mapping[8]]) + boss.add(TunicFairy(0), TunicFairy(1)) + + self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/location.py b/worlds/ladx/LADXR/logic/location.py new file mode 100644 index 000000000000..18615a11647f --- /dev/null +++ b/worlds/ladx/LADXR/logic/location.py @@ -0,0 +1,57 @@ +import typing +from .requirements import hasConsumableRequirement, OR +from ..locations.itemInfo import ItemInfo + + +class Location: + def __init__(self, name=None, dungeon=None): + self.name = name + self.items = [] # type: typing.List[ItemInfo] + self.dungeon = dungeon + self.__connected_to = set() + self.simple_connections = [] + self.gated_connections = [] + + def add(self, *item_infos): + for ii in item_infos: + assert isinstance(ii, ItemInfo) + ii.setLocation(self) + self.items.append(ii) + return self + + def connect(self, other, req, *, one_way=False): + assert isinstance(other, Location), type(other) + + if isinstance(req, bool): + if req: + self.connect(other, None, one_way=one_way) + return + + if other in self.__connected_to: + for idx, data in enumerate(self.gated_connections): + if data[0] == other: + if req is None or data[1] is None: + self.gated_connections[idx] = (other, None) + else: + self.gated_connections[idx] = (other, OR(req, data[1])) + break + for idx, data in enumerate(self.simple_connections): + if data[0] == other: + if req is None or data[1] is None: + self.simple_connections[idx] = (other, None) + else: + self.simple_connections[idx] = (other, OR(req, data[1])) + break + else: + self.__connected_to.add(other) + + if hasConsumableRequirement(req): + self.gated_connections.append((other, req)) + else: + self.simple_connections.append((other, req)) + if not one_way: + other.connect(self, req, one_way=True) + return self + + def __repr__(self): + return "<%s:%s:%d:%d:%d>" % (self.__class__.__name__, self.dungeon, len(self.items), len(self.simple_connections), len(self.gated_connections)) diff --git a/worlds/ladx/LADXR/logic/overworld.py b/worlds/ladx/LADXR/logic/overworld.py new file mode 100644 index 000000000000..551cf8353f4a --- /dev/null +++ b/worlds/ladx/LADXR/logic/overworld.py @@ -0,0 +1,682 @@ +from .requirements import * +from .location import Location +from ..locations.all import * +from ..worldSetup import ENTRANCE_INFO + + +class World: + def __init__(self, options, world_setup, r): + self.overworld_entrance = {} + self.indoor_location = {} + + mabe_village = Location("Mabe Village") + Location().add(HeartPiece(0x2A4)).connect(mabe_village, r.bush) # well + Location().add(FishingMinigame()).connect(mabe_village, AND(r.bush, COUNT("RUPEES", 20))) # fishing game, heart piece is directly done by the minigame. + Location().add(Seashell(0x0A3)).connect(mabe_village, r.bush) # bushes below the shop + Location().add(Seashell(0x0D2)).connect(mabe_village, PEGASUS_BOOTS) # smash into tree next to lv1 + Location().add(Song(0x092)).connect(mabe_village, OCARINA) # Marins song + rooster_cave = Location("Rooster Cave") + Location().add(DroppedKey(0x1E4)).connect(rooster_cave, AND(OCARINA, SONG3)) + + papahl_house = Location("Papahl House") + papahl_house.connect(Location().add(TradeSequenceItem(0x2A6, TRADING_ITEM_RIBBON)), TRADING_ITEM_YOSHI_DOLL) + + trendy_shop = Location("Trendy Shop").add(TradeSequenceItem(0x2A0, TRADING_ITEM_YOSHI_DOLL)) + #trendy_shop.connect(Location()) + + self._addEntrance("papahl_house_left", mabe_village, papahl_house, None) + self._addEntrance("papahl_house_right", mabe_village, papahl_house, None) + self._addEntrance("rooster_grave", mabe_village, rooster_cave, COUNT(POWER_BRACELET, 2)) + self._addEntranceRequirementExit("rooster_grave", None) # if exiting, you do not need l2 bracelet + self._addEntrance("madambowwow", mabe_village, None, None) + self._addEntrance("ulrira", mabe_village, None, None) + self._addEntrance("mabe_phone", mabe_village, None, None) + self._addEntrance("library", mabe_village, None, None) + self._addEntrance("trendy_shop", mabe_village, trendy_shop, r.bush) + self._addEntrance("d1", mabe_village, None, TAIL_KEY) + self._addEntranceRequirementExit("d1", None) # if exiting, you do not need the key + + start_house = Location("Start House").add(StartItem()) + self._addEntrance("start_house", mabe_village, start_house, None) + + shop = Location("Shop") + Location().add(ShopItem(0)).connect(shop, OR(COUNT("RUPEES", 500), SWORD)) + Location().add(ShopItem(1)).connect(shop, OR(COUNT("RUPEES", 1480), SWORD)) + self._addEntrance("shop", mabe_village, shop, None) + + dream_hut = Location("Dream Hut") + dream_hut_right = Location().add(Chest(0x2BF)).connect(dream_hut, SWORD) + if options.logic != "casual": + dream_hut_right.connect(dream_hut, OR(BOOMERANG, HOOKSHOT, FEATHER)) + dream_hut_left = Location().add(Chest(0x2BE)).connect(dream_hut_right, PEGASUS_BOOTS) + self._addEntrance("dream_hut", mabe_village, dream_hut, POWER_BRACELET) + + kennel = Location("Kennel").connect(Location().add(Seashell(0x2B2)), SHOVEL) # in the kennel + kennel.connect(Location().add(TradeSequenceItem(0x2B2, TRADING_ITEM_DOG_FOOD)), TRADING_ITEM_RIBBON) + self._addEntrance("kennel", mabe_village, kennel, None) + + sword_beach = Location("Sword Beach").add(BeachSword()).connect(mabe_village, OR(r.bush, SHIELD, r.attack_hookshot)) + banana_seller = Location("Banana Seller") + banana_seller.connect(Location().add(TradeSequenceItem(0x2FE, TRADING_ITEM_BANANAS)), TRADING_ITEM_DOG_FOOD) + self._addEntrance("banana_seller", sword_beach, banana_seller, r.bush) + boomerang_cave = Location("Boomerang Cave") + if options.boomerang == 'trade': + Location().add(BoomerangGuy()).connect(boomerang_cave, OR(BOOMERANG, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, FEATHER, SHOVEL)) + elif options.boomerang == 'gift': + Location().add(BoomerangGuy()).connect(boomerang_cave, None) + self._addEntrance("boomerang_cave", sword_beach, boomerang_cave, BOMB) + self._addEntranceRequirementExit("boomerang_cave", None) # if exiting, you do not need bombs + + sword_beach_to_ghost_hut = Location("Sword Beach to Ghost House").add(Chest(0x0E5)).connect(sword_beach, POWER_BRACELET) + ghost_hut_outside = Location("Outside Ghost House").connect(sword_beach_to_ghost_hut, POWER_BRACELET) + ghost_hut_inside = Location("Ghost House").connect(Location().add(Seashell(0x1E3)), POWER_BRACELET) + self._addEntrance("ghost_house", ghost_hut_outside, ghost_hut_inside, None) + + ## Forest area + forest = Location("Forest").connect(mabe_village, r.bush) # forest stretches all the way from the start town to the witch hut + Location().add(Chest(0x071)).connect(forest, POWER_BRACELET) # chest at start forest with 2 zols + forest_heartpiece = Location("Forest Heart Piece").add(HeartPiece(0x044)) # next to the forest, surrounded by pits + forest.connect(forest_heartpiece, OR(BOOMERANG, FEATHER, HOOKSHOT, ROOSTER), one_way=True) + + witch_hut = Location().connect(Location().add(Witch()), TOADSTOOL) + self._addEntrance("witch", forest, witch_hut, None) + crazy_tracy_hut = Location("Outside Crazy Tracy's House").connect(forest, POWER_BRACELET) + crazy_tracy_hut_inside = Location("Crazy Tracy's House") + Location().add(KeyLocation("MEDICINE2")).connect(crazy_tracy_hut_inside, FOUND("RUPEES", 50)) + self._addEntrance("crazy_tracy", crazy_tracy_hut, crazy_tracy_hut_inside, None) + start_house.connect(crazy_tracy_hut, SONG2, one_way=True) # Manbo's Mambo into the pond outside Tracy + + forest_madbatter = Location("Forest Mad Batter") + Location().add(MadBatter(0x1E1)).connect(forest_madbatter, MAGIC_POWDER) + self._addEntrance("forest_madbatter", forest, forest_madbatter, POWER_BRACELET) + self._addEntranceRequirementExit("forest_madbatter", None) # if exiting, you do not need bracelet + + forest_cave = Location("Forest Cave") + Location().add(Chest(0x2BD)).connect(forest_cave, SWORD) # chest in forest cave on route to mushroom + log_cave_heartpiece = Location().add(HeartPiece(0x2AB)).connect(forest_cave, POWER_BRACELET) # piece of heart in the forest cave on route to the mushroom + forest_toadstool = Location().add(Toadstool()) + self._addEntrance("toadstool_entrance", forest, forest_cave, None) + self._addEntrance("toadstool_exit", forest_toadstool, forest_cave, None) + + hookshot_cave = Location("Hookshot Cave") + hookshot_cave_chest = Location().add(Chest(0x2B3)).connect(hookshot_cave, OR(HOOKSHOT, ROOSTER)) + self._addEntrance("hookshot_cave", forest, hookshot_cave, POWER_BRACELET) + + swamp = Location("Swamp").connect(forest, AND(OR(MAGIC_POWDER, FEATHER, ROOSTER), r.bush)) + swamp.connect(forest, r.bush, one_way=True) # can go backwards past Tarin + swamp.connect(forest_toadstool, OR(FEATHER, ROOSTER)) + swamp_chest = Location("Swamp Chest").add(Chest(0x034)).connect(swamp, OR(BOWWOW, HOOKSHOT, MAGIC_ROD, BOOMERANG)) + self._addEntrance("d2", swamp, None, OR(BOWWOW, HOOKSHOT, MAGIC_ROD, BOOMERANG)) + forest_rear_chest = Location().add(Chest(0x041)).connect(swamp, r.bush) # tail key + self._addEntrance("writes_phone", swamp, None, None) + + writes_hut_outside = Location("Outside Write's House").connect(swamp, OR(FEATHER, ROOSTER)) # includes the cave behind the hut + writes_house = Location("Write's House") + writes_house.connect(Location().add(TradeSequenceItem(0x2a8, TRADING_ITEM_BROOM)), TRADING_ITEM_LETTER) + self._addEntrance("writes_house", writes_hut_outside, writes_house, None) + if options.owlstatues == "both" or options.owlstatues == "overworld": + writes_hut_outside.add(OwlStatue(0x11)) + writes_cave = Location("Write's Cave") + writes_cave_left_chest = Location().add(Chest(0x2AE)).connect(writes_cave, OR(FEATHER, ROOSTER, HOOKSHOT)) # 1st chest in the cave behind the hut + Location().add(Chest(0x2AF)).connect(writes_cave, POWER_BRACELET) # 2nd chest in the cave behind the hut. + self._addEntrance("writes_cave_left", writes_hut_outside, writes_cave, None) + self._addEntrance("writes_cave_right", writes_hut_outside, writes_cave, None) + + graveyard = Location("Graveyard").connect(forest, OR(FEATHER, ROOSTER, POWER_BRACELET)) # whole area from the graveyard up to the moblin cave + if options.owlstatues == "both" or options.owlstatues == "overworld": + graveyard.add(OwlStatue(0x035)) # Moblin cave owl + self._addEntrance("photo_house", graveyard, None, None) + self._addEntrance("d0", graveyard, None, POWER_BRACELET) + self._addEntranceRequirementExit("d0", None) # if exiting, you do not need bracelet + ghost_grave = Location().connect(forest, POWER_BRACELET) + Location().add(Seashell(0x074)).connect(ghost_grave, AND(r.bush, SHOVEL)) # next to grave cave, digging spot + + graveyard_cave_left = Location() + graveyard_cave_right = Location().connect(graveyard_cave_left, OR(FEATHER, ROOSTER)) + graveyard_heartpiece = Location().add(HeartPiece(0x2DF)).connect(graveyard_cave_right, OR(AND(BOMB, OR(HOOKSHOT, PEGASUS_BOOTS), FEATHER), ROOSTER)) # grave cave + self._addEntrance("graveyard_cave_left", ghost_grave, graveyard_cave_left, POWER_BRACELET) + self._addEntrance("graveyard_cave_right", graveyard, graveyard_cave_right, None) + moblin_cave = Location().connect(Location().add(Chest(0x2E2)), AND(r.attack_hookshot_powder, r.miniboss_requirements[world_setup.miniboss_mapping["moblin_cave"]])) + self._addEntrance("moblin_cave", graveyard, moblin_cave, None) + + # "Ukuku Prairie" + ukuku_prairie = Location().connect(mabe_village, POWER_BRACELET).connect(graveyard, POWER_BRACELET) + ukuku_prairie.connect(Location().add(TradeSequenceItem(0x07B, TRADING_ITEM_STICK)), TRADING_ITEM_BANANAS) + ukuku_prairie.connect(Location().add(TradeSequenceItem(0x087, TRADING_ITEM_HONEYCOMB)), TRADING_ITEM_STICK) + self._addEntrance("prairie_left_phone", ukuku_prairie, None, None) + self._addEntrance("prairie_right_phone", ukuku_prairie, None, None) + self._addEntrance("prairie_left_cave1", ukuku_prairie, Location().add(Chest(0x2CD)), None) # cave next to town + self._addEntrance("prairie_left_fairy", ukuku_prairie, None, BOMB) + self._addEntranceRequirementExit("prairie_left_fairy", None) # if exiting, you do not need bombs + + prairie_left_cave2 = Location() # Bomb cave + Location().add(Chest(0x2F4)).connect(prairie_left_cave2, PEGASUS_BOOTS) + Location().add(HeartPiece(0x2E5)).connect(prairie_left_cave2, AND(BOMB, PEGASUS_BOOTS)) + self._addEntrance("prairie_left_cave2", ukuku_prairie, prairie_left_cave2, BOMB) + self._addEntranceRequirementExit("prairie_left_cave2", None) # if exiting, you do not need bombs + + mamu = Location().connect(Location().add(Song(0x2FB)), AND(OCARINA, COUNT("RUPEES", 1480))) + self._addEntrance("mamu", ukuku_prairie, mamu, AND(OR(AND(FEATHER, PEGASUS_BOOTS), ROOSTER), OR(HOOKSHOT, ROOSTER), POWER_BRACELET)) + + dungeon3_entrance = Location().connect(ukuku_prairie, OR(FEATHER, ROOSTER, FLIPPERS)) + self._addEntrance("d3", dungeon3_entrance, None, SLIME_KEY) + self._addEntranceRequirementExit("d3", None) # if exiting, you do not need to open the door + Location().add(Seashell(0x0A5)).connect(dungeon3_entrance, SHOVEL) # above lv3 + dungeon3_entrance.connect(ukuku_prairie, None, one_way=True) # jump down ledge back to ukuku_prairie + + prairie_island_seashell = Location().add(Seashell(0x0A6)).connect(ukuku_prairie, AND(FLIPPERS, r.bush)) # next to lv3 + Location().add(Seashell(0x08B)).connect(ukuku_prairie, r.bush) # next to seashell house + Location().add(Seashell(0x0A4)).connect(ukuku_prairie, PEGASUS_BOOTS) # smash into tree next to phonehouse + self._addEntrance("castle_jump_cave", ukuku_prairie, Location().add(Chest(0x1FD)), OR(AND(FEATHER, PEGASUS_BOOTS), ROOSTER)) # left of the castle, 5 holes turned into 3 + Location().add(Seashell(0x0B9)).connect(ukuku_prairie, POWER_BRACELET) # under the rock + + left_bay_area = Location() + left_bay_area.connect(ghost_hut_outside, OR(AND(FEATHER, PEGASUS_BOOTS), ROOSTER)) + self._addEntrance("prairie_low_phone", left_bay_area, None, None) + + Location().add(Seashell(0x0E9)).connect(left_bay_area, r.bush) # same screen as mermaid statue + tiny_island = Location().add(Seashell(0x0F8)).connect(left_bay_area, AND(OR(FLIPPERS, ROOSTER), r.bush)) # tiny island + + prairie_plateau = Location() # prairie plateau at the owl statue + if options.owlstatues == "both" or options.owlstatues == "overworld": + prairie_plateau.add(OwlStatue(0x0A8)) + Location().add(Seashell(0x0A8)).connect(prairie_plateau, SHOVEL) # at the owl statue + + prairie_cave = Location() + prairie_cave_secret_exit = Location().connect(prairie_cave, AND(BOMB, OR(FEATHER, ROOSTER))) + self._addEntrance("prairie_right_cave_top", ukuku_prairie, prairie_cave, None) + self._addEntrance("prairie_right_cave_bottom", left_bay_area, prairie_cave, None) + self._addEntrance("prairie_right_cave_high", prairie_plateau, prairie_cave_secret_exit, None) + + bay_madbatter_connector_entrance = Location() + bay_madbatter_connector_exit = Location().connect(bay_madbatter_connector_entrance, FLIPPERS) + bay_madbatter_connector_outside = Location() + bay_madbatter = Location().connect(Location().add(MadBatter(0x1E0)), MAGIC_POWDER) + self._addEntrance("prairie_madbatter_connector_entrance", left_bay_area, bay_madbatter_connector_entrance, AND(OR(FEATHER, ROOSTER), OR(SWORD, MAGIC_ROD, BOOMERANG))) + self._addEntranceRequirementExit("prairie_madbatter_connector_entrance", AND(OR(FEATHER, ROOSTER), r.bush)) # if exiting, you can pick up the bushes by normal means + self._addEntrance("prairie_madbatter_connector_exit", bay_madbatter_connector_outside, bay_madbatter_connector_exit, None) + self._addEntrance("prairie_madbatter", bay_madbatter_connector_outside, bay_madbatter, None) + + seashell_mansion = Location() + if options.goal != "seashells": + Location().add(SeashellMansion(0x2E9)).connect(seashell_mansion, COUNT(SEASHELL, 20)) + else: + seashell_mansion.add(DroppedKey(0x2E9)) + self._addEntrance("seashell_mansion", ukuku_prairie, seashell_mansion, None) + + bay_water = Location() + bay_water.connect(ukuku_prairie, FLIPPERS) + bay_water.connect(left_bay_area, FLIPPERS) + fisher_under_bridge = Location().add(TradeSequenceItem(0x2F5, TRADING_ITEM_NECKLACE)) + fisher_under_bridge.connect(bay_water, AND(TRADING_ITEM_FISHING_HOOK, FEATHER, FLIPPERS)) + bay_water.connect(Location().add(TradeSequenceItem(0x0C9, TRADING_ITEM_SCALE)), AND(TRADING_ITEM_NECKLACE, FLIPPERS)) + d5_entrance = Location().connect(bay_water, FLIPPERS) + self._addEntrance("d5", d5_entrance, None, None) + + # Richard + richard_house = Location() + richard_cave = Location().connect(richard_house, COUNT(GOLD_LEAF, 5)) + richard_cave.connect(richard_house, None, one_way=True) # can exit richard's cave even without leaves + richard_cave_chest = Location().add(Chest(0x2C8)).connect(richard_cave, OR(FEATHER, HOOKSHOT, ROOSTER)) + richard_maze = Location() + self._addEntrance("richard_house", ukuku_prairie, richard_house, None) + self._addEntrance("richard_maze", richard_maze, richard_cave, None) + if options.owlstatues == "both" or options.owlstatues == "overworld": + Location().add(OwlStatue(0x0C6)).connect(richard_maze, r.bush) + Location().add(SlimeKey()).connect(richard_maze, AND(r.bush, SHOVEL)) + + next_to_castle = Location() + if options.tradequest: + ukuku_prairie.connect(next_to_castle, TRADING_ITEM_BANANAS, one_way=True) # can only give bananas from ukuku prairie side + else: + next_to_castle.connect(ukuku_prairie, None) + next_to_castle.connect(ukuku_prairie, FLIPPERS) + self._addEntrance("castle_phone", next_to_castle, None, None) + castle_secret_entrance_left = Location() + castle_secret_entrance_right = Location().connect(castle_secret_entrance_left, FEATHER) + castle_courtyard = Location() + castle_frontdoor = Location().connect(castle_courtyard, r.bush) + castle_frontdoor.connect(ukuku_prairie, "CASTLE_BUTTON") # the button in the castle connector allows access to the castle grounds in ER + self._addEntrance("castle_secret_entrance", next_to_castle, castle_secret_entrance_right, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD)) + self._addEntrance("castle_secret_exit", castle_courtyard, castle_secret_entrance_left, None) + + Location().add(HeartPiece(0x078)).connect(bay_water, FLIPPERS) # in the moat of the castle + castle_inside = Location() + Location().add(KeyLocation("CASTLE_BUTTON")).connect(castle_inside, None) + castle_top_outside = Location() + castle_top_inside = Location() + self._addEntrance("castle_main_entrance", castle_frontdoor, castle_inside, r.bush) + self._addEntrance("castle_upper_left", castle_top_outside, castle_inside, None) + self._addEntrance("castle_upper_right", castle_top_outside, castle_top_inside, None) + Location().add(GoldLeaf(0x05A)).connect(castle_courtyard, OR(SWORD, BOW, MAGIC_ROD)) # mad bomber, enemy hiding in the 6 holes + crow_gold_leaf = Location().add(GoldLeaf(0x058)).connect(castle_courtyard, AND(POWER_BRACELET, r.attack_hookshot_no_bomb)) # bird on tree, can't kill with bomb cause it flies off. immune to magic_powder + Location().add(GoldLeaf(0x2D2)).connect(castle_inside, r.attack_hookshot_powder) # in the castle, kill enemies + Location().add(GoldLeaf(0x2C5)).connect(castle_inside, AND(BOMB, r.attack_hookshot_powder)) # in the castle, bomb wall to show enemy + kanalet_chain_trooper = Location().add(GoldLeaf(0x2C6)) # in the castle, spinning spikeball enemy + castle_top_inside.connect(kanalet_chain_trooper, AND(POWER_BRACELET, r.attack_hookshot), one_way=True) + + animal_village = Location() + animal_village.connect(Location().add(TradeSequenceItem(0x0CD, TRADING_ITEM_FISHING_HOOK)), TRADING_ITEM_BROOM) + cookhouse = Location() + cookhouse.connect(Location().add(TradeSequenceItem(0x2D7, TRADING_ITEM_PINEAPPLE)), TRADING_ITEM_HONEYCOMB) + goathouse = Location() + goathouse.connect(Location().add(TradeSequenceItem(0x2D9, TRADING_ITEM_LETTER)), TRADING_ITEM_HIBISCUS) + mermaid_statue = Location() + mermaid_statue.connect(animal_village, AND(TRADING_ITEM_SCALE, HOOKSHOT)) + mermaid_statue.add(TradeSequenceItem(0x297, TRADING_ITEM_MAGNIFYING_GLASS)) + self._addEntrance("animal_phone", animal_village, None, None) + self._addEntrance("animal_house1", animal_village, None, None) + self._addEntrance("animal_house2", animal_village, None, None) + self._addEntrance("animal_house3", animal_village, goathouse, None) + self._addEntrance("animal_house4", animal_village, None, None) + self._addEntrance("animal_house5", animal_village, cookhouse, None) + animal_village.connect(bay_water, FLIPPERS) + animal_village.connect(ukuku_prairie, OR(HOOKSHOT, ROOSTER)) + animal_village_connector_left = Location() + animal_village_connector_right = Location().connect(animal_village_connector_left, PEGASUS_BOOTS) + self._addEntrance("prairie_to_animal_connector", ukuku_prairie, animal_village_connector_left, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD)) # passage under river blocked by bush + self._addEntrance("animal_to_prairie_connector", animal_village, animal_village_connector_right, None) + if options.owlstatues == "both" or options.owlstatues == "overworld": + animal_village.add(OwlStatue(0x0DA)) + Location().add(Seashell(0x0DA)).connect(animal_village, SHOVEL) # owl statue at the water + desert = Location().connect(animal_village, r.bush) # Note: We moved the walrus blocking the desert. + if options.owlstatues == "both" or options.owlstatues == "overworld": + desert.add(OwlStatue(0x0CF)) + desert_lanmola = Location().add(AnglerKey()).connect(desert, OR(BOW, SWORD, HOOKSHOT, MAGIC_ROD, BOOMERANG)) + + animal_village_bombcave = Location() + self._addEntrance("animal_cave", desert, animal_village_bombcave, BOMB) + self._addEntranceRequirementExit("animal_cave", None) # if exiting, you do not need bombs + animal_village_bombcave_heartpiece = Location().add(HeartPiece(0x2E6)).connect(animal_village_bombcave, OR(AND(BOMB, FEATHER, HOOKSHOT), ROOSTER)) # cave in the upper right of animal town + + desert_cave = Location() + self._addEntrance("desert_cave", desert, desert_cave, None) + desert.connect(desert_cave, None, one_way=True) # Drop down the sinkhole + + Location().add(HeartPiece(0x1E8)).connect(desert_cave, BOMB) # above the quicksand cave + Location().add(Seashell(0x0FF)).connect(desert, POWER_BRACELET) # bottom right corner of the map + + armos_maze = Location().connect(animal_village, POWER_BRACELET) + armos_temple = Location() + Location().add(FaceKey()).connect(armos_temple, r.miniboss_requirements[world_setup.miniboss_mapping["armos_temple"]]) + if options.owlstatues == "both" or options.owlstatues == "overworld": + armos_maze.add(OwlStatue(0x08F)) + self._addEntrance("armos_maze_cave", armos_maze, Location().add(Chest(0x2FC)), None) + self._addEntrance("armos_temple", armos_maze, armos_temple, None) + + armos_fairy_entrance = Location().connect(bay_water, FLIPPERS).connect(animal_village, POWER_BRACELET) + self._addEntrance("armos_fairy", armos_fairy_entrance, None, BOMB) + self._addEntranceRequirementExit("armos_fairy", None) # if exiting, you do not need bombs + + d6_connector_left = Location() + d6_connector_right = Location().connect(d6_connector_left, OR(AND(HOOKSHOT, OR(FLIPPERS, AND(FEATHER, PEGASUS_BOOTS))), ROOSTER)) + d6_entrance = Location() + d6_entrance.connect(bay_water, FLIPPERS, one_way=True) + d6_armos_island = Location().connect(bay_water, FLIPPERS) + self._addEntrance("d6_connector_entrance", d6_armos_island, d6_connector_right, None) + self._addEntrance("d6_connector_exit", d6_entrance, d6_connector_left, None) + self._addEntrance("d6", d6_entrance, None, FACE_KEY) + self._addEntranceRequirementExit("d6", None) # if exiting, you do not need to open the dungeon + + windfish_egg = Location().connect(swamp, POWER_BRACELET).connect(graveyard, POWER_BRACELET) + windfish_egg.connect(graveyard, None, one_way=True) # Ledge jump + + obstacle_cave_entrance = Location() + obstacle_cave_inside = Location().connect(obstacle_cave_entrance, SWORD) + obstacle_cave_inside.connect(obstacle_cave_entrance, FEATHER, one_way=True) # can get past the rock room from right to left pushing blocks and jumping over the pit + obstacle_cave_inside_chest = Location().add(Chest(0x2BB)).connect(obstacle_cave_inside, OR(HOOKSHOT, ROOSTER)) # chest at obstacles + obstacle_cave_exit = Location().connect(obstacle_cave_inside, OR(PEGASUS_BOOTS, ROOSTER)) + + lower_right_taltal = Location() + self._addEntrance("obstacle_cave_entrance", windfish_egg, obstacle_cave_entrance, POWER_BRACELET) + self._addEntrance("obstacle_cave_outside_chest", Location().add(Chest(0x018)), obstacle_cave_inside, None) + self._addEntrance("obstacle_cave_exit", lower_right_taltal, obstacle_cave_exit, None) + + papahl_cave = Location().add(Chest(0x28A)) + papahl = Location().connect(lower_right_taltal, None, one_way=True) + hibiscus_item = Location().add(TradeSequenceItem(0x019, TRADING_ITEM_HIBISCUS)) + papahl.connect(hibiscus_item, TRADING_ITEM_PINEAPPLE, one_way=True) + self._addEntrance("papahl_entrance", lower_right_taltal, papahl_cave, None) + self._addEntrance("papahl_exit", papahl, papahl_cave, None) + + # D4 entrance and related things + below_right_taltal = Location().connect(windfish_egg, POWER_BRACELET) + below_right_taltal.add(KeyLocation("ANGLER_KEYHOLE")) + below_right_taltal.connect(bay_water, FLIPPERS) + below_right_taltal.connect(next_to_castle, ROOSTER) # fly from staircase to staircase on the north side of the moat + lower_right_taltal.connect(below_right_taltal, FLIPPERS, one_way=True) + + heartpiece_swim_cave = Location().connect(Location().add(HeartPiece(0x1F2)), FLIPPERS) + self._addEntrance("heartpiece_swim_cave", below_right_taltal, heartpiece_swim_cave, FLIPPERS) # cave next to level 4 + d4_entrance = Location().connect(below_right_taltal, FLIPPERS) + lower_right_taltal.connect(d4_entrance, AND(ANGLER_KEY, "ANGLER_KEYHOLE"), one_way=True) + self._addEntrance("d4", d4_entrance, None, ANGLER_KEY) + self._addEntranceRequirementExit("d4", FLIPPERS) # if exiting, you can leave with flippers without opening the dungeon + mambo = Location().connect(Location().add(Song(0x2FD)), AND(OCARINA, FLIPPERS)) # Manbo's Mambo + self._addEntrance("mambo", d4_entrance, mambo, FLIPPERS) + + # Raft game. + raft_house = Location("Raft House") + Location().add(KeyLocation("RAFT")).connect(raft_house, COUNT("RUPEES", 100)) + raft_return_upper = Location() + raft_return_lower = Location().connect(raft_return_upper, None, one_way=True) + outside_raft_house = Location().connect(below_right_taltal, HOOKSHOT).connect(below_right_taltal, FLIPPERS, one_way=True) + raft_game = Location() + raft_game.connect(outside_raft_house, "RAFT") + raft_game.add(Chest(0x05C), Chest(0x05D)) # Chests in the rafting game + raft_exit = Location() + if options.logic != "casual": # use raft to reach north armos maze entrances without flippers + raft_game.connect(raft_exit, None, one_way=True) + raft_game.connect(armos_fairy_entrance, None, one_way=True) + self._addEntrance("raft_return_exit", outside_raft_house, raft_return_upper, None) + self._addEntrance("raft_return_enter", raft_exit, raft_return_lower, None) + raft_exit.connect(armos_fairy_entrance, FLIPPERS) + self._addEntrance("raft_house", outside_raft_house, raft_house, None) + if options.owlstatues == "both" or options.owlstatues == "overworld": + raft_game.add(OwlStatue(0x5D)) + + outside_rooster_house = Location().connect(lower_right_taltal, OR(FLIPPERS, ROOSTER)) + self._addEntrance("rooster_house", outside_rooster_house, None, None) + bird_cave = Location() + bird_key = Location().add(BirdKey()) + bird_cave.connect(bird_key, OR(AND(FEATHER, COUNT(POWER_BRACELET, 2)), ROOSTER)) + if options.logic != "casual": + bird_cave.connect(lower_right_taltal, None, one_way=True) # Drop in a hole at bird cave + self._addEntrance("bird_cave", outside_rooster_house, bird_cave, None) + bridge_seashell = Location().add(Seashell(0x00C)).connect(outside_rooster_house, AND(OR(FEATHER, ROOSTER), POWER_BRACELET)) # seashell right of rooster house, there is a hole in the bridge + + multichest_cave = Location() + multichest_cave_secret = Location().connect(multichest_cave, BOMB) + water_cave_hole = Location() # Location with the hole that drops you onto the hearth piece under water + if options.logic != "casual": + water_cave_hole.connect(heartpiece_swim_cave, FLIPPERS, one_way=True) + multichest_outside = Location().add(Chest(0x01D)) # chest after multichest puzzle outside + self._addEntrance("multichest_left", lower_right_taltal, multichest_cave, OR(FLIPPERS, ROOSTER)) + self._addEntrance("multichest_right", water_cave_hole, multichest_cave, None) + self._addEntrance("multichest_top", multichest_outside, multichest_cave_secret, None) + if options.owlstatues == "both" or options.owlstatues == "overworld": + water_cave_hole.add(OwlStatue(0x1E)) # owl statue below d7 + + right_taltal_connector1 = Location() + right_taltal_connector_outside1 = Location() + right_taltal_connector2 = Location() + right_taltal_connector3 = Location() + right_taltal_connector2.connect(right_taltal_connector3, AND(OR(FEATHER, ROOSTER), HOOKSHOT), one_way=True) + right_taltal_connector_outside2 = Location() + right_taltal_connector4 = Location() + d7_platau = Location() + d7_tower = Location() + d7_platau.connect(d7_tower, AND(POWER_BRACELET, BIRD_KEY), one_way=True) + self._addEntrance("right_taltal_connector1", water_cave_hole, right_taltal_connector1, None) + self._addEntrance("right_taltal_connector2", right_taltal_connector_outside1, right_taltal_connector1, None) + self._addEntrance("right_taltal_connector3", right_taltal_connector_outside1, right_taltal_connector2, None) + self._addEntrance("right_taltal_connector4", right_taltal_connector_outside2, right_taltal_connector3, None) + self._addEntrance("right_taltal_connector5", right_taltal_connector_outside2, right_taltal_connector4, None) + self._addEntrance("right_taltal_connector6", d7_platau, right_taltal_connector4, None) + self._addEntrance("right_fairy", right_taltal_connector_outside2, None, BOMB) + self._addEntranceRequirementExit("right_fairy", None) # if exiting, you do not need bombs + self._addEntrance("d7", d7_tower, None, None) + if options.logic != "casual": # D7 area ledge drops + d7_platau.connect(heartpiece_swim_cave, FLIPPERS, one_way=True) + d7_platau.connect(right_taltal_connector_outside1, None, one_way=True) + + mountain_bridge_staircase = Location().connect(outside_rooster_house, OR(HOOKSHOT, ROOSTER)) # cross bridges to staircase + if options.logic != "casual": # ledge drop + mountain_bridge_staircase.connect(windfish_egg, None, one_way=True) + + left_right_connector_cave_entrance = Location() + left_right_connector_cave_exit = Location() + left_right_connector_cave_entrance.connect(left_right_connector_cave_exit, OR(HOOKSHOT, ROOSTER), one_way=True) # pass through the underground passage to left side + taltal_boulder_zone = Location() + self._addEntrance("left_to_right_taltalentrance", mountain_bridge_staircase, left_right_connector_cave_entrance, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD)) + self._addEntrance("left_taltal_entrance", taltal_boulder_zone, left_right_connector_cave_exit, None) + mountain_heartpiece = Location().add(HeartPiece(0x2BA)) # heartpiece in connecting cave + left_right_connector_cave_entrance.connect(mountain_heartpiece, BOMB, one_way=True) # in the connecting cave from right to left. one_way to prevent access to left_side_mountain via glitched logic + + taltal_boulder_zone.add(Chest(0x004)) # top of falling rocks hill + taltal_madbatter = Location().connect(Location().add(MadBatter(0x1E2)), MAGIC_POWDER) + self._addEntrance("madbatter_taltal", taltal_boulder_zone, taltal_madbatter, POWER_BRACELET) + self._addEntranceRequirementExit("madbatter_taltal", None) # if exiting, you do not need bracelet + + outside_fire_cave = Location() + if options.logic != "casual": + outside_fire_cave.connect(writes_hut_outside, None, one_way=True) # Jump down the ledge + taltal_boulder_zone.connect(outside_fire_cave, None, one_way=True) + fire_cave_bottom = Location() + fire_cave_top = Location().connect(fire_cave_bottom, COUNT(SHIELD, 2)) + self._addEntrance("fire_cave_entrance", outside_fire_cave, fire_cave_bottom, BOMB) + self._addEntranceRequirementExit("fire_cave_entrance", None) # if exiting, you do not need bombs + + d8_entrance = Location() + if options.logic != "casual": + d8_entrance.connect(writes_hut_outside, None, one_way=True) # Jump down the ledge + d8_entrance.connect(outside_fire_cave, None, one_way=True) # Jump down the other ledge + self._addEntrance("fire_cave_exit", d8_entrance, fire_cave_top, None) + self._addEntrance("phone_d8", d8_entrance, None, None) + self._addEntrance("d8", d8_entrance, None, AND(OCARINA, SONG3, SWORD)) + self._addEntranceRequirementExit("d8", None) # if exiting, you do not need to wake the turtle + + nightmare = Location("Nightmare") + windfish = Location("Windfish").connect(nightmare, AND(MAGIC_POWDER, SWORD, OR(BOOMERANG, BOW))) + + if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': + hookshot_cave.connect(hookshot_cave_chest, AND(FEATHER, PEGASUS_BOOTS)) # boots jump the gap to the chest + graveyard_cave_left.connect(graveyard_cave_right, HOOKSHOT, one_way=True) # hookshot the block behind the stairs while over the pit + swamp_chest.connect(swamp, None) # Clip past the flower + self._addEntranceRequirement("d2", POWER_BRACELET) # clip the top wall to walk between the goponga flower and the wall + self._addEntranceRequirement("d2", COUNT(SWORD, 2)) # use l2 sword spin to kill goponga flowers + swamp.connect(writes_hut_outside, HOOKSHOT, one_way=True) # hookshot the sign in front of writes hut + graveyard_heartpiece.connect(graveyard_cave_right, FEATHER) # jump to the bottom right tile around the blocks + graveyard_heartpiece.connect(graveyard_cave_right, OR(HOOKSHOT, BOOMERANG)) # push bottom block, wall clip and hookshot/boomerang corner to grab item + + self._addEntranceRequirement("mamu", AND(FEATHER, POWER_BRACELET)) # can clear the gaps at the start with just feather, can reach bottom left sign with a well timed jump while wall clipped + self._addEntranceRequirement("prairie_madbatter_connector_entrance", AND(OR(FEATHER, ROOSTER), OR(MAGIC_POWDER, BOMB))) # use bombs or powder to get rid of a bush on the other side by jumping across and placing the bomb/powder before you fall into the pit + fisher_under_bridge.connect(bay_water, AND(TRADING_ITEM_FISHING_HOOK, FLIPPERS)) # can talk to the fisherman from the water when the boat is low (requires swimming up out of the water a bit) + crow_gold_leaf.connect(castle_courtyard, POWER_BRACELET) # bird on tree at left side kanalet, can use both rocks to kill the crow removing the kill requirement + castle_inside.connect(kanalet_chain_trooper, BOOMERANG, one_way=True) # kill the ball and chain trooper from the left side, then use boomerang to grab the dropped item + animal_village_bombcave_heartpiece.connect(animal_village_bombcave, AND(PEGASUS_BOOTS, FEATHER)) # jump across horizontal 4 gap to heart piece + desert_lanmola.connect(desert, BOMB) # use bombs to kill lanmola + + d6_connector_left.connect(d6_connector_right, AND(OR(FLIPPERS, PEGASUS_BOOTS), FEATHER)) # jump the gap in underground passage to d6 left side to skip hookshot + bird_key.connect(bird_cave, COUNT(POWER_BRACELET, 2)) # corner walk past the one pit on the left side to get to the elephant statue + fire_cave_bottom.connect(fire_cave_top, PEGASUS_BOOTS, one_way=True) # flame skip + + if options.logic == 'glitched' or options.logic == 'hell': + #self._addEntranceRequirement("dream_hut", FEATHER) # text clip TODO: require nag messages + self._addEntranceRequirementEnter("dream_hut", HOOKSHOT) # clip past the rocks in front of dream hut + dream_hut_right.connect(dream_hut_left, FEATHER) # super jump + forest.connect(swamp, BOMB) # bomb trigger tarin + forest.connect(forest_heartpiece, BOMB, one_way=True) # bomb trigger heartpiece + self._addEntranceRequirementEnter("hookshot_cave", HOOKSHOT) # clip past the rocks in front of hookshot cave + swamp.connect(forest_toadstool, None, one_way=True) # villa buffer from top (swamp phonebooth area) to bottom (toadstool area) + writes_hut_outside.connect(swamp, None, one_way=True) # villa buffer from top (writes hut) to bottom (swamp phonebooth area) or damage boost + graveyard.connect(forest_heartpiece, None, one_way=True) # villa buffer from top. + log_cave_heartpiece.connect(forest_cave, FEATHER) # super jump + log_cave_heartpiece.connect(forest_cave, BOMB) # bomb trigger + graveyard_cave_left.connect(graveyard_heartpiece, BOMB, one_way=True) # bomb trigger the heartpiece from the left side + graveyard_heartpiece.connect(graveyard_cave_right, None) # sideways block push from the right staircase. + + prairie_island_seashell.connect(ukuku_prairie, AND(FEATHER, r.bush)) # jesus jump from right side, screen transition on top of the water to reach the island + self._addEntranceRequirement("castle_jump_cave", FEATHER) # 1 pit buffer to clip bottom wall and jump across. + left_bay_area.connect(ghost_hut_outside, FEATHER) # 1 pit buffer to get across + tiny_island.connect(left_bay_area, AND(FEATHER, r.bush)) # jesus jump around + bay_madbatter_connector_exit.connect(bay_madbatter_connector_entrance, FEATHER, one_way=True) # jesus jump (3 screen) through the underground passage leading to martha's bay mad batter + self._addEntranceRequirement("prairie_madbatter_connector_entrance", AND(FEATHER, POWER_BRACELET)) # villa buffer into the top side of the bush, then pick it up + + ukuku_prairie.connect(richard_maze, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD), one_way=True) # break bushes on north side of the maze, and 1 pit buffer into the maze + fisher_under_bridge.connect(bay_water, AND(BOMB, FLIPPERS)) # can bomb trigger the item without having the hook + animal_village.connect(ukuku_prairie, FEATHER) # jesus jump + below_right_taltal.connect(next_to_castle, FEATHER) # jesus jump (north of kanalet castle phonebooth) + animal_village_connector_right.connect(animal_village_connector_left, FEATHER) # text clip past the obstacles (can go both ways), feather to wall clip the obstacle without triggering text or shaq jump in bottom right corner if text is off + animal_village_bombcave_heartpiece.connect(animal_village_bombcave, AND(BOMB, OR(HOOKSHOT, FEATHER, PEGASUS_BOOTS))) # bomb trigger from right side, corner walking top right pit is stupid so hookshot or boots added + animal_village_bombcave_heartpiece.connect(animal_village_bombcave, FEATHER) # villa buffer across the pits + + d6_entrance.connect(ukuku_prairie, FEATHER, one_way=True) # jesus jump (2 screen) from d6 entrance bottom ledge to ukuku prairie + d6_entrance.connect(armos_fairy_entrance, FEATHER, one_way=True) # jesus jump (2 screen) from d6 entrance top ledge to armos fairy entrance + armos_fairy_entrance.connect(d6_armos_island, FEATHER, one_way=True) # jesus jump from top (fairy bomb cave) to armos island + armos_fairy_entrance.connect(raft_exit, FEATHER) # jesus jump (2-ish screen) from fairy cave to lower raft connector + self._addEntranceRequirementEnter("obstacle_cave_entrance", HOOKSHOT) # clip past the rocks in front of obstacle cave entrance + obstacle_cave_inside_chest.connect(obstacle_cave_inside, FEATHER) # jump to the rightmost pits + 1 pit buffer to jump across + obstacle_cave_exit.connect(obstacle_cave_inside, FEATHER) # 1 pit buffer above boots crystals to get past + lower_right_taltal.connect(hibiscus_item, AND(TRADING_ITEM_PINEAPPLE, BOMB), one_way=True) # bomb trigger papahl from below ledge, requires pineapple + + self._addEntranceRequirement("heartpiece_swim_cave", FEATHER) # jesus jump into the cave entrance after jumping down the ledge, can jesus jump back to the ladder 1 screen below + self._addEntranceRequirement("mambo", FEATHER) # jesus jump from (unlocked) d4 entrance to mambo's cave entrance + outside_raft_house.connect(below_right_taltal, FEATHER, one_way=True) # jesus jump from the ledge at raft to the staircase 1 screen south + + self._addEntranceRequirement("multichest_left", FEATHER) # jesus jump past staircase leading up the mountain + outside_rooster_house.connect(lower_right_taltal, FEATHER) # jesus jump (1 or 2 screen depending if angler key is used) to staircase leading up the mountain + d7_platau.connect(water_cave_hole, None, one_way=True) # use save and quit menu to gain control while falling to dodge the water cave hole + mountain_bridge_staircase.connect(outside_rooster_house, AND(PEGASUS_BOOTS, FEATHER)) # cross bridge to staircase with pit buffer to clip bottom wall and jump across + bird_key.connect(bird_cave, AND(FEATHER, HOOKSHOT)) # hookshot jump across the big pits room + right_taltal_connector2.connect(right_taltal_connector3, None, one_way=True) # 2 seperate pit buffers so not obnoxious to get past the two pit rooms before d7 area. 2nd pits can pit buffer on top right screen, bottom wall to scroll on top of the wall on bottom screen + left_right_connector_cave_exit.connect(left_right_connector_cave_entrance, AND(HOOKSHOT, FEATHER), one_way=True) # pass through the passage in reverse using a superjump to get out of the dead end + obstacle_cave_inside.connect(mountain_heartpiece, BOMB, one_way=True) # bomb trigger from boots crystal cave + self._addEntranceRequirement("d8", OR(BOMB, AND(OCARINA, SONG3))) # bomb trigger the head and walk trough, or play the ocarina song 3 and walk through + + if options.logic == 'hell': + dream_hut_right.connect(dream_hut, None) # alternate diagonal movement with orthogonal movement to control the mimics. Get them clipped into the walls to walk past + swamp.connect(forest_toadstool, None) # damage boost from toadstool area across the pit + swamp.connect(forest, AND(r.bush, OR(PEGASUS_BOOTS, HOOKSHOT))) # boots bonk / hookshot spam over the pits right of forest_rear_chest + forest.connect(forest_heartpiece, PEGASUS_BOOTS, one_way=True) # boots bonk across the pits + log_cave_heartpiece.connect(forest_cave, BOOMERANG) # clip the boomerang through the corner gaps on top right to grab the item + log_cave_heartpiece.connect(forest_cave, AND(ROOSTER, OR(PEGASUS_BOOTS, SWORD, BOW, MAGIC_ROD))) # boots rooster hop in bottom left corner to "superjump" into the area. use buffers after picking up rooster to gain height / time to throw rooster again facing up + writes_hut_outside.connect(swamp, None) # damage boost with moblin arrow next to telephone booth + writes_cave_left_chest.connect(writes_cave, None) # damage boost off the zol to get across the pit. + graveyard.connect(crazy_tracy_hut, HOOKSHOT, one_way=True) # use hookshot spam to clip the rock on the right with the crow + graveyard.connect(forest, OR(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk witches hut, or hookshot spam across the pit + graveyard_cave_left.connect(graveyard_cave_right, HOOKSHOT) # hookshot spam over the pit + graveyard_cave_right.connect(graveyard_cave_left, PEGASUS_BOOTS, one_way=True) # boots bonk off the cracked block + + self._addEntranceRequirementEnter("mamu", AND(PEGASUS_BOOTS, POWER_BRACELET)) # can clear the gaps at the start with multiple pit buffers, can reach bottom left sign with bonking along the bottom wall + self._addEntranceRequirement("castle_jump_cave", PEGASUS_BOOTS) # pit buffer to clip bottom wall and boots bonk across + prairie_cave_secret_exit.connect(prairie_cave, AND(BOMB, OR(PEGASUS_BOOTS, HOOKSHOT))) # hookshot spam or boots bonk across pits can go from left to right by pit buffering on top of the bottom wall then boots bonk across + richard_cave_chest.connect(richard_cave, None) # use the zol on the other side of the pit to damage boost across (requires damage from pit + zol) + castle_secret_entrance_right.connect(castle_secret_entrance_left, AND(PEGASUS_BOOTS, "MEDICINE2")) # medicine iframe abuse to get across spikes with a boots bonk + left_bay_area.connect(ghost_hut_outside, PEGASUS_BOOTS) # multiple pit buffers to bonk across the bottom wall + tiny_island.connect(left_bay_area, AND(PEGASUS_BOOTS, r.bush)) # jesus jump around with boots bonks, then one final bonk off the bottom wall to get on the staircase (needs to be centered correctly) + self._addEntranceRequirement("prairie_madbatter_connector_entrance", AND(PEGASUS_BOOTS, OR(MAGIC_POWDER, BOMB, SWORD, MAGIC_ROD, BOOMERANG))) # Boots bonk across the bottom wall, then remove one of the bushes to get on land + self._addEntranceRequirementExit("prairie_madbatter_connector_entrance", AND(PEGASUS_BOOTS, r.bush)) # if exiting, you can pick up the bushes by normal means and boots bonk across the bottom wall + + # bay_water connectors, only left_bay_area, ukuku_prairie and animal_village have to be connected with jesus jumps. below_right_taltal, d6_armos_island and armos_fairy_entrance are accounted for via ukuku prairie in glitch logic + left_bay_area.connect(bay_water, FEATHER) # jesus jump (can always reach bay_water with jesus jumping from every way to enter bay_water, so no one_way) + animal_village.connect(bay_water, FEATHER) # jesus jump (can always reach bay_water with jesus jumping from every way to enter bay_water, so no one_way) + ukuku_prairie.connect(bay_water, FEATHER, one_way=True) # jesus jump + bay_water.connect(d5_entrance, FEATHER) # jesus jump into d5 entrance (wall clip), wall clip + jesus jump to get out + + crow_gold_leaf.connect(castle_courtyard, BOMB) # bird on tree at left side kanalet, place a bomb against the tree and the crow flies off. With well placed second bomb the crow can be killed + mermaid_statue.connect(animal_village, AND(TRADING_ITEM_SCALE, FEATHER)) # early mermaid statue by buffering on top of the right ledge, then superjumping to the left (horizontal pixel perfect) + animal_village_bombcave_heartpiece.connect(animal_village_bombcave, PEGASUS_BOOTS) # boots bonk across bottom wall (both at entrance and in item room) + + d6_armos_island.connect(ukuku_prairie, FEATHER) # jesus jump (3 screen) from seashell mansion to armos island + armos_fairy_entrance.connect(d6_armos_island, PEGASUS_BOOTS, one_way=True) # jesus jump from top (fairy bomb cave) to armos island with fast falling + d6_connector_right.connect(d6_connector_left, PEGASUS_BOOTS) # boots bonk across bottom wall at water and pits (can do both ways) + + obstacle_cave_entrance.connect(obstacle_cave_inside, OR(HOOKSHOT, AND(FEATHER, PEGASUS_BOOTS, OR(SWORD, MAGIC_ROD, BOW)))) # get past crystal rocks by hookshotting into top pushable block, or boots dashing into top wall where the pushable block is to superjump down + obstacle_cave_entrance.connect(obstacle_cave_inside, AND(PEGASUS_BOOTS, ROOSTER)) # get past crystal rocks pushing the top pushable block, then boots dashing up picking up the rooster before bonking. Pause buffer until rooster is fully picked up then throw it down before bonking into wall + d4_entrance.connect(below_right_taltal, FEATHER) # jesus jump a long way + if options.entranceshuffle in ("default", "simple"): # connector cave from armos d6 area to raft shop may not be randomized to add a flippers path since flippers stop you from jesus jumping + below_right_taltal.connect(raft_game, AND(FEATHER, r.attack_hookshot_powder), one_way=True) # jesus jump from heartpiece water cave, around the island and clip past the diagonal gap in the rock, then jesus jump all the way down the waterfall to the chests (attack req for hardlock flippers+feather scenario) + outside_raft_house.connect(below_right_taltal, AND(FEATHER, PEGASUS_BOOTS)) #superjump from ledge left to right, can buffer to land on ledge instead of water, then superjump right which is pixel perfect + bridge_seashell.connect(outside_rooster_house, AND(PEGASUS_BOOTS, POWER_BRACELET)) # boots bonk + bird_key.connect(bird_cave, AND(FEATHER, PEGASUS_BOOTS)) # boots jump above wall, use multiple pit buffers to get across + mountain_bridge_staircase.connect(outside_rooster_house, OR(PEGASUS_BOOTS, FEATHER)) # cross bridge to staircase with pit buffer to clip bottom wall and jump or boots bonk across + left_right_connector_cave_entrance.connect(left_right_connector_cave_exit, AND(PEGASUS_BOOTS, FEATHER), one_way=True) # boots jump to bottom left corner of pits, pit buffer and jump to left + left_right_connector_cave_exit.connect(left_right_connector_cave_entrance, AND(ROOSTER, OR(PEGASUS_BOOTS, SWORD, BOW, MAGIC_ROD)), one_way=True) # pass through the passage in reverse using a boots rooster hop or rooster superjump in the one way passage area + + self.start = start_house + self.egg = windfish_egg + self.nightmare = nightmare + self.windfish = windfish + + def _addEntrance(self, name, outside, inside, requirement): + assert name not in self.overworld_entrance, "Duplicate entrance: %s" % name + assert name in ENTRANCE_INFO + self.overworld_entrance[name] = EntranceExterior(outside, requirement) + self.indoor_location[name] = inside + + def _addEntranceRequirement(self, name, requirement): + assert name in self.overworld_entrance + self.overworld_entrance[name].addRequirement(requirement) + + def _addEntranceRequirementEnter(self, name, requirement): + assert name in self.overworld_entrance + self.overworld_entrance[name].addEnterRequirement(requirement) + + def _addEntranceRequirementExit(self, name, requirement): + assert name in self.overworld_entrance + self.overworld_entrance[name].addExitRequirement(requirement) + + def updateIndoorLocation(self, name, location): + assert name in self.indoor_location + assert self.indoor_location[name] is None + self.indoor_location[name] = location + + +class DungeonDiveOverworld: + def __init__(self, options, r): + self.overworld_entrance = {} + self.indoor_location = {} + + start_house = Location("Start House").add(StartItem()) + Location().add(ShopItem(0)).connect(start_house, OR(COUNT("RUPEES", 200), SWORD)) + Location().add(ShopItem(1)).connect(start_house, OR(COUNT("RUPEES", 980), SWORD)) + Location().add(Song(0x0B1)).connect(start_house, OCARINA) # Marins song + start_house.add(DroppedKey(0xB2)) # Sword on the beach + egg = Location().connect(start_house, AND(r.bush, BOMB)) + Location().add(MadBatter(0x1E1)).connect(start_house, MAGIC_POWDER) + if options.boomerang == 'trade': + Location().add(BoomerangGuy()).connect(start_house, AND(BOMB, OR(BOOMERANG, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, FEATHER, SHOVEL))) + elif options.boomerang == 'gift': + Location().add(BoomerangGuy()).connect(start_house, BOMB) + + nightmare = Location("Nightmare") + windfish = Location("Windfish").connect(nightmare, AND(MAGIC_POWDER, SWORD, OR(BOOMERANG, BOW))) + + self.start = start_house + self.overworld_entrance = { + "d1": EntranceExterior(start_house, None), + "d2": EntranceExterior(start_house, None), + "d3": EntranceExterior(start_house, None), + "d4": EntranceExterior(start_house, None), + "d5": EntranceExterior(start_house, FLIPPERS), + "d6": EntranceExterior(start_house, None), + "d7": EntranceExterior(start_house, None), + "d8": EntranceExterior(start_house, None), + "d0": EntranceExterior(start_house, None), + } + self.egg = egg + self.nightmare = nightmare + self.windfish = windfish + + def updateIndoorLocation(self, name, location): + self.indoor_location[name] = location + + +class EntranceExterior: + def __init__(self, outside, requirement, one_way_enter_requirement="UNSET", one_way_exit_requirement="UNSET"): + self.location = outside + self.requirement = requirement + self.one_way_enter_requirement = one_way_enter_requirement + self.one_way_exit_requirement = one_way_exit_requirement + + def addRequirement(self, new_requirement): + self.requirement = OR(self.requirement, new_requirement) + + def addExitRequirement(self, new_requirement): + if self.one_way_exit_requirement == "UNSET": + self.one_way_exit_requirement = new_requirement + else: + self.one_way_exit_requirement = OR(self.one_way_exit_requirement, new_requirement) + + def addEnterRequirement(self, new_requirement): + if self.one_way_enter_requirement == "UNSET": + self.one_way_enter_requirement = new_requirement + else: + self.one_way_enter_requirement = OR(self.one_way_enter_requirement, new_requirement) + + def enterIsSet(self): + return self.one_way_enter_requirement != "UNSET" + + def exitIsSet(self): + return self.one_way_exit_requirement != "UNSET" diff --git a/worlds/ladx/LADXR/logic/requirements.py b/worlds/ladx/LADXR/logic/requirements.py new file mode 100644 index 000000000000..acc969ba938d --- /dev/null +++ b/worlds/ladx/LADXR/logic/requirements.py @@ -0,0 +1,318 @@ +from typing import Optional +from ..locations.items import * + + +class OR: + __slots__ = ('__items', '__children') + + def __new__(cls, *args): + if True in args: + return True + return super().__new__(cls) + + def __init__(self, *args): + self.__items = [item for item in args if isinstance(item, str)] + self.__children = [item for item in args if type(item) not in (bool, str) and item is not None] + + assert self.__items or self.__children, args + + def __repr__(self) -> str: + return "or%s" % (self.__items+self.__children) + + def remove(self, item) -> None: + if item in self.__items: + self.__items.remove(item) + + def hasConsumableRequirement(self) -> bool: + for item in self.__items: + if isConsumable(item): + print("Consumable OR requirement? %r" % self) + return True + for child in self.__children: + if child.hasConsumableRequirement(): + print("Consumable OR requirement? %r" % self) + return True + return False + + def test(self, inventory) -> bool: + for item in self.__items: + if item in inventory: + return True + for child in self.__children: + if child.test(inventory): + return True + return False + + def consume(self, inventory) -> bool: + for item in self.__items: + if item in inventory: + if isConsumable(item): + inventory[item] -= 1 + if inventory[item] == 0: + del inventory[item] + inventory["%s_USED" % item] = inventory.get("%s_USED" % item, 0) + 1 + return True + for child in self.__children: + if child.consume(inventory): + return True + return False + + def getItems(self, inventory, target_set) -> None: + if self.test(inventory): + return + for item in self.__items: + target_set.add(item) + for child in self.__children: + child.getItems(inventory, target_set) + + def copyWithModifiedItemNames(self, f) -> "OR": + return OR(*(f(item) for item in self.__items), *(child.copyWithModifiedItemNames(f) for child in self.__children)) + + +class AND: + __slots__ = ('__items', '__children') + + def __new__(cls, *args): + if False in args: + return False + return super().__new__(cls) + + def __init__(self, *args): + self.__items = [item for item in args if isinstance(item, str)] + self.__children = [item for item in args if type(item) not in (bool, str) and item is not None] + + def __repr__(self) -> str: + return "and%s" % (self.__items+self.__children) + + def remove(self, item) -> None: + if item in self.__items: + self.__items.remove(item) + + def hasConsumableRequirement(self) -> bool: + for item in self.__items: + if isConsumable(item): + return True + for child in self.__children: + if child.hasConsumableRequirement(): + return True + return False + + def test(self, inventory) -> bool: + for item in self.__items: + if item not in inventory: + return False + for child in self.__children: + if not child.test(inventory): + return False + return True + + def consume(self, inventory) -> bool: + for item in self.__items: + if isConsumable(item): + inventory[item] -= 1 + if inventory[item] == 0: + del inventory[item] + inventory["%s_USED" % item] = inventory.get("%s_USED" % item, 0) + 1 + for child in self.__children: + if not child.consume(inventory): + return False + return True + + def getItems(self, inventory, target_set) -> None: + if self.test(inventory): + return + for item in self.__items: + target_set.add(item) + for child in self.__children: + child.getItems(inventory, target_set) + + def copyWithModifiedItemNames(self, f) -> "AND": + return AND(*(f(item) for item in self.__items), *(child.copyWithModifiedItemNames(f) for child in self.__children)) + + +class COUNT: + __slots__ = ('__item', '__amount') + + def __init__(self, item: str, amount: int) -> None: + self.__item = item + self.__amount = amount + + def __repr__(self) -> str: + return "<%dx%s>" % (self.__amount, self.__item) + + def hasConsumableRequirement(self) -> bool: + if isConsumable(self.__item): + return True + return False + + def test(self, inventory) -> bool: + return inventory.get(self.__item, 0) >= self.__amount + + def consume(self, inventory) -> None: + if isConsumable(self.__item): + inventory[self.__item] -= self.__amount + if inventory[self.__item] == 0: + del inventory[self.__item] + inventory["%s_USED" % self.__item] = inventory.get("%s_USED" % self.__item, 0) + self.__amount + + def getItems(self, inventory, target_set) -> None: + if self.test(inventory): + return + target_set.add(self.__item) + + def copyWithModifiedItemNames(self, f) -> "COUNT": + return COUNT(f(self.__item), self.__amount) + + +class COUNTS: + __slots__ = ('__items', '__amount') + + def __init__(self, items, amount): + self.__items = items + self.__amount = amount + + def __repr__(self) -> str: + return "<%dx%s>" % (self.__amount, self.__items) + + def hasConsumableRequirement(self) -> bool: + for item in self.__items: + if isConsumable(item): + print("Consumable COUNTS requirement? %r" % (self)) + return True + return False + + def test(self, inventory) -> bool: + count = 0 + for item in self.__items: + count += inventory.get(item, 0) + return count >= self.__amount + + def consume(self, inventory) -> None: + for item in self.__items: + if isConsumable(item): + inventory[item] -= self.__amount + if inventory[item] == 0: + del inventory[item] + inventory["%s_USED" % item] = inventory.get("%s_USED" % item, 0) + self.__amount + + def getItems(self, inventory, target_set) -> None: + if self.test(inventory): + return + for item in self.__items: + target_set.add(item) + + def copyWithModifiedItemNames(self, f) -> "COUNTS": + return COUNTS([f(item) for item in self.__items], self.__amount) + + +class FOUND: + __slots__ = ('__item', '__amount') + + def __init__(self, item: str, amount: int) -> None: + self.__item = item + self.__amount = amount + + def __repr__(self) -> str: + return "{%dx%s}" % (self.__amount, self.__item) + + def hasConsumableRequirement(self) -> bool: + return False + + def test(self, inventory) -> bool: + return inventory.get(self.__item, 0) + inventory.get("%s_USED" % self.__item, 0) >= self.__amount + + def consume(self, inventory) -> None: + pass + + def getItems(self, inventory, target_set) -> None: + if self.test(inventory): + return + target_set.add(self.__item) + + def copyWithModifiedItemNames(self, f) -> "FOUND": + return FOUND(f(self.__item), self.__amount) + + +def hasConsumableRequirement(requirements) -> bool: + if isinstance(requirements, str): + return isConsumable(requirements) + if requirements is None: + return False + return requirements.hasConsumableRequirement() + + +def isConsumable(item) -> bool: + if item is None: + return False + #if item.startswith("RUPEES_") or item == "RUPEES": + # return True + if item.startswith("KEY"): + return True + return False + + +class RequirementsSettings: + def __init__(self, options): + self.bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, POWER_BRACELET, BOOMERANG) + self.attack = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG) + self.attack_hookshot = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # switches, hinox, shrouded stalfos + self.attack_hookshot_powder = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT, MAGIC_POWDER) # zols, keese, moldorm + self.attack_no_bomb = OR(SWORD, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # ? + self.attack_hookshot_no_bomb = OR(SWORD, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # vire + self.attack_no_boomerang = OR(SWORD, BOMB, BOW, MAGIC_ROD, HOOKSHOT) # teleporting owls + self.attack_skeleton = OR(SWORD, BOMB, BOW, BOOMERANG, HOOKSHOT) # cannot kill skeletons with the fire rod + self.rear_attack = OR(SWORD, BOMB) # mimic + self.rear_attack_range = OR(MAGIC_ROD, BOW) # mimic + self.fire = OR(MAGIC_POWDER, MAGIC_ROD) # torches + self.push_hardhat = OR(SHIELD, SWORD, HOOKSHOT, BOOMERANG) + + self.boss_requirements = [ + SWORD, # D1 boss + AND(OR(SWORD, MAGIC_ROD), POWER_BRACELET), # D2 boss + AND(PEGASUS_BOOTS, SWORD), # D3 boss + AND(FLIPPERS, OR(SWORD, MAGIC_ROD, BOW)), # D4 boss + AND(HOOKSHOT, SWORD), # D5 boss + BOMB, # D6 boss + AND(OR(MAGIC_ROD, SWORD, HOOKSHOT), COUNT(SHIELD, 2)), # D7 boss + MAGIC_ROD, # D8 boss + self.attack_hookshot_no_bomb, # D9 boss + ] + self.miniboss_requirements = { + "ROLLING_BONES": self.attack_hookshot, + "HINOX": self.attack_hookshot, + "DODONGO": BOMB, + "CUE_BALL": SWORD, + "GHOMA": OR(BOW, HOOKSHOT), + "SMASHER": POWER_BRACELET, + "GRIM_CREEPER": self.attack_hookshot_no_bomb, + "BLAINO": SWORD, + "AVALAUNCH": self.attack_hookshot, + "GIANT_BUZZ_BLOB": MAGIC_POWDER, + "MOBLIN_KING": SWORD, + "ARMOS_KNIGHT": OR(BOW, MAGIC_ROD, SWORD), + } + + # Adjust for options + if options.bowwow != 'normal': + # We cheat in bowwow mode, we pretend we have the sword, as bowwow can pretty much do all what the sword ca$ # Except for taking out bushes (and crystal pillars are removed) + self.bush.remove(SWORD) + if options.logic == "casual": + # In casual mode, remove the more complex kill methods + self.bush.remove(MAGIC_POWDER) + self.attack_hookshot_powder.remove(MAGIC_POWDER) + self.attack.remove(BOMB) + self.attack_hookshot.remove(BOMB) + self.attack_hookshot_powder.remove(BOMB) + self.attack_no_boomerang.remove(BOMB) + self.attack_skeleton.remove(BOMB) + if options.logic == "hard": + self.boss_requirements[3] = AND(FLIPPERS, OR(SWORD, MAGIC_ROD, BOW, BOMB)) # bomb angler fish + self.boss_requirements[6] = OR(MAGIC_ROD, AND(BOMB, BOW), COUNT(SWORD, 2), AND(OR(SWORD, HOOKSHOT, BOW), SHIELD)) # evil eagle 3 cycle magic rod / bomb arrows / l2 sword, and bow kill + if options.logic == "glitched": + self.boss_requirements[3] = AND(FLIPPERS, OR(SWORD, MAGIC_ROD, BOW, BOMB)) # bomb angler fish + self.boss_requirements[6] = OR(MAGIC_ROD, BOMB, BOW, HOOKSHOT, COUNT(SWORD, 2), AND(SWORD, SHIELD)) # evil eagle off screen kill or 3 cycle with bombs + if options.logic == "hell": + self.boss_requirements[3] = AND(FLIPPERS, OR(SWORD, MAGIC_ROD, BOW, BOMB)) # bomb angler fish + self.boss_requirements[6] = OR(MAGIC_ROD, BOMB, BOW, HOOKSHOT, COUNT(SWORD, 2), AND(SWORD, SHIELD)) # evil eagle off screen kill or 3 cycle with bombs + self.boss_requirements[7] = OR(MAGIC_ROD, COUNT(SWORD, 2)) # hot head sword beams + self.miniboss_requirements["GIANT_BUZZ_BLOB"] = OR(MAGIC_POWDER, COUNT(SWORD,2)) # use sword beams to damage buzz blob diff --git a/worlds/ladx/LADXR/main.py b/worlds/ladx/LADXR/main.py new file mode 100644 index 000000000000..5b563675c039 --- /dev/null +++ b/worlds/ladx/LADXR/main.py @@ -0,0 +1,52 @@ +import binascii +from .romTables import ROMWithTables +import json +from . import logic +import argparse +from .settings import Settings +from typing import Optional, List + +def get_parser(): + + parser = argparse.ArgumentParser(description='Randomize!') + parser.add_argument('input_filename', metavar='input rom', type=str, + help="Rom file to use as input.") + parser.add_argument('-o', '--output', dest="output_filename", metavar='output rom', type=str, required=False, + help="Output filename to use. If not specified [seed].gbc is used.") + parser.add_argument('--dump', dest="dump", type=str, nargs="*", + help="Dump the logic of the given rom (spoilers!)") + parser.add_argument('--spoilerformat', dest="spoilerformat", choices=["none", "console", "text", "json"], default="none", + help="Sets the output format for the generated seed's spoiler log") + parser.add_argument('--spoilerfilename', dest="spoiler_filename", type=str, required=False, + help="Output filename to use for the spoiler log. If not specified, LADXR_[seed].txt/json is used.") + parser.add_argument('--test', dest="test", action="store_true", + help="Test the logic of the given rom, without showing anything.") + parser.add_argument('--romdebugmode', dest="romdebugmode", action="store_true", + help="Patch the rom so that debug mode is enabled, this creates a default save with most items and unlocks some debug features.") + parser.add_argument('--exportmap', dest="exportmap", action="store_true", + help="Export the map (many graphical mistakes)") + parser.add_argument('--emptyplan', dest="emptyplan", type=str, required=False, + help="Write an unfilled plan file") + parser.add_argument('--timeout', type=float, required=False, + help="Timeout generating the seed after the specified number of seconds") + parser.add_argument('--logdirectory', dest="log_directory", type=str, required=False, + help="Directory to write the JSON log file. Generated independently from the spoiler log and omitted by default.") + + parser.add_argument('-s', '--setting', dest="settings", action="append", required=False, + help="Set a configuration setting for rom generation") + parser.add_argument('--short', dest="shortsettings", type=str, required=False, + help="Set a configuration setting for rom generation") + parser.add_argument('--settingjson', dest="settingjson", action="store_true", + help="Dump a json blob which describes all settings") + + parser.add_argument('--plan', dest="plan", metavar='plandomizer', type=str, required=False, + help="Read an item placement plan") + parser.add_argument('--multiworld', dest="multiworld", action="append", required=False, + help="Set configuration for a multiworld player, supply multiple times for settings per player, requires a short setting string per player.") + parser.add_argument('--doubletrouble', dest="doubletrouble", action="store_true", + help="Warning, bugged in various ways") + parser.add_argument('--pymod', dest="pymod", action='append', + help="Load python code mods.") + + return parser + diff --git a/worlds/ladx/LADXR/mapgen/__init__.py b/worlds/ladx/LADXR/mapgen/__init__.py new file mode 100644 index 000000000000..d38c27fbddaa --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/__init__.py @@ -0,0 +1,147 @@ +from ..romTables import ROMWithTables +from ..roomEditor import RoomEditor, ObjectWarp +from ..patches import overworld, core +from .tileset import loadTileInfo +from .map import Map, MazeGen +from .wfc import WFCMap, ContradictionException +from .roomgen import setup_room_types +from .imagegenerator import ImageGen +from .util import xyrange +from .locations.entrance import DummyEntrance +from .locationgen import LocationGenerator +from .logic import LogicGenerator +from .enemygen import generate_enemies +from ..assembler import ASM + + +def store_map(rom, the_map: Map): + # Move all exceptions to room FF + # Dig seashells + rom.patch(0x03, 0x220F, ASM("cp $DA"), ASM("cp $FF")) + rom.patch(0x03, 0x2213, ASM("cp $A5"), ASM("cp $FF")) + rom.patch(0x03, 0x2217, ASM("cp $74"), ASM("cp $FF")) + rom.patch(0x03, 0x221B, ASM("cp $3A"), ASM("cp $FF")) + rom.patch(0x03, 0x221F, ASM("cp $A8"), ASM("cp $FF")) + rom.patch(0x03, 0x2223, ASM("cp $B2"), ASM("cp $FF")) + # Force tile 04 under bushes and rocks, instead of conditionally tile 3, else seashells won't spawn. + rom.patch(0x14, 0x1655, 0x1677, "", fill_nop=True) + # Bonk trees + rom.patch(0x03, 0x0F03, ASM("cp $A4"), ASM("cp $FF")) + rom.patch(0x03, 0x0F07, ASM("cp $D2"), ASM("cp $FF")) + # Stairs under rocks + rom.patch(0x14, 0x1638, ASM("cp $52"), ASM("cp $FF")) + rom.patch(0x14, 0x163C, ASM("cp $04"), ASM("cp $FF")) + + # Patch D6 raft game exit, just remove the exit. + re = RoomEditor(rom, 0x1B0) + re.removeObject(7, 0) + re.store(rom) + # Patch D8 back entrance, remove the outside part + re = RoomEditor(rom, 0x23A) + re.objects = [obj for obj in re.objects if not isinstance(obj, ObjectWarp)] + [ObjectWarp(1, 7, 0x23D, 0x58, 0x10)] + re.store(rom) + re = RoomEditor(rom, 0x23D) + re.objects = [obj for obj in re.objects if not isinstance(obj, ObjectWarp)] + [ObjectWarp(1, 7, 0x23A, 0x58, 0x10)] + re.store(rom) + + for room in the_map: + for location in room.locations: + location.prepare(rom) + for n in range(0x00, 0x100): + sx = n & 0x0F + sy = ((n >> 4) & 0x0F) + if sx < the_map.w and sy < the_map.h: + tiles = the_map.get(sx, sy).tiles + else: + tiles = [4] * 80 + tiles[44] = 0xC6 + + re = RoomEditor(rom, n) + # tiles = re.getTileArray() + re.objects = [] + re.entities = [] + room = the_map.get(sx, sy) if sx < the_map.w and sy < the_map.h else None + + tileset = the_map.tilesets[room.tileset_id] if room else None + rom.banks[0x3F][0x3F00 + n] = tileset.main_id if tileset else 0x0F + rom.banks[0x21][0x02EF + n] = tileset.palette_id if tileset and tileset.palette_id is not None else 0x03 + rom.banks[0x1A][0x2476 + n] = tileset.attr_bank if tileset and tileset.attr_bank else 0x22 + rom.banks[0x1A][0x1E76 + n * 2] = (tileset.attr_addr & 0xFF) if tileset and tileset.attr_addr else 0x00 + rom.banks[0x1A][0x1E77 + n * 2] = (tileset.attr_addr >> 8) if tileset and tileset.attr_addr else 0x60 + re.animation_id = tileset.animation_id if tileset and tileset.animation_id is not None else 0x03 + + re.buildObjectList(tiles) + if room: + for idx, tile_id in enumerate(tiles): + if tile_id == 0x61: # Fix issues with the well being used as chimney as well and causing wrong warps + DummyEntrance(room, idx % 10, idx // 10) + re.entities += room.entities + room.locations.sort(key=lambda loc: (loc.y, loc.x, id(loc))) + for location in room.locations: + location.update_room(rom, re) + else: + re.objects.append(ObjectWarp(0x01, 0x10, 0x2A3, 0x50, 0x7C)) + re.store(rom) + + rom.banks[0x21][0x00BF:0x00BF+3] = [0, 0, 0] # Patch out the "load palette on screen transition" exception code. + + # Fix some tile attribute issues + def change_attr(tileset, index, a, b, c, d): + rom.banks[the_map.tilesets[tileset].attr_bank][the_map.tilesets[tileset].attr_addr - 0x4000 + index * 4 + 0] = a + rom.banks[the_map.tilesets[tileset].attr_bank][the_map.tilesets[tileset].attr_addr - 0x4000 + index * 4 + 1] = b + rom.banks[the_map.tilesets[tileset].attr_bank][the_map.tilesets[tileset].attr_addr - 0x4000 + index * 4 + 2] = c + rom.banks[the_map.tilesets[tileset].attr_bank][the_map.tilesets[tileset].attr_addr - 0x4000 + index * 4 + 3] = d + change_attr("mountains", 0x04, 6, 6, 6, 6) + change_attr("mountains", 0x27, 6, 6, 3, 3) + change_attr("mountains", 0x28, 6, 6, 3, 3) + change_attr("mountains", 0x6E, 1, 1, 1, 1) + change_attr("town", 0x59, 2, 2, 2, 2) # Roof tile wrong color + + +def generate(rom_filename, w, h): + rom = ROMWithTables(rom_filename) + overworld.patchOverworldTilesets(rom) + core.cleanup(rom) + tilesets = loadTileInfo(rom) + + the_map = Map(w, h, tilesets) + setup_room_types(the_map) + + MazeGen(the_map) + imggen = ImageGen(tilesets, the_map, rom) + imggen.enabled = False + wfcmap = WFCMap(the_map, tilesets) #, step_callback=imggen.on_step) + try: + wfcmap.initialize() + except ContradictionException as e: + print(f"Failed on setup {e.x // 10} {e.y // 8} {e.x % 10} {e.y % 8}") + imggen.on_step(wfcmap, err=(e.x, e.y)) + return + imggen.on_step(wfcmap) + for x, y in xyrange(w, h): + for n in range(50): + try: + wfcmap.build(x * 10, y * 8, 10, 8) + imggen.on_step(wfcmap) + break + except ContradictionException as e: + print(f"Failed {x} {y} {e.x%10} {e.y%8} {n}") + imggen.on_step(wfcmap, err=(e.x, e.y)) + wfcmap.clear() + if n == 49: + raise RuntimeError("Failed to fill chunk") + print(f"Done {x} {y}") + imggen.on_step(wfcmap) + wfcmap.store_tile_data(the_map) + + LocationGenerator(the_map) + + for room in the_map: + generate_enemies(room) + + if imggen.enabled: + store_map(rom, the_map) + from mapexport import MapExport + MapExport(rom).export_all(w, h, dungeons=False) + rom.save("test.gbc") + return the_map diff --git a/worlds/ladx/LADXR/mapgen/enemygen.py b/worlds/ladx/LADXR/mapgen/enemygen.py new file mode 100644 index 000000000000..45020b93a998 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/enemygen.py @@ -0,0 +1,59 @@ +from .tileset import walkable_tiles, entrance_tiles +import random + + +ENEMIES = { + "mountains": [ + (0x0B,), + (0x0E,), + (0x29,), + (0x0E, 0x0E), + (0x0E, 0x0E, 0x23), + (0x0D,), (0x0D, 0x0D), + ], + "egg": [], + "basic": [ + (), (), (), (), (), (), + (0x09,), (0x09, 0x09), # octorock + (0x9B, 0x9B), (0x9B, 0x9B, 0x1B), # slimes + (0xBB, 0x9B), # bush crawler + slime + (0xB9,), + (0x0B, 0x23), # likelike + moblin + (0x14, 0x0B, 0x0B), # moblins + sword + (0x0B, 0x23, 0x23), # likelike + moblin + (0xAE, 0xAE), # flying octorock + (0xBA, ), # Bomber + (0x0D, 0x0D), (0x0D, ), + ], + "town": [ + (), (), (0x6C, 0x6E), (0x6E,), (0x6E, 0x6E), + ], + "forest": [ + (0x0B,), # moblins + (0x0B, 0x0B), # moblins + (0x14, 0x0B, 0x0B), # moblins + sword + ], + "beach": [ + (0xC6, 0xC6), + (0x0E, 0x0E, 0xC6), + (0x0E, 0x0E, 0x09), + ], + "water": [], +} + + +def generate_enemies(room): + options = ENEMIES[room.tileset_id] + if not options: + return + positions = [] + for y in range(1, 7): + for x in range(1, 9): + if room.tiles[x + y * 10] in walkable_tiles and room.tiles[x + (y - 1) * 10] not in entrance_tiles: + positions.append((x, y)) + for type_id in random.choice(options): + if not positions: + return + x, y = random.choice(positions) + positions.remove((x, y)) + room.entities.append((x, y, type_id)) diff --git a/worlds/ladx/LADXR/mapgen/imagegenerator.py b/worlds/ladx/LADXR/mapgen/imagegenerator.py new file mode 100644 index 000000000000..fc9d5bbeee6b --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/imagegenerator.py @@ -0,0 +1,95 @@ +from .tileset import open_tiles, solid_tiles + + +def tx(x): + return x * 16 + x // 10 + + +def ty(y): + return y * 16 + y // 8 + + +class ImageGen: + def __init__(self, tilesets, the_map, rom): + self.tilesets = tilesets + self.map = the_map + self.rom = rom + self.image = None + self.draw = None + self.count = 0 + self.enabled = False + self.__tile_cache = {} + + def on_step(self, wfc, cur=None, err=None): + if not self.enabled: + return + if self.image is None: + import PIL.Image + import PIL.ImageDraw + self.image = PIL.Image.new("RGB", (self.map.w * 161, self.map.h * 129)) + self.draw = PIL.ImageDraw.Draw(self.image) + self.image.paste(0, (0, 0, wfc.w * 16, wfc.h * 16)) + for y in range(wfc.h): + for x in range(wfc.w): + cell = wfc.cell_data[(x, y)] + if len(cell.options) == 1: + tile_id = next(iter(cell.options)) + room = self.map.get(x//10, y//8) + tile = self.get_tile(room.tileset_id, tile_id) + self.image.paste(tile, (tx(x), ty(y))) + else: + self.draw.text((tx(x) + 3, ty(y) + 3), f"{len(cell.options):2}", (255, 255, 255)) + if cell.options.issubset(open_tiles): + self.draw.rectangle((tx(x), ty(y), tx(x) + 15, ty(y) + 15), outline=(0, 128, 0)) + elif cell.options.issubset(solid_tiles): + self.draw.rectangle((tx(x), ty(y), tx(x) + 15, ty(y) + 15), outline=(0, 0, 192)) + if cur: + self.draw.rectangle((tx(cur[0]),ty(cur[1]),tx(cur[0])+15,ty(cur[1])+15), outline=(0, 255, 0)) + if err: + self.draw.rectangle((tx(err[0]),ty(err[1]),tx(err[0])+15,ty(err[1])+15), outline=(255, 0, 0)) + self.image.save(f"_map/tmp{self.count:08}.png") + self.count += 1 + + def get_tile(self, tileset_id, tile_id): + tile = self.__tile_cache.get((tileset_id, tile_id), None) + if tile is not None: + return tile + import PIL.Image + tile = PIL.Image.new("L", (16, 16)) + tileset = self.get_tileset(tileset_id) + metatile = self.rom.banks[0x1A][0x2749 + tile_id * 4:0x2749 + tile_id * 4+4] + + def draw(ox, oy, t): + addr = (t & 0x3FF) << 4 + tile_data = self.rom.banks[t >> 10][addr:addr+0x10] + for y in range(8): + a = tile_data[y * 2] + b = tile_data[y * 2 + 1] + for x in range(8): + v = 0 + bit = 0x80 >> x + if a & bit: + v |= 0x01 + if b & bit: + v |= 0x02 + tile.putpixel((ox+x,oy+y), (255, 192, 128, 32)[v]) + draw(0, 0, tileset[metatile[0]]) + draw(8, 0, tileset[metatile[1]]) + draw(0, 8, tileset[metatile[2]]) + draw(8, 8, tileset[metatile[3]]) + self.__tile_cache[(tileset_id, tile_id)] = tile + return tile + + def get_tileset(self, tileset_id): + subtiles = [0] * 0x100 + for n in range(0, 0x20): + subtiles[n] = (0x0F << 10) + (self.tilesets[tileset_id].main_id << 4) + n + for n in range(0x20, 0x80): + subtiles[n] = (0x0C << 10) + 0x100 + n + for n in range(0x80, 0x100): + subtiles[n] = (0x0C << 10) + n + + addr = (0x000, 0x000, 0x2B0, 0x2C0, 0x2D0, 0x2E0, 0x2F0, 0x2D0, 0x300, 0x310, 0x320, 0x2A0, 0x330, 0x350, 0x360, 0x340, 0x370)[self.tilesets[tileset_id].animation_id or 3] + for n in range(0x6C, 0x70): + subtiles[n] = (0x0C << 10) + addr + n - 0x6C + return subtiles diff --git a/worlds/ladx/LADXR/mapgen/locationgen.py b/worlds/ladx/LADXR/mapgen/locationgen.py new file mode 100644 index 000000000000..0a30d80bd53f --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/locationgen.py @@ -0,0 +1,203 @@ +from .tileset import entrance_tiles, solid_tiles, walkable_tiles +from .map import Map +from .util import xyrange +from .locations.entrance import Entrance +from .locations.chest import Chest, FloorItem +from .locations.seashell import HiddenSeashell, DigSeashell, BonkSeashell +import random +from typing import List + +all_location_constructors = (Chest, FloorItem, HiddenSeashell, DigSeashell, BonkSeashell) + + +def remove_duplicate_tile(tiles, to_find): + try: + idx0 = tiles.index(to_find) + idx1 = tiles.index(to_find, idx0 + 1) + tiles[idx1] = 0x04 + except ValueError: + return + + +class Dijkstra: + def __init__(self, the_map: Map): + self.map = the_map + self.w = the_map.w * 10 + self.h = the_map.h * 8 + self.area = [-1] * (self.w * self.h) + self.distance = [0] * (self.w * self.h) + self.area_size = [] + self.next_area_id = 0 + + def fill(self, start_x, start_y): + size = 0 + todo = [(start_x, start_y, 0)] + while todo: + x, y, distance = todo.pop(0) + room = self.map.get(x // 10, y // 8) + tile_idx = (x % 10) + (y % 8) * 10 + area_idx = x + y * self.w + if room.tiles[tile_idx] not in solid_tiles and self.area[area_idx] == -1: + size += 1 + self.area[area_idx] = self.next_area_id + self.distance[area_idx] = distance + todo += [(x - 1, y, distance + 1), (x + 1, y, distance + 1), (x, y - 1, distance + 1), (x, y + 1, distance + 1)] + self.next_area_id += 1 + self.area_size.append(size) + return self.next_area_id - 1 + + def dump(self): + print(self.area_size) + for y in range(self.map.h * 8): + for x in range(self.map.w * 10): + n = self.area[x + y * self.map.w * 10] + if n < 0: + print(' ', end='') + else: + print(n, end='') + print() + + +class EntranceInfo: + def __init__(self, room, x, y): + self.room = room + self.x = x + self.y = y + self.tile = room.tiles[x + y * 10] + + @property + def map_x(self): + return self.room.x * 10 + self.x + + @property + def map_y(self): + return self.room.y * 8 + self.y + + +class LocationGenerator: + def __init__(self, the_map: Map): + # Find all entrances + entrances: List[EntranceInfo] = [] + for room in the_map: + # Prevent more then one chest or hole-entrance per map + remove_duplicate_tile(room.tiles, 0xA0) + remove_duplicate_tile(room.tiles, 0xC6) + for x, y in xyrange(10, 8): + if room.tiles[x + y * 10] in entrance_tiles: + entrances.append(EntranceInfo(room, x, y)) + if room.tiles[x + y * 10] == 0xA0: + Chest(room, x, y) + todo_entrances = entrances.copy() + + # Find a place to put the start position + start_entrances = [info for info in todo_entrances if info.room.tileset_id == "town"] + if not start_entrances: + start_entrances = entrances + start_entrance = random.choice(start_entrances) + todo_entrances.remove(start_entrance) + + # Setup the start position and fill the basic dijkstra flood fill from there. + Entrance(start_entrance.room, start_entrance.x, start_entrance.y, "start_house") + reachable_map = Dijkstra(the_map) + reachable_map.fill(start_entrance.map_x, start_entrance.map_y) + + # Find each entrance that is not reachable from any other spot, and flood fill from that entrance + for info in entrances: + if reachable_map.area[info.map_x + info.map_y * reachable_map.w] == -1: + reachable_map.fill(info.map_x, info.map_y) + + disabled_entrances = ["boomerang_cave", "seashell_mansion"] + house_entrances = ["rooster_house", "writes_house", "photo_house", "raft_house", "crazy_tracy", "witch", "dream_hut", "shop", "madambowwow", "kennel", "library", "ulrira", "trendy_shop", "armos_temple", "banana_seller", "ghost_house", "animal_house1", "animal_house2", "animal_house3", "animal_house4", "animal_house5"] + cave_entrances = ["madbatter_taltal", "bird_cave", "right_fairy", "moblin_cave", "hookshot_cave", "forest_madbatter", "castle_jump_cave", "rooster_grave", "prairie_left_cave1", "prairie_left_cave2", "prairie_left_fairy", "mamu", "armos_fairy", "armos_maze_cave", "prairie_madbatter", "animal_cave", "desert_cave"] + water_entrances = ["mambo", "heartpiece_swim_cave"] + phone_entrances = ["phone_d8", "writes_phone", "castle_phone", "mabe_phone", "prairie_left_phone", "prairie_right_phone", "prairie_low_phone", "animal_phone"] + dungeon_entrances = ["d7", "d8", "d6", "d5", "d4", "d3", "d2", "d1", "d0"] + connector_entrances = [("fire_cave_entrance", "fire_cave_exit"), ("left_to_right_taltalentrance", "left_taltal_entrance"), ("obstacle_cave_entrance", "obstacle_cave_outside_chest", "obstacle_cave_exit"), ("papahl_entrance", "papahl_exit"), ("multichest_left", "multichest_right", "multichest_top"), ("right_taltal_connector1", "right_taltal_connector2"), ("right_taltal_connector3", "right_taltal_connector4"), ("right_taltal_connector5", "right_taltal_connector6"), ("writes_cave_left", "writes_cave_right"), ("raft_return_enter", "raft_return_exit"), ("toadstool_entrance", "toadstool_exit"), ("graveyard_cave_left", "graveyard_cave_right"), ("castle_main_entrance", "castle_upper_left", "castle_upper_right"), ("castle_secret_entrance", "castle_secret_exit"), ("papahl_house_left", "papahl_house_right"), ("prairie_right_cave_top", "prairie_right_cave_bottom", "prairie_right_cave_high"), ("prairie_to_animal_connector", "animal_to_prairie_connector"), ("d6_connector_entrance", "d6_connector_exit"), ("richard_house", "richard_maze"), ("prairie_madbatter_connector_entrance", "prairie_madbatter_connector_exit")] + + # For each area that is not yet reachable from the start area: + # add a connector cave from a reachable area to this new area. + reachable_areas = [0] + unreachable_areas = list(range(1, reachable_map.next_area_id)) + retry_count = 10000 + while unreachable_areas: + source = random.choice(reachable_areas) + target = random.choice(unreachable_areas) + + source_entrances = [info for info in todo_entrances if reachable_map.area[info.map_x + info.map_y * reachable_map.w] == source] + target_entrances = [info for info in todo_entrances if reachable_map.area[info.map_x + info.map_y * reachable_map.w] == target] + if not source_entrances: + retry_count -= 1 + if retry_count < 1: + raise RuntimeError("Failed to add connectors...") + continue + + source_info = random.choice(source_entrances) + target_info = random.choice(target_entrances) + + connector = random.choice(connector_entrances) + connector_entrances.remove(connector) + Entrance(source_info.room, source_info.x, source_info.y, connector[0]) + todo_entrances.remove(source_info) + Entrance(target_info.room, target_info.x, target_info.y, connector[1]) + todo_entrances.remove(target_info) + + for extra_exit in connector[2:]: + info = random.choice(todo_entrances) + todo_entrances.remove(info) + Entrance(info.room, info.x, info.y, extra_exit) + + unreachable_areas.remove(target) + reachable_areas.append(target) + + # Find areas that only have a single entrance, and try to force something in there. + # As else we have useless dead ends, and that is no fun. + for area_id in range(reachable_map.next_area_id): + area_entrances = [info for info in entrances if reachable_map.area[info.map_x + info.map_y * reachable_map.w] == area_id] + if len(area_entrances) != 1: + continue + cells = [] + for y in range(reachable_map.h): + for x in range(reachable_map.w): + if reachable_map.area[x + y * reachable_map.w] == area_id: + if the_map.get(x // 10, y // 8).tiles[(x % 10) + (y % 8) * 10] in walkable_tiles: + cells.append((reachable_map.distance[x + y * reachable_map.w], x, y)) + cells.sort(reverse=True) + d, x, y = random.choice(cells[:10]) + FloorItem(the_map.get(x // 10, y // 8), x % 10, y % 8) + + # Find potential dungeon entrances + # Assign some dungeons + for n in range(4): + if not todo_entrances: + break + info = random.choice(todo_entrances) + todo_entrances.remove(info) + dungeon = random.choice(dungeon_entrances) + dungeon_entrances.remove(dungeon) + Entrance(info.room, info.x, info.y, dungeon) + + # Assign something to all other entrances + for info in todo_entrances: + options = house_entrances if info.tile == 0xE2 else cave_entrances + entrance = random.choice(options) + options.remove(entrance) + Entrance(info.room, info.x, info.y, entrance) + + # Go over each room, and assign something if nothing is assigned yet + todo_list = [room for room in the_map if not room.locations] + random.shuffle(todo_list) + done_count = {} + for room in todo_list: + options = [] + # figure out what things could potentially be placed here + for constructor in all_location_constructors: + if done_count.get(constructor, 0) >= constructor.MAX_COUNT: + continue + xy = constructor.check_possible(room, reachable_map) + if xy is not None: + options.append((*xy, constructor)) + + if options: + x, y, constructor = random.choice(options) + constructor(room, x, y) + done_count[constructor] = done_count.get(constructor, 0) + 1 diff --git a/worlds/ladx/LADXR/mapgen/locations/base.py b/worlds/ladx/LADXR/mapgen/locations/base.py new file mode 100644 index 000000000000..a6526193fca8 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/locations/base.py @@ -0,0 +1,24 @@ +from ...roomEditor import RoomEditor +from ..map import RoomInfo + + +class LocationBase: + MAX_COUNT = 9999 + + def __init__(self, room: RoomInfo, x, y): + self.room = room + self.x = x + self.y = y + room.locations.append(self) + + def prepare(self, rom): + pass + + def update_room(self, rom, re: RoomEditor): + pass + + def connect_logic(self, logic_location): + raise NotImplementedError(self.__class__) + + def get_item_pool(self): + raise NotImplementedError(self.__class__) diff --git a/worlds/ladx/LADXR/mapgen/locations/chest.py b/worlds/ladx/LADXR/mapgen/locations/chest.py new file mode 100644 index 000000000000..4cfeb0bc6b5d --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/locations/chest.py @@ -0,0 +1,73 @@ +from .base import LocationBase +from ..tileset import solid_tiles, open_tiles, walkable_tiles +from ...roomEditor import RoomEditor +from ...locations.all import HeartPiece, Chest as ChestLocation +import random + + +class Chest(LocationBase): + def __init__(self, room, x, y): + super().__init__(room, x, y) + room.tiles[x + y * 10] = 0xA0 + + def connect_logic(self, logic_location): + logic_location.add(ChestLocation(self.room.x + self.room.y * 16)) + + def get_item_pool(self): + return {None: 1} + + @staticmethod + def check_possible(room, reachable_map): + # Check if we can potentially place a chest here, and what the best spot would be. + options = [] + for y in range(1, 6): + for x in range(1, 9): + if room.tiles[x + y * 10 - 10] not in solid_tiles: # Chest needs to be against a "wall" at the top + continue + if room.tiles[x + y * 10] not in walkable_tiles or room.tiles[x + y * 10 + 10] not in walkable_tiles: + continue + if room.tiles[x - 1 + y * 10] not in solid_tiles and room.tiles[x - 1 + y * 10 + 10] not in open_tiles: + continue + if room.tiles[x + 1 + y * 10] not in solid_tiles and room.tiles[x + 1 + y * 10 + 10] not in open_tiles: + continue + idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w + if reachable_map.area[idx] == -1: + continue + options.append((reachable_map.distance[idx], x, y)) + if not options: + return None + options.sort(reverse=True) + options = [(x, y) for d, x, y in options if d > options[0][0] - 4] + return random.choice(options) + + +class FloorItem(LocationBase): + def __init__(self, room, x, y): + super().__init__(room, x, y) + + def update_room(self, rom, re: RoomEditor): + re.entities.append((self.x, self.y, 0x35)) + + def connect_logic(self, logic_location): + logic_location.add(HeartPiece(self.room.x + self.room.y * 16)) + + def get_item_pool(self): + return {None: 1} + + @staticmethod + def check_possible(room, reachable_map): + # Check if we can potentially place a floor item here, and what the best spot would be. + options = [] + for y in range(1, 7): + for x in range(1, 9): + if room.tiles[x + y * 10] not in walkable_tiles: + continue + idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w + if reachable_map.area[idx] == -1: + continue + options.append((reachable_map.distance[idx], x, y)) + if not options: + return None + options.sort(reverse=True) + options = [(x, y) for d, x, y in options if d > options[0][0] - 4] + return random.choice(options) diff --git a/worlds/ladx/LADXR/mapgen/locations/entrance.py b/worlds/ladx/LADXR/mapgen/locations/entrance.py new file mode 100644 index 000000000000..be4dde6634fa --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/locations/entrance.py @@ -0,0 +1,107 @@ +from ...locations.items import BOMB +from .base import LocationBase +from ...roomEditor import RoomEditor, Object, ObjectWarp +from ...entranceInfo import ENTRANCE_INFO +from ...assembler import ASM +from .entrance_info import INFO + + +class Entrance(LocationBase): + def __init__(self, room, x, y, entrance_name): + super().__init__(room, x, y) + self.entrance_name = entrance_name + self.entrance_info = ENTRANCE_INFO[entrance_name] + self.source_warp = None + self.target_warp_idx = None + + self.inside_logic = None + + def prepare(self, rom): + info = self.entrance_info + re = RoomEditor(rom, info.alt_room if info.alt_room is not None else info.room) + self.source_warp = re.getWarps()[info.index if info.index not in (None, "all") else 0] + re = RoomEditor(rom, self.source_warp.room) + for idx, warp in enumerate(re.getWarps()): + if warp.room == info.room or warp.room == info.alt_room: + self.target_warp_idx = idx + + def update_room(self, rom, re: RoomEditor): + re.objects.append(self.source_warp) + + target = RoomEditor(rom, self.source_warp.room) + warp = target.getWarps()[self.target_warp_idx] + warp.room = self.room.x | (self.room.y << 4) + warp.target_x = self.x * 16 + 8 + warp.target_y = self.y * 16 + 18 + target.store(rom) + + def prepare_logic(self, configuration_options, world_setup, requirements_settings): + if self.entrance_name in INFO and INFO[self.entrance_name].logic is not None: + self.inside_logic = INFO[self.entrance_name].logic(configuration_options, world_setup, requirements_settings) + + def connect_logic(self, logic_location): + if self.entrance_name not in INFO: + raise RuntimeError(f"WARNING: Logic connection to entrance unmapped! {self.entrance_name}") + if self.inside_logic: + req = None + if self.room.tiles[self.x + self.y * 10] == 0xBA: + req = BOMB + logic_location.connect(self.inside_logic, req) + if INFO[self.entrance_name].exits: + return [(name, logic(logic_location)) for name, logic in INFO[self.entrance_name].exits] + return None + + def get_item_pool(self): + if self.entrance_name not in INFO: + return {} + return INFO[self.entrance_name].items or {} + + +class DummyEntrance(LocationBase): + def __init__(self, room, x, y): + super().__init__(room, x, y) + + def update_room(self, rom, re: RoomEditor): + re.objects.append(ObjectWarp(0x01, 0x10, 0x2A3, 0x50, 0x7C)) + + def connect_logic(self, logic_location): + return + + def get_item_pool(self): + return {} + + +class EggEntrance(LocationBase): + def __init__(self, room, x, y): + super().__init__(room, x, y) + + def update_room(self, rom, re: RoomEditor): + # Setup the warps + re.objects.insert(0, Object(5, 3, 0xE1)) # Hide an entrance tile under the tile where the egg will open. + re.objects.append(ObjectWarp(0x01, 0x08, 0x270, 0x50, 0x7C)) + re.entities.append((0, 0, 0xDE)) # egg song event + + egg_inside = RoomEditor(rom, 0x270) + egg_inside.getWarps()[0].room = self.room.x + egg_inside.store(rom) + + # Fix the alt room layout + alt = RoomEditor(rom, "Alt06") + tiles = re.getTileArray() + tiles[25] = 0xC1 + tiles[35] = 0xCB + alt.buildObjectList(tiles, reduce_size=True) + alt.store(rom) + + # Patch which room shows as Alt06 + rom.patch(0x00, 0x31F1, ASM("cp $06"), ASM(f"cp ${self.room.x:02x}")) + rom.patch(0x00, 0x31F5, ASM("ld a, [$D806]"), ASM(f"ld a, [${0xD800 + self.room.x:04x}]")) + rom.patch(0x20, 0x2DE6, ASM("cp $06"), ASM(f"cp ${self.room.x:02x}")) + rom.patch(0x20, 0x2DEA, ASM("ld a, [$D806]"), ASM(f"ld a, [${0xD800 + self.room.x:04x}]")) + rom.patch(0x19, 0x0D1A, ASM("ld hl, $D806"), ASM(f"ld hl, ${0xD800 + self.room.x:04x}")) + + def connect_logic(self, logic_location): + return + + def get_item_pool(self): + return {} diff --git a/worlds/ladx/LADXR/mapgen/locations/entrance_info.py b/worlds/ladx/LADXR/mapgen/locations/entrance_info.py new file mode 100644 index 000000000000..9de2b8610170 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/locations/entrance_info.py @@ -0,0 +1,341 @@ +from ...locations.birdKey import BirdKey +from ...locations.chest import Chest +from ...locations.faceKey import FaceKey +from ...locations.goldLeaf import GoldLeaf +from ...locations.heartPiece import HeartPiece +from ...locations.madBatter import MadBatter +from ...locations.song import Song +from ...locations.startItem import StartItem +from ...locations.tradeSequence import TradeSequenceItem +from ...locations.seashell import Seashell +from ...locations.shop import ShopItem +from ...locations.droppedKey import DroppedKey +from ...locations.witch import Witch +from ...logic import * +from ...logic.dungeon1 import Dungeon1 +from ...logic.dungeon2 import Dungeon2 +from ...logic.dungeon3 import Dungeon3 +from ...logic.dungeon4 import Dungeon4 +from ...logic.dungeon5 import Dungeon5 +from ...logic.dungeon6 import Dungeon6 +from ...logic.dungeon7 import Dungeon7 +from ...logic.dungeon8 import Dungeon8 +from ...logic.dungeonColor import DungeonColor + + +def one_way(loc, req=None): + res = Location() + loc.connect(res, req, one_way=True) + return res + + +class EntranceInfo: + def __init__(self, *, items=None, logic=None, exits=None): + self.items = items + self.logic = logic + self.exits = exits + + +INFO = { + "start_house": EntranceInfo(items={None: 1}, logic=lambda c, w, r: Location().add(StartItem())), + "d0": EntranceInfo( + items={None: 2, KEY9: 3, MAP9: 1, COMPASS9: 1, STONE_BEAK9: 1, NIGHTMARE_KEY9: 1}, + logic=lambda c, w, r: DungeonColor(c, w, r).entrance + ), + "d1": EntranceInfo( + items={None: 3, KEY1: 3, MAP1: 1, COMPASS1: 1, STONE_BEAK1: 1, NIGHTMARE_KEY1: 1, HEART_CONTAINER: 1, INSTRUMENT1: 1}, + logic=lambda c, w, r: Dungeon1(c, w, r).entrance + ), + "d2": EntranceInfo( + items={None: 3, KEY2: 5, MAP2: 1, COMPASS2: 1, STONE_BEAK2: 1, NIGHTMARE_KEY2: 1, HEART_CONTAINER: 1, INSTRUMENT2: 1}, + logic=lambda c, w, r: Dungeon2(c, w, r).entrance + ), + "d3": EntranceInfo( + items={None: 4, KEY3: 9, MAP3: 1, COMPASS3: 1, STONE_BEAK3: 1, NIGHTMARE_KEY3: 1, HEART_CONTAINER: 1, INSTRUMENT3: 1}, + logic=lambda c, w, r: Dungeon3(c, w, r).entrance + ), + "d4": EntranceInfo( + items={None: 4, KEY4: 5, MAP4: 1, COMPASS4: 1, STONE_BEAK4: 1, NIGHTMARE_KEY4: 1, HEART_CONTAINER: 1, INSTRUMENT4: 1}, + logic=lambda c, w, r: Dungeon4(c, w, r).entrance + ), + "d5": EntranceInfo( + items={None: 5, KEY5: 3, MAP5: 1, COMPASS5: 1, STONE_BEAK5: 1, NIGHTMARE_KEY5: 1, HEART_CONTAINER: 1, INSTRUMENT5: 1}, + logic=lambda c, w, r: Dungeon5(c, w, r).entrance + ), + "d6": EntranceInfo( + items={None: 6, KEY6: 3, MAP6: 1, COMPASS6: 1, STONE_BEAK6: 1, NIGHTMARE_KEY6: 1, HEART_CONTAINER: 1, INSTRUMENT6: 1}, + logic=lambda c, w, r: Dungeon6(c, w, r, raft_game_chest=False).entrance + ), + "d7": EntranceInfo( + items={None: 4, KEY7: 3, MAP7: 1, COMPASS7: 1, STONE_BEAK7: 1, NIGHTMARE_KEY7: 1, HEART_CONTAINER: 1, INSTRUMENT7: 1}, + logic=lambda c, w, r: Dungeon7(c, w, r).entrance + ), + "d8": EntranceInfo( + items={None: 6, KEY8: 7, MAP8: 1, COMPASS8: 1, STONE_BEAK8: 1, NIGHTMARE_KEY8: 1, HEART_CONTAINER: 1, INSTRUMENT8: 1}, + logic=lambda c, w, r: Dungeon8(c, w, r, back_entrance_heartpiece=False).entrance + ), + + "writes_cave_left": EntranceInfo( + items={None: 2}, + logic=lambda c, w, r: Location().connect( + Location().add(Chest(0x2AE)), OR(FEATHER, ROOSTER, HOOKSHOT) + ).connect( + Location().add(Chest(0x2AF)), POWER_BRACELET + ), + exits=[("writes_cave_right", lambda loc: loc)], + ), + "writes_cave_right": EntranceInfo(), + + "castle_main_entrance": EntranceInfo( + items={None: 2}, + logic=lambda c, w, r: Location().connect( + Location().add(GoldLeaf(0x2D2)), r.attack_hookshot_powder # in the castle, kill enemies + ).connect( + Location().add(GoldLeaf(0x2C5)), AND(BOMB, r.attack_hookshot_powder) # in the castle, bomb wall to show enemy + ), + exits=[("castle_upper_left", lambda loc: loc)], + ), + "castle_upper_left": EntranceInfo(), + + "castle_upper_right": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(GoldLeaf(0x2C6)), AND(POWER_BRACELET, r.attack_hookshot)), + ), + + "right_taltal_connector1": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("right_taltal_connector2", lambda loc: loc)], + ), + "right_taltal_connector2": EntranceInfo(), + + "fire_cave_entrance": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("fire_cave_exit", lambda loc: Location().connect(loc, COUNT(SHIELD, 2)))], + ), + "fire_cave_exit": EntranceInfo(), + + "graveyard_cave_left": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(HeartPiece(0x2DF)), OR(AND(BOMB, OR(HOOKSHOT, PEGASUS_BOOTS), FEATHER), ROOSTER)), + exits=[("graveyard_cave_right", lambda loc: Location().connect(loc, OR(FEATHER, ROOSTER)))], + ), + "graveyard_cave_right": EntranceInfo(), + + "raft_return_enter": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("raft_return_exit", one_way)], + ), + "raft_return_exit": EntranceInfo(), + + "prairie_right_cave_top": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("prairie_right_cave_bottom", lambda loc: loc), ("prairie_right_cave_high", lambda loc: Location().connect(loc, AND(BOMB, OR(FEATHER, ROOSTER))))], + ), + "prairie_right_cave_bottom": EntranceInfo(), + "prairie_right_cave_high": EntranceInfo(), + + "armos_maze_cave": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().add(Chest(0x2FC)), + ), + "right_taltal_connector3": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("right_taltal_connector4", lambda loc: one_way(loc, AND(OR(FEATHER, ROOSTER), HOOKSHOT)))], + ), + "right_taltal_connector4": EntranceInfo(), + + "obstacle_cave_entrance": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2BB)), AND(SWORD, OR(HOOKSHOT, ROOSTER))), + exits=[ + ("obstacle_cave_outside_chest", lambda loc: Location().connect(loc, SWORD)), + ("obstacle_cave_exit", lambda loc: Location().connect(loc, AND(SWORD, OR(PEGASUS_BOOTS, ROOSTER)))) + ], + ), + "obstacle_cave_outside_chest": EntranceInfo(), + "obstacle_cave_exit": EntranceInfo(), + + "d6_connector_entrance": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("d6_connector_exit", lambda loc: Location().connect(loc, OR(AND(HOOKSHOT, OR(FLIPPERS, AND(FEATHER, PEGASUS_BOOTS))), ROOSTER)))], + ), + "d6_connector_exit": EntranceInfo(), + + "multichest_left": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[ + ("multichest_right", lambda loc: loc), + ("multichest_top", lambda loc: Location().connect(loc, BOMB)), + ], + ), + "multichest_right": EntranceInfo(), + "multichest_top": EntranceInfo(), + + "prairie_madbatter_connector_entrance": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("prairie_madbatter_connector_exit", lambda loc: Location().connect(loc, FLIPPERS))], + ), + "prairie_madbatter_connector_exit": EntranceInfo(), + + "papahl_house_left": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("papahl_house_right", lambda loc: loc)], + ), + "papahl_house_right": EntranceInfo(), + + "prairie_to_animal_connector": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("animal_to_prairie_connector", lambda loc: Location().connect(loc, PEGASUS_BOOTS))], + ), + "animal_to_prairie_connector": EntranceInfo(), + + "castle_secret_entrance": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("castle_secret_exit", lambda loc: Location().connect(loc, FEATHER))], + ), + "castle_secret_exit": EntranceInfo(), + + "papahl_entrance": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().add(Chest(0x28A)), + exits=[("papahl_exit", lambda loc: loc)], + ), + "papahl_exit": EntranceInfo(), + + "right_taltal_connector5": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("right_taltal_connector6", lambda loc: loc)], + ), + "right_taltal_connector6": EntranceInfo(), + + "toadstool_entrance": EntranceInfo( + items={None: 2}, + logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2BD)), SWORD).connect( # chest in forest cave on route to mushroom + Location().add(HeartPiece(0x2AB), POWER_BRACELET)), # piece of heart in the forest cave on route to the mushroom + exits=[("right_taltal_connector6", lambda loc: loc)], + ), + "toadstool_exit": EntranceInfo(), + + "richard_house": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2C8)), AND(COUNT(GOLD_LEAF, 5), OR(FEATHER, HOOKSHOT, ROOSTER))), + exits=[("richard_maze", lambda loc: Location().connect(loc, COUNT(GOLD_LEAF, 5)))], + ), + "richard_maze": EntranceInfo(), + + "left_to_right_taltalentrance": EntranceInfo( + exits=[("left_taltal_entrance", lambda loc: one_way(loc, OR(HOOKSHOT, ROOSTER)))], + ), + "left_taltal_entrance": EntranceInfo(), + + "boomerang_cave": EntranceInfo(), # TODO boomerang gift + "trendy_shop": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(TradeSequenceItem(0x2A0, TRADING_ITEM_YOSHI_DOLL)), FOUND("RUPEES", 50)) + ), + "moblin_cave": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2E2)), AND(r.attack_hookshot_powder, r.miniboss_requirements[w.miniboss_mapping["moblin_cave"]])) + ), + "prairie_madbatter": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(MadBatter(0x1E0)), MAGIC_POWDER) + ), + "ulrira": EntranceInfo(), + "rooster_house": EntranceInfo(), + "animal_house2": EntranceInfo(), + "animal_house4": EntranceInfo(), + "armos_fairy": EntranceInfo(), + "right_fairy": EntranceInfo(), + "photo_house": EntranceInfo(), + + "bird_cave": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(BirdKey()), OR(AND(FEATHER, COUNT(POWER_BRACELET, 2)), ROOSTER)) + ), + "mamu": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(Song(0x2FB)), AND(OCARINA, COUNT("RUPEES", 300))) + ), + "armos_temple": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(FaceKey()), r.miniboss_requirements[w.miniboss_mapping["armos_temple"]]) + ), + "animal_house1": EntranceInfo(), + "madambowwow": EntranceInfo(), + "library": EntranceInfo(), + "kennel": EntranceInfo( + items={None: 1, TRADING_ITEM_RIBBON: 1}, + logic=lambda c, w, r: Location().connect(Location().add(Seashell(0x2B2)), SHOVEL).connect(Location().add(TradeSequenceItem(0x2B2, TRADING_ITEM_DOG_FOOD)), TRADING_ITEM_RIBBON) + ), + "dream_hut": EntranceInfo( + items={None: 2}, + logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2BF)), OR(SWORD, BOOMERANG, HOOKSHOT, FEATHER)).connect(Location().add(Chest(0x2BE)), AND(OR(SWORD, BOOMERANG, HOOKSHOT, FEATHER), PEGASUS_BOOTS)) + ), + "hookshot_cave": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2B3)), OR(HOOKSHOT, ROOSTER)) + ), + "madbatter_taltal": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(MadBatter(0x1E2)), MAGIC_POWDER) + ), + "forest_madbatter": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(MadBatter(0x1E1)), MAGIC_POWDER) + ), + "banana_seller": EntranceInfo( + items={TRADING_ITEM_DOG_FOOD: 1}, + logic=lambda c, w, r: Location().connect(Location().add(TradeSequenceItem(0x2FE, TRADING_ITEM_BANANAS)), TRADING_ITEM_DOG_FOOD) + ), + "shop": EntranceInfo( + items={None: 2}, + logic=lambda c, w, r: Location().connect(Location().add(ShopItem(0)), COUNT("RUPEES", 200)).connect(Location().add(ShopItem(1)), COUNT("RUPEES", 980)) + ), + "ghost_house": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(Seashell(0x1E3)), POWER_BRACELET) + ), + "writes_house": EntranceInfo( + items={TRADING_ITEM_LETTER: 1}, + logic=lambda c, w, r: Location().connect(Location().add(TradeSequenceItem(0x2A8, TRADING_ITEM_BROOM)), TRADING_ITEM_LETTER) + ), + "animal_house3": EntranceInfo( + items={TRADING_ITEM_HIBISCUS: 1}, + logic=lambda c, w, r: Location().connect(Location().add(TradeSequenceItem(0x2D9, TRADING_ITEM_LETTER)), TRADING_ITEM_HIBISCUS) + ), + "animal_house5": EntranceInfo( + items={TRADING_ITEM_HONEYCOMB: 1}, + logic=lambda c, w, r: Location().connect(Location().add(TradeSequenceItem(0x2D7, TRADING_ITEM_PINEAPPLE)), TRADING_ITEM_HONEYCOMB) + ), + "crazy_tracy": EntranceInfo( + items={"MEDICINE2": 1}, + logic=lambda c, w, r: Location().connect(Location().add(KeyLocation("MEDICINE2")), FOUND("RUPEES", 50)) + ), + "rooster_grave": EntranceInfo( + logic=lambda c, w, r: Location().connect(Location().add(DroppedKey(0x1E4)), AND(OCARINA, SONG3)) + ), + "desert_cave": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(HeartPiece(0x1E8)), BOMB) + ), + "witch": EntranceInfo( + items={TOADSTOOL: 1}, + logic=lambda c, w, r: Location().connect(Location().add(Witch()), TOADSTOOL) + ), + "prairie_left_cave1": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().add(Chest(0x2CD)) + ), + "prairie_left_cave2": EntranceInfo( + items={None: 2}, + logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2F4)), PEGASUS_BOOTS).connect(Location().add(HeartPiece(0x2E5)), AND(BOMB, PEGASUS_BOOTS)) + ), + "castle_jump_cave": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().add(Chest(0x1FD)) + ), + "raft_house": EntranceInfo(), + "prairie_left_fairy": EntranceInfo(), + "seashell_mansion": EntranceInfo(), # TODO: Not sure if we can guarantee enough shells +} diff --git a/worlds/ladx/LADXR/mapgen/locations/seashell.py b/worlds/ladx/LADXR/mapgen/locations/seashell.py new file mode 100644 index 000000000000..521d0c500baf --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/locations/seashell.py @@ -0,0 +1,172 @@ +from ..logic import Location, PEGASUS_BOOTS, SHOVEL +from .base import LocationBase +from ..tileset import solid_tiles, open_tiles, walkable_tiles +from ...roomEditor import RoomEditor +from ...assembler import ASM +from ...locations.all import Seashell +import random + + +class HiddenSeashell(LocationBase): + def __init__(self, room, x, y): + super().__init__(room, x, y) + if room.tiles[x + y * 10] not in (0x20, 0x5C): + if random.randint(0, 1): + room.tiles[x + y * 10] = 0x20 # rock + else: + room.tiles[x + y * 10] = 0x5C # bush + + def update_room(self, rom, re: RoomEditor): + re.entities.append((self.x, self.y, 0x3D)) + + def connect_logic(self, logic_location): + logic_location.add(Seashell(self.room.x + self.room.y * 16)) + + def get_item_pool(self): + return {None: 1} + + @staticmethod + def check_possible(room, reachable_map): + # Check if we can potentially place a hidden seashell here + # First see if we have a nice bush or rock to hide under + options = [] + for y in range(1, 7): + for x in range(1, 9): + if room.tiles[x + y * 10] not in {0x20, 0x5C}: + continue + idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w + if reachable_map.area[idx] == -1: + continue + options.append((reachable_map.distance[idx], x, y)) + if not options: + # No existing bush, we can always add one. So find a nice spot + for y in range(1, 7): + for x in range(1, 9): + if room.tiles[x + y * 10] not in walkable_tiles: + continue + if room.tiles[x + y * 10] == 0x1E: # ocean edge + continue + idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w + if reachable_map.area[idx] == -1: + continue + options.append((reachable_map.distance[idx], x, y)) + if not options: + return None + options.sort(reverse=True) + options = [(x, y) for d, x, y in options if d > options[0][0] - 4] + return random.choice(options) + + +class DigSeashell(LocationBase): + MAX_COUNT = 6 + + def __init__(self, room, x, y): + super().__init__(room, x, y) + if room.tileset_id == "beach": + room.tiles[x + y * 10] = 0x08 + for ox, oy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: + if room.tiles[x + ox + (y + oy) * 10] != 0x1E: + room.tiles[x + ox + (y + oy) * 10] = 0x24 + else: + room.tiles[x + y * 10] = 0x04 + for ox, oy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: + room.tiles[x + ox + (y + oy) * 10] = 0x0A + + def update_room(self, rom, re: RoomEditor): + re.entities.append((self.x, self.y, 0x3D)) + if rom.banks[0x03][0x2210] == 0xFF: + rom.patch(0x03, 0x220F, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}")) + elif rom.banks[0x03][0x2214] == 0xFF: + rom.patch(0x03, 0x2213, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}")) + elif rom.banks[0x03][0x2218] == 0xFF: + rom.patch(0x03, 0x2217, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}")) + elif rom.banks[0x03][0x221C] == 0xFF: + rom.patch(0x03, 0x221B, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}")) + elif rom.banks[0x03][0x2220] == 0xFF: + rom.patch(0x03, 0x221F, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}")) + elif rom.banks[0x03][0x2224] == 0xFF: + rom.patch(0x03, 0x2223, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}")) + + def connect_logic(self, logic_location): + logic_location.connect(Location().add(Seashell(self.room.x + self.room.y * 16)), SHOVEL) + + def get_item_pool(self): + return {None: 1} + + @staticmethod + def check_possible(room, reachable_map): + options = [] + for y in range(1, 7): + for x in range(1, 9): + if room.tiles[x + y * 10] not in walkable_tiles: + continue + if room.tiles[x - 1 + y * 10] not in walkable_tiles: + continue + if room.tiles[x + 1 + y * 10] not in walkable_tiles: + continue + if room.tiles[x + (y - 1) * 10] not in walkable_tiles: + continue + if room.tiles[x + (y + 1) * 10] not in walkable_tiles: + continue + idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w + if reachable_map.area[idx] == -1: + continue + options.append((x, y)) + if not options: + return None + return random.choice(options) + + +class BonkSeashell(LocationBase): + MAX_COUNT = 2 + + def __init__(self, room, x, y): + super().__init__(room, x, y) + self.tree_x = x + self.tree_y = y + for offsetx, offsety in [(-1, 0), (-1, 1), (2, 0), (2, 1), (0, -1), (1, -1), (0, 2), (1, 2)]: + if room.tiles[x + offsetx + (y + offsety) * 10] in walkable_tiles: + self.x += offsetx + self.y += offsety + break + + def update_room(self, rom, re: RoomEditor): + re.entities.append((self.tree_x, self.tree_y, 0x3D)) + if rom.banks[0x03][0x0F04] == 0xFF: + rom.patch(0x03, 0x0F03, ASM("cp $FF"), ASM(f"cp ${self.room.x|(self.room.y<<4):02x}")) + elif rom.banks[0x03][0x0F08] == 0xFF: + rom.patch(0x03, 0x0F07, ASM("cp $FF"), ASM(f"cp ${self.room.x|(self.room.y<<4):02x}")) + else: + raise RuntimeError("To many bonk seashells") + + def connect_logic(self, logic_location): + logic_location.connect(Location().add(Seashell(self.room.x + self.room.y * 16)), PEGASUS_BOOTS) + + def get_item_pool(self): + return {None: 1} + + @staticmethod + def check_possible(room, reachable_map): + # Check if we can potentially place a hidden seashell here + # Find potential trees + options = [] + for y in range(1, 6): + for x in range(1, 8): + if room.tiles[x + y * 10] != 0x25: + continue + if room.tiles[x + y * 10 + 1] != 0x26: + continue + if room.tiles[x + y * 10 + 10] != 0x27: + continue + if room.tiles[x + y * 10 + 11] != 0x28: + continue + idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w + top_reachable = reachable_map.area[idx - reachable_map.w] != -1 or reachable_map.area[idx - reachable_map.w + 1] != -1 + bottom_reachable = reachable_map.area[idx + reachable_map.w * 2] != -1 or reachable_map.area[idx + reachable_map.w * 2 + 1] != -1 + left_reachable = reachable_map.area[idx - 1] != -1 or reachable_map.area[idx + reachable_map.w - 1] != -1 + right_reachable = reachable_map.area[idx + 2] != -1 or reachable_map.area[idx + reachable_map.w + 2] != -1 + if (top_reachable and bottom_reachable) or (left_reachable and right_reachable): + options.append((x, y)) + if not options: + return None + return random.choice(options) diff --git a/worlds/ladx/LADXR/mapgen/logic.py b/worlds/ladx/LADXR/mapgen/logic.py new file mode 100644 index 000000000000..607a00c26f0d --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/logic.py @@ -0,0 +1,146 @@ +from .map import Map +from .locations.entrance import Entrance +from ..logic import * +from .tileset import walkable_tiles, entrance_tiles + + +class LogicGenerator: + def __init__(self, configuration_options, world_setup, requirements_settings, the_map: Map): + self.w = the_map.w * 10 + self.h = the_map.h * 8 + self.map = the_map + self.logic_map = [None] * (self.w * self.h) + self.location_lookup = {} + self.configuration_options = configuration_options + self.world_setup = world_setup + self.requirements_settings = requirements_settings + + self.entrance_map = {} + for room in the_map: + for location in room.locations: + self.location_lookup[(room.x * 10 + location.x, room.y * 8 + location.y)] = location + if isinstance(location, Entrance): + location.prepare_logic(configuration_options, world_setup, requirements_settings) + self.entrance_map[location.entrance_name] = location + + start = self.entrance_map["start_house"] + self.start = Location() + self.egg = self.start # TODO + self.nightmare = Location() + self.windfish = Location().connect(self.nightmare, AND(MAGIC_POWDER, SWORD, OR(BOOMERANG, BOW))) + self.fill_walkable(self.start, start.room.x * 10 + start.x, start.room.y * 8 + start.y) + + logic_str_map = {None: "."} + for y in range(self.h): + line = "" + for x in range(self.w): + if self.logic_map[x + y * self.w] not in logic_str_map: + logic_str_map[self.logic_map[x + y * self.w]] = chr(len(logic_str_map)+48) + line += logic_str_map[self.logic_map[x + y * self.w]] + print(line) + + for room in the_map: + for location in room.locations: + if self.logic_map[(room.x * 10 + location.x) + (room.y * 8 + location.y) * self.w] is None: + raise RuntimeError(f"Location not mapped to logic: {room} {location.__class__.__name__} {location.x} {location.y}") + + tmp = set() + def r(n): + if n in tmp: + return + tmp.add(n) + for item in n.items: + print(item) + for o, req in n.simple_connections: + r(o) + for o, req in n.gated_connections: + r(o) + r(self.start) + + def fill_walkable(self, location, x, y): + tile_options = walkable_tiles | entrance_tiles + for x, y in self.flood_fill_logic(location, tile_options, x, y): + if self.logic_map[x + y * self.w] is not None: + continue + tile = self.map.get_tile(x, y) + if tile == 0x5C: # bush + other_location = Location() + location.connect(other_location, self.requirements_settings.bush) + self.fill_bush(other_location, x, y) + elif tile == 0x20: # rock + other_location = Location() + location.connect(other_location, POWER_BRACELET) + self.fill_rock(other_location, x, y) + elif tile == 0xE8: # pit + if self.map.get_tile(x - 1, y) in tile_options and self.map.get_tile(x + 1, y) in tile_options: + if self.logic_map[x - 1 + y * self.w] == location and self.logic_map[x + 1 + y * self.w] is None: + other_location = Location().connect(location, FEATHER) + self.fill_walkable(other_location, x + 1, y) + if self.logic_map[x - 1 + y * self.w] is None and self.logic_map[x + 1 + y * self.w] == location: + other_location = Location().connect(location, FEATHER) + self.fill_walkable(other_location, x - 1, y) + if self.map.get_tile(x, y - 1) in tile_options and self.map.get_tile(x, y + 1) in tile_options: + if self.logic_map[x + (y - 1) * self.w] == location and self.logic_map[x + (y + 1) * self.w] is None: + other_location = Location().connect(location, FEATHER) + self.fill_walkable(other_location, x, y + 1) + if self.logic_map[x + (y - 1) * self.w] is None and self.logic_map[x + (y + 1) * self.w] == location: + other_location = Location().connect(location, FEATHER) + self.fill_walkable(other_location, x, y - 1) + + def fill_bush(self, location, x, y): + for x, y in self.flood_fill_logic(location, {0x5C}, x, y): + if self.logic_map[x + y * self.w] is not None: + continue + tile = self.map.get_tile(x, y) + if tile in walkable_tiles or tile in entrance_tiles: + other_location = Location() + location.connect(other_location, self.requirements_settings.bush) + self.fill_walkable(other_location, x, y) + + def fill_rock(self, location, x, y): + for x, y in self.flood_fill_logic(location, {0x20}, x, y): + if self.logic_map[x + y * self.w] is not None: + continue + tile = self.map.get_tile(x, y) + if tile in walkable_tiles or tile in entrance_tiles: + other_location = Location() + location.connect(other_location, POWER_BRACELET) + self.fill_walkable(other_location, x, y) + + def flood_fill_logic(self, location, tile_types, x, y): + assert self.map.get_tile(x, y) in tile_types + todo = [(x, y)] + entrance_todo = [] + + edge_set = set() + while todo: + x, y = todo.pop() + if self.map.get_tile(x, y) not in tile_types: + edge_set.add((x, y)) + continue + if self.logic_map[x + y * self.w] is not None: + continue + self.logic_map[x + y * self.w] = location + if (x, y) in self.location_lookup: + room_location = self.location_lookup[(x, y)] + result = room_location.connect_logic(location) + if result: + entrance_todo += result + + if x < self.w - 1 and self.logic_map[x + 1 + y * self.w] is None: + todo.append((x + 1, y)) + if x > 0 and self.logic_map[x - 1 + y * self.w] is None: + todo.append((x - 1, y)) + if y < self.h - 1 and self.logic_map[x + y * self.w + self.w] is None: + todo.append((x, y + 1)) + if y > 0 and self.logic_map[x + y * self.w - self.w] is None: + if self.map.get_tile(x, y - 1) == 0xA0: # Chest, can only be collected from the south + self.location_lookup[(x, y - 1)].connect_logic(location) + self.logic_map[x + (y - 1) * self.w] = location + todo.append((x, y - 1)) + + for entrance_name, logic_connection in entrance_todo: + entrance = self.entrance_map[entrance_name] + entrance.connect_logic(logic_connection) + self.fill_walkable(logic_connection, entrance.room.x * 10 + entrance.x, entrance.room.y * 8 + entrance.y) + return edge_set diff --git a/worlds/ladx/LADXR/mapgen/map.py b/worlds/ladx/LADXR/mapgen/map.py new file mode 100644 index 000000000000..0c9be58bcd6f --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/map.py @@ -0,0 +1,231 @@ +import random +from .tileset import solid_tiles, open_tiles +from ..locations.items import * + + +PRIMARY_ITEMS = [POWER_BRACELET, SHIELD, BOW, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, OCARINA, FEATHER, SHOVEL, MAGIC_POWDER, BOMB, SWORD, FLIPPERS, SONG1] +SECONDARY_ITEMS = [BOOMERANG, RED_TUNIC, BLUE_TUNIC, MAX_POWDER_UPGRADE, MAX_BOMBS_UPGRADE, MAX_ARROWS_UPGRADE, GEL] + +HORIZONTAL = 0 +VERTICAL = 1 + + +class RoomEdge: + def __init__(self, direction): + self.__solid = False + self.__open_range = None + self.direction = direction + self.__open_min = 2 if direction == HORIZONTAL else 1 + self.__open_max = 8 if direction == HORIZONTAL else 7 + + def force_solid(self): + self.__open_min = -1 + self.__open_max = -1 + self.__open_range = None + self.__solid = True + + def set_open_min(self, value): + if self.__open_min < 0: + return + self.__open_min = max(self.__open_min, value) + + def set_open_max(self, value): + if self.__open_max < 0: + return + self.__open_max = min(self.__open_max, value) + + def set_solid(self): + self.__open_range = None + self.__solid = True + + def can_open(self): + return self.__open_min > -1 + + def set_open(self): + cnt = random.randint(1, self.__open_max - self.__open_min) + if random.randint(1, 100) < 50: + cnt = 1 + offset = random.randint(self.__open_min, self.__open_max - cnt) + self.__open_range = (offset, offset + cnt) + self.__solid = False + + def is_solid(self): + return self.__solid + + def get_open_range(self): + return self.__open_range + + def seed(self, wfc, x, y): + for offset, cell in self.__cells(wfc, x, y): + if self.__open_range and self.__open_range[0] <= offset < self.__open_range[1]: + cell.init_options.intersection_update(open_tiles) + elif self.__solid: + cell.init_options.intersection_update(solid_tiles) + + def __cells(self, wfc, x, y): + if self.direction == HORIZONTAL: + for n in range(1, 9): + yield n, wfc.cell_data[(x + n, y)] + else: + for n in range(1, 7): + yield n, wfc.cell_data[(x, y + n)] + + +class RoomInfo: + def __init__(self, x, y): + self.x = x + self.y = y + self.tileset_id = "basic" + self.room_type = None + self.tiles = None + self.edge_left = None + self.edge_up = None + self.edge_right = RoomEdge(VERTICAL) + self.edge_down = RoomEdge(HORIZONTAL) + self.room_left = None + self.room_up = None + self.room_right = None + self.room_down = None + self.locations = [] + self.entities = [] + + def __repr__(self): + return f"Room<{self.x} {self.y}>" + + +class Map: + def __init__(self, w, h, tilesets): + self.w = w + self.h = h + self.tilesets = tilesets + self.__rooms = [RoomInfo(x, y) for y in range(h) for x in range(w)] + for x in range(w): + for y in range(h): + room = self.get(x, y) + if x == 0: + room.edge_left = RoomEdge(VERTICAL) + else: + room.edge_left = self.get(x - 1, y).edge_right + if y == 0: + room.edge_up = RoomEdge(HORIZONTAL) + else: + room.edge_up = self.get(x, y - 1).edge_down + if x > 0: + room.room_left = self.get(x - 1, y) + if x < w - 1: + room.room_right = self.get(x + 1, y) + if y > 0: + room.room_up = self.get(x, y - 1) + if y < h - 1: + room.room_down = self.get(x, y + 1) + for x in range(w): + self.get(x, 0).edge_up.set_solid() + self.get(x, h-1).edge_down.set_solid() + for y in range(h): + self.get(0, y).edge_left.set_solid() + self.get(w-1, y).edge_right.set_solid() + + def __iter__(self): + return iter(self.__rooms) + + def get(self, x, y) -> RoomInfo: + assert 0 <= x < self.w and 0 <= y < self.h, f"{x} {y}" + return self.__rooms[x + y * self.w] + + def get_tile(self, x, y): + return self.get(x // 10, y // 8).tiles[(x % 10) + (y % 8) * 10] + + def get_item_pool(self): + item_pool = {} + for room in self.__rooms: + for location in room.locations: + print(room, location.get_item_pool(), location.__class__.__name__) + for k, v in location.get_item_pool().items(): + item_pool[k] = item_pool.get(k, 0) + v + unmapped_count = item_pool.get(None, 0) + del item_pool[None] + for item in PRIMARY_ITEMS: + if item not in item_pool: + item_pool[item] = 1 + unmapped_count -= 1 + while item_pool[POWER_BRACELET] < 2: + item_pool[POWER_BRACELET] = item_pool.get(POWER_BRACELET, 0) + 1 + unmapped_count -= 1 + while item_pool[SHIELD] < 2: + item_pool[SHIELD] = item_pool.get(SHIELD, 0) + 1 + unmapped_count -= 1 + assert unmapped_count >= 0 + + for item in SECONDARY_ITEMS: + if unmapped_count > 0: + item_pool[item] = item_pool.get(item, 0) + 1 + unmapped_count -= 1 + + # Add a heart container per 10 items "spots" left. + heart_piece_count = unmapped_count // 10 + unmapped_count -= heart_piece_count * 4 + item_pool[HEART_PIECE] = item_pool.get(HEART_PIECE, 0) + heart_piece_count * 4 + + # Add the rest as rupees + item_pool[RUPEES_50] = item_pool.get(RUPEES_50, 0) + unmapped_count + return item_pool + + def dump(self): + for y in range(self.h): + for x in range(self.w): + if self.get(x, y).edge_right.is_solid(): + print(" |", end="") + elif self.get(x, y).edge_right.get_open_range(): + print(" ", end="") + else: + print(" ?", end="") + print() + for x in range(self.w): + if self.get(x, y).edge_down.is_solid(): + print("-+", end="") + elif self.get(x, y).edge_down.get_open_range(): + print(" +", end="") + else: + print("?+", end="") + print() + print() + + +class MazeGen: + UP = 0x01 + DOWN = 0x02 + LEFT = 0x04 + RIGHT = 0x08 + + def __init__(self, the_map: Map): + self.map = the_map + self.visited = set() + self.visit(0, 0) + + def visit(self, x, y): + self.visited.add((x, y)) + neighbours = self.get_neighbours(x, y) + while any((x, y) not in self.visited for x, y, d in neighbours): + x, y, d = random.choice(neighbours) + if (x, y) not in self.visited: + if d == self.RIGHT and self.map.get(x, y).edge_left.can_open(): + self.map.get(x, y).edge_left.set_open() + elif d == self.LEFT and self.map.get(x, y).edge_right.can_open(): + self.map.get(x, y).edge_right.set_open() + elif d == self.DOWN and self.map.get(x, y).edge_up.can_open(): + self.map.get(x, y).edge_up.set_open() + elif d == self.UP and self.map.get(x, y).edge_down.can_open(): + self.map.get(x, y).edge_down.set_open() + self.visit(x, y) + + def get_neighbours(self, x, y): + neighbours = [] + if x > 0: + neighbours.append((x - 1, y, self.LEFT)) + if x < self.map.w - 1: + neighbours.append((x + 1, y, self.RIGHT)) + if y > 0: + neighbours.append((x, y - 1, self.UP)) + if y < self.map.h - 1: + neighbours.append((x, y + 1, self.DOWN)) + return neighbours diff --git a/worlds/ladx/LADXR/mapgen/roomgen.py b/worlds/ladx/LADXR/mapgen/roomgen.py new file mode 100644 index 000000000000..189bb25d7231 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/roomgen.py @@ -0,0 +1,78 @@ +from .map import Map +from .roomtype.town import Town +from .roomtype.mountain import Mountain, MountainEgg +from .roomtype.forest import Forest +from .roomtype.base import RoomType +from .roomtype.water import Water, Beach +import random + + +def is_area_clear(the_map: Map, x, y, w, h): + for y0 in range(y, y+h): + for x0 in range(x, x+w): + if 0 <= x0 < the_map.w and 0 <= y0 < the_map.h: + if the_map.get(x0, y0).room_type is not None: + return False + return True + + +def find_random_clear_area(the_map: Map, w, h, *, tries): + for n in range(tries): + x = random.randint(0, the_map.w - w) + y = random.randint(0, the_map.h - h) + if is_area_clear(the_map, x - 1, y - 1, w + 2, h + 2): + return x, y + return None, None + + +def setup_room_types(the_map: Map): + # Always make the rop row mountains. + egg_x = the_map.w // 2 + for x in range(the_map.w): + if x == egg_x: + MountainEgg(the_map.get(x, 0)) + else: + Mountain(the_map.get(x, 0)) + + # Add some beach. + width = the_map.w if random.random() < 0.5 else random.randint(max(2, the_map.w // 4), the_map.w // 2) + beach_x = 0 # current tileset doesn't allow anything else + for x in range(beach_x, beach_x+width): + # Beach(the_map.get(x, the_map.h - 2)) + Beach(the_map.get(x, the_map.h - 1)) + the_map.get(beach_x + width - 1, the_map.h - 1).edge_right.force_solid() + + town_x, town_y = find_random_clear_area(the_map, 2, 2, tries=20) + if town_x is not None: + for y in range(town_y, town_y + 2): + for x in range(town_x, town_x + 2): + Town(the_map.get(x, y)) + + forest_w, forest_h = 2, 2 + if random.random() < 0.5: + forest_w += 1 + else: + forest_h += 1 + forest_x, forest_y = find_random_clear_area(the_map, forest_w, forest_h, tries=20) + if forest_x is None: + forest_w, forest_h = 2, 2 + forest_x, forest_y = find_random_clear_area(the_map, forest_w, forest_h, tries=20) + if forest_x is not None: + for y in range(forest_y, forest_y + forest_h): + for x in range(forest_x, forest_x + forest_w): + Forest(the_map.get(x, y)) + + # for n in range(5): + # water_w, water_h = 2, 1 + # if random.random() < 0.5: + # water_w, water_h = water_h, water_w + # water_x, water_y = find_random_clear_area(the_map, water_w, water_h, tries=20) + # if water_x is not None: + # for y in range(water_y, water_y + water_h): + # for x in range(water_x, water_x + water_w): + # Water(the_map.get(x, y)) + + for y in range(the_map.h): + for x in range(the_map.w): + if the_map.get(x, y).room_type is None: + RoomType(the_map.get(x, y)) diff --git a/worlds/ladx/LADXR/mapgen/roomtype/base.py b/worlds/ladx/LADXR/mapgen/roomtype/base.py new file mode 100644 index 000000000000..b524c4fb2342 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/roomtype/base.py @@ -0,0 +1,54 @@ +from ..tileset import open_tiles + + +def plot_line(x0, y0, x1, y1): + dx = abs(x1 - x0) + sx = 1 if x0 < x1 else -1 + dy = -abs(y1 - y0) + sy = 1 if y0 < y1 else -1 + error = dx + dy + + yield x0, y0 + while True: + if x0 == x1 and y0 == y1: + break + e2 = 2 * error + if e2 >= dy: + error = error + dy + x0 = x0 + sx + yield x0, y0 + if e2 <= dx: + error = error + dx + y0 = y0 + sy + yield x0, y0 + + yield x1, y1 + + +class RoomType: + def __init__(self, room): + self.room = room + room.room_type = self + + def seed(self, wfc, x, y): + open_points = [] + r = self.room.edge_left.get_open_range() + if r: + open_points.append((x + 1, y + (r[0] + r[1]) // 2)) + r = self.room.edge_right.get_open_range() + if r: + open_points.append((x + 8, y + (r[0] + r[1]) // 2)) + r = self.room.edge_up.get_open_range() + if r: + open_points.append((x + (r[0] + r[1]) // 2, y + 1)) + r = self.room.edge_down.get_open_range() + if r: + open_points.append((x + (r[0] + r[1]) // 2, y + 6)) + if len(open_points) < 2: + return + mid_x = sum([x for x, y in open_points]) // len(open_points) + mid_y = sum([y for x, y in open_points]) // len(open_points) + + for x0, y0 in open_points: + for px, py in plot_line(x0, y0, mid_x, mid_y): + wfc.cell_data[(px, py)].init_options.intersection_update(open_tiles) diff --git a/worlds/ladx/LADXR/mapgen/roomtype/forest.py b/worlds/ladx/LADXR/mapgen/roomtype/forest.py new file mode 100644 index 000000000000..25c71eefc83d --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/roomtype/forest.py @@ -0,0 +1,28 @@ +from .base import RoomType +from ..tileset import open_tiles +import random + + +class Forest(RoomType): + def __init__(self, room): + super().__init__(room) + room.tileset_id = "forest" + + def seed(self, wfc, x, y): + if self.room.room_up and isinstance(self.room.room_up.room_type, Forest) and self.room.edge_up.get_open_range() is None: + self.room.edge_up.set_solid() + if self.room.room_left and isinstance(self.room.room_left.room_type, Forest) and self.room.edge_left.get_open_range() is None: + self.room.edge_left.set_solid() + + if self.room.room_up and isinstance(self.room.room_up.room_type, Forest) and random.random() < 0.5: + door_x, door_y = x + 5 + random.randint(-1, 1), y + 3 + random.randint(-1, 1) + wfc.cell_data[(door_x, door_y)].init_options.intersection_update({0xE3}) + self.room.edge_up.set_solid() + if self.room.edge_left.get_open_range() is not None: + for x0 in range(x + 1, door_x): + wfc.cell_data[(x0, door_y + 1)].init_options.intersection_update(open_tiles) + if self.room.edge_right.get_open_range() is not None: + for x0 in range(door_x + 1, x + 10): + wfc.cell_data[(x0, door_y + 1)].init_options.intersection_update(open_tiles) + else: + super().seed(wfc, x, y) diff --git a/worlds/ladx/LADXR/mapgen/roomtype/mountain.py b/worlds/ladx/LADXR/mapgen/roomtype/mountain.py new file mode 100644 index 000000000000..d80d5dc58150 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/roomtype/mountain.py @@ -0,0 +1,38 @@ +from .base import RoomType +from ..locations.entrance import EggEntrance +import random + + +class Mountain(RoomType): + def __init__(self, room): + super().__init__(room) + room.tileset_id = "mountains" + room.edge_left.set_open_min(3) + room.edge_right.set_open_min(3) + + def seed(self, wfc, x, y): + super().seed(wfc, x, y) + if y == 0: + if x == 0: + wfc.cell_data[(0, 1)].init_options.intersection_update({0}) + if x == wfc.w - 10: + wfc.cell_data[(x + 9, 1)].init_options.intersection_update({0}) + wfc.cell_data[(x + random.randint(3, 6), random.randint(0, 1))].init_options.intersection_update({0}) + + +class MountainEgg(RoomType): + def __init__(self, room): + super().__init__(room) + room.tileset_id = "egg" + room.edge_left.force_solid() + room.edge_right.force_solid() + room.edge_down.set_open_min(5) + room.edge_down.set_open_max(6) + + EggEntrance(room, 5, 4) + + def seed(self, wfc, x, y): + super().seed(wfc, x, y) + wfc.cell_data[(x + 2, y + 1)].init_options.intersection_update({0x00}) + wfc.cell_data[(x + 2, y + 2)].init_options.intersection_update({0xEF}) + wfc.cell_data[(x + 5, y + 3)].init_options.intersection_update({0xAA}) diff --git a/worlds/ladx/LADXR/mapgen/roomtype/town.py b/worlds/ladx/LADXR/mapgen/roomtype/town.py new file mode 100644 index 000000000000..2553a133084a --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/roomtype/town.py @@ -0,0 +1,16 @@ +from .base import RoomType +from ..tileset import solid_tiles +import random + + +class Town(RoomType): + def __init__(self, room): + super().__init__(room) + room.tileset_id = "town" + + def seed(self, wfc, x, y): + ex = x + 5 + random.randint(-1, 1) + ey = y + 3 + random.randint(-1, 1) + wfc.cell_data[(ex, ey)].init_options.intersection_update({0xE2}) + wfc.cell_data[(ex - 1, ey - 1)].init_options.intersection_update(solid_tiles) + wfc.cell_data[(ex + 1, ey - 1)].init_options.intersection_update(solid_tiles) diff --git a/worlds/ladx/LADXR/mapgen/roomtype/water.py b/worlds/ladx/LADXR/mapgen/roomtype/water.py new file mode 100644 index 000000000000..e3f4830ecf65 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/roomtype/water.py @@ -0,0 +1,30 @@ +from .base import RoomType +import random + + +class Water(RoomType): + def __init__(self, room): + super().__init__(room) + room.tileset_id = "water" + + # def seed(self, wfc, x, y): + # wfc.cell_data[(x + 5 + random.randint(-1, 1), y + 3 + random.randint(-1, 1))].init_options.intersection_update({0x0E}) + + +class Beach(RoomType): + def __init__(self, room): + super().__init__(room) + room.tileset_id = "beach" + if self.room.room_down is None: + self.room.edge_left.set_open_max(4) + self.room.edge_right.set_open_max(4) + self.room.edge_up.set_open_min(4) + self.room.edge_up.set_open_max(6) + + def seed(self, wfc, x, y): + if self.room.room_down is None: + for n in range(1, 9): + wfc.cell_data[(x + n, y + 5)].init_options.intersection_update({0x1E}) + for n in range(1, 9): + wfc.cell_data[(x + n, y + 7)].init_options.intersection_update({0x1F}) + super().seed(wfc, x, y) \ No newline at end of file diff --git a/worlds/ladx/LADXR/mapgen/tileset.py b/worlds/ladx/LADXR/mapgen/tileset.py new file mode 100644 index 000000000000..b634c302236c --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/tileset.py @@ -0,0 +1,253 @@ +from typing import Dict, Set +from ..roomEditor import RoomEditor + + +animated_tiles = {0x0E, 0x1B, 0x1E, 0x1F, 0x44, 0x91, 0xCF, 0xD0, 0xD1, 0xD2, 0xD9, 0xDC, 0xE9, 0xEB, 0xEC, 0xED, 0xEE, 0xEF} +entrance_tiles = {0xE1, 0xE2, 0xE3, 0xBA, 0xC6} + +solid_tiles = set() +open_tiles = set() +walkable_tiles = set() +vertical_edge_tiles = set() +horizontal_edge_tiles = set() + + +class TileInfo: + def __init__(self, key): + self.key = key + self.up = set() + self.right = set() + self.down = set() + self.left = set() + self.up_freq = {} + self.right_freq = {} + self.down_freq = {} + self.left_freq = {} + self.frequency = 0 + + def copy(self): + result = TileInfo(self.key) + result.up = self.up.copy() + result.right = self.right.copy() + result.down = self.down.copy() + result.left = self.left.copy() + result.up_freq = self.up_freq.copy() + result.right_freq = self.right_freq.copy() + result.down_freq = self.down_freq.copy() + result.left_freq = self.left_freq.copy() + result.frequency = self.frequency + return result + + def remove(self, tile_id): + if tile_id in self.up: + self.up.remove(tile_id) + del self.up_freq[tile_id] + if tile_id in self.down: + self.down.remove(tile_id) + del self.down_freq[tile_id] + if tile_id in self.left: + self.left.remove(tile_id) + del self.left_freq[tile_id] + if tile_id in self.right: + self.right.remove(tile_id) + del self.right_freq[tile_id] + + def update(self, other: "TileInfo", tile_filter: Set[int]): + self.frequency += other.frequency + self.up.update(other.up.intersection(tile_filter)) + self.down.update(other.down.intersection(tile_filter)) + self.left.update(other.left.intersection(tile_filter)) + self.right.update(other.right.intersection(tile_filter)) + for k, v in other.up_freq.items(): + if k not in tile_filter: + continue + self.up_freq[k] = self.up_freq.get(k, 0) + v + for k, v in other.down_freq.items(): + if k not in tile_filter: + continue + self.down_freq[k] = self.down_freq.get(k, 0) + v + for k, v in other.left_freq.items(): + if k not in tile_filter: + continue + self.left_freq[k] = self.left_freq.get(k, 0) + v + for k, v in other.down_freq.items(): + if k not in tile_filter: + continue + self.right_freq[k] = self.right_freq.get(k, 0) + v + + def __repr__(self): + return f"<{self.key}>\n U{[f'{n:02x}' for n in self.up]}\n R{[f'{n:02x}' for n in self.right]}\n D{[f'{n:02x}' for n in self.down]}\n L{[f'{n:02x}' for n in self.left]}>" + + +class TileSet: + def __init__(self, *, main_id=None, animation_id=None): + self.main_id = main_id + self.animation_id = animation_id + self.palette_id = None + self.attr_bank = None + self.attr_addr = None + self.tiles: Dict[int, "TileInfo"] = {} + self.all: Set[int] = set() + + def copy(self) -> "TileSet": + result = TileSet(main_id=self.main_id, animation_id=self.animation_id) + for k, v in self.tiles.items(): + result.tiles[k] = v.copy() + result.all = self.all.copy() + return result + + def remove(self, tile_id): + self.all.remove(tile_id) + del self.tiles[tile_id] + for k, v in self.tiles.items(): + v.remove(tile_id) + + # Look at the "other" tileset and merge information about tiles known in this tileset + def learn_from(self, other: "TileSet"): + for key, other_info in other.tiles.items(): + if key not in self.all: + continue + self.tiles[key].update(other_info, self.all) + + def combine(self, other: "TileSet"): + if other.main_id and not self.main_id: + self.main_id = other.main_id + if other.animation_id and not self.animation_id: + self.animation_id = other.animation_id + for key, other_info in other.tiles.items(): + if key not in self.all: + self.tiles[key] = other_info.copy() + else: + self.tiles[key].update(other_info, self.all) + self.all.update(other.all) + + +def loadTileInfo(rom) -> Dict[str, TileSet]: + for n in range(0x100): + physics_flag = rom.banks[8][0x0AD4 + n] + if n == 0xEF: + physics_flag = 0x01 # One of the sky tiles is marked as a pit instead of solid, which messes with the generation of sky + if physics_flag in {0x00, 0x05, 0x06, 0x07}: + open_tiles.add(n) + walkable_tiles.add(n) + vertical_edge_tiles.add(n) + horizontal_edge_tiles.add(n) + elif physics_flag in {0x01, 0x04, 0x60}: + solid_tiles.add(n) + vertical_edge_tiles.add(n) + horizontal_edge_tiles.add(n) + elif physics_flag in {0x08}: # Bridge + open_tiles.add(n) + walkable_tiles.add(n) + elif physics_flag in {0x02}: # Stairs + open_tiles.add(n) + walkable_tiles.add(n) + horizontal_edge_tiles.add(n) + elif physics_flag in {0x03}: # Entrances + open_tiles.add(n) + elif physics_flag in {0x30}: # bushes/rocks + open_tiles.add(n) + elif physics_flag in {0x50}: # pits + open_tiles.add(n) + world_tiles = {} + for ry in range(0, 16): + for rx in range(0, 16): + tileset_id = rom.banks[0x3F][0x3F00 + rx + (ry << 4)] + re = RoomEditor(rom, rx | (ry << 4)) + tiles = re.getTileArray() + for y in range(8): + for x in range(10): + tile_id = tiles[x+y*10] + world_tiles[(rx*10+x, ry*8+y)] = (tile_id, tileset_id, re.animation_id | 0x100) + + # Fix up wrong tiles + world_tiles[(150, 24)] = (0x2A, world_tiles[(150, 24)][1], world_tiles[(150, 24)][2]) # Left of the raft house, a tree has the wrong tile. + + rom_tilesets: Dict[int, TileSet] = {} + for (x, y), (key, tileset_id, animation_id) in world_tiles.items(): + if key in animated_tiles: + if animation_id not in rom_tilesets: + rom_tilesets[animation_id] = TileSet(animation_id=animation_id&0xFF) + tileset = rom_tilesets[animation_id] + else: + if tileset_id not in rom_tilesets: + rom_tilesets[tileset_id] = TileSet(main_id=tileset_id) + tileset = rom_tilesets[tileset_id] + tileset.all.add(key) + if key not in tileset.tiles: + tileset.tiles[key] = TileInfo(key) + ti = tileset.tiles[key] + ti.frequency += 1 + if (x, y - 1) in world_tiles: + tile_id = world_tiles[(x, y - 1)][0] + ti.up.add(tile_id) + ti.up_freq[tile_id] = ti.up_freq.get(tile_id, 0) + 1 + if (x + 1, y) in world_tiles: + tile_id = world_tiles[(x + 1, y)][0] + ti.right.add(tile_id) + ti.right_freq[tile_id] = ti.right_freq.get(tile_id, 0) + 1 + if (x, y + 1) in world_tiles: + tile_id = world_tiles[(x, y + 1)][0] + ti.down.add(tile_id) + ti.down_freq[tile_id] = ti.down_freq.get(tile_id, 0) + 1 + if (x - 1, y) in world_tiles: + tile_id = world_tiles[(x - 1, y)][0] + ti.left.add(tile_id) + ti.left_freq[tile_id] = ti.left_freq.get(tile_id, 0) + 1 + + tilesets = { + "basic": rom_tilesets[0x0F].copy() + } + for key, tileset in rom_tilesets.items(): + tilesets["basic"].learn_from(tileset) + tilesets["mountains"] = rom_tilesets[0x3E].copy() + tilesets["mountains"].combine(rom_tilesets[0x10B]) + tilesets["mountains"].remove(0xB6) # Remove the raft house roof + tilesets["mountains"].remove(0xB7) # Remove the raft house roof + tilesets["mountains"].remove(0x66) # Remove the raft house roof + tilesets["mountains"].learn_from(rom_tilesets[0x1C]) + tilesets["mountains"].learn_from(rom_tilesets[0x3C]) + tilesets["mountains"].learn_from(rom_tilesets[0x30]) + tilesets["mountains"].palette_id = 0x15 + tilesets["mountains"].attr_bank = 0x27 + tilesets["mountains"].attr_addr = 0x5A20 + + tilesets["egg"] = rom_tilesets[0x3C].copy() + tilesets["egg"].combine(tilesets["mountains"]) + tilesets["egg"].palette_id = 0x13 + tilesets["egg"].attr_bank = 0x27 + tilesets["egg"].attr_addr = 0x5620 + + tilesets["forest"] = rom_tilesets[0x20].copy() + tilesets["forest"].palette_id = 0x00 + tilesets["forest"].attr_bank = 0x25 + tilesets["forest"].attr_addr = 0x4000 + + tilesets["town"] = rom_tilesets[0x26].copy() + tilesets["town"].combine(rom_tilesets[0x103]) + tilesets["town"].palette_id = 0x03 + tilesets["town"].attr_bank = 0x25 + tilesets["town"].attr_addr = 0x4C00 + + tilesets["swamp"] = rom_tilesets[0x36].copy() + tilesets["swamp"].combine(rom_tilesets[0x103]) + tilesets["swamp"].palette_id = 0x0E + tilesets["swamp"].attr_bank = 0x22 + tilesets["swamp"].attr_addr = 0x7400 + + tilesets["beach"] = rom_tilesets[0x22].copy() + tilesets["beach"].combine(rom_tilesets[0x102]) + tilesets["beach"].palette_id = 0x01 + tilesets["beach"].attr_bank = 0x22 + tilesets["beach"].attr_addr = 0x5000 + + tilesets["water"] = rom_tilesets[0x3E].copy() + tilesets["water"].combine(rom_tilesets[0x103]) + tilesets["water"].learn_from(tilesets["basic"]) + tilesets["water"].remove(0x7A) + tilesets["water"].remove(0xC8) + tilesets["water"].palette_id = 0x09 + tilesets["water"].attr_bank = 0x22 + tilesets["water"].attr_addr = 0x6400 + + return tilesets diff --git a/worlds/ladx/LADXR/mapgen/util.py b/worlds/ladx/LADXR/mapgen/util.py new file mode 100644 index 000000000000..ab22755b2e9b --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/util.py @@ -0,0 +1,5 @@ + +def xyrange(w, h): + for y in range(h): + for x in range(w): + yield x, y diff --git a/worlds/ladx/LADXR/mapgen/wfc.py b/worlds/ladx/LADXR/mapgen/wfc.py new file mode 100644 index 000000000000..e40b6af127f9 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/wfc.py @@ -0,0 +1,250 @@ +from .tileset import TileSet, solid_tiles, open_tiles, vertical_edge_tiles, horizontal_edge_tiles +from .map import Map +from typing import Set +import random + + +class ContradictionException(Exception): + def __init__(self, x, y): + self.x = x + self.y = y + + +class Cell: + def __init__(self, x, y, tileset: TileSet, options: Set[int]): + self.x = x + self.y = y + self.tileset = tileset + self.init_options = options + self.options = None + self.result = None + + def __set_new_options(self, new_options): + if new_options != self.options: + if self.result is not None: + raise ContradictionException(self.x, self.y) + if not new_options: + raise ContradictionException(self.x, self.y) + self.options = new_options + return True + return False + + def update_options_up(self, cell: "Cell") -> bool: + new_options = set() + for tile in cell.options: + new_options.update(cell.tileset.tiles[tile].up) + new_options.intersection_update(self.options) + if (self.y % 8) == 7: + if cell.options.issubset(solid_tiles): + new_options.intersection_update(solid_tiles) + if cell.options.issubset(open_tiles): + new_options.intersection_update(open_tiles) + return self.__set_new_options(new_options) + + def update_options_right(self, cell: "Cell") -> bool: + new_options = set() + for tile in cell.options: + new_options.update(cell.tileset.tiles[tile].right) + new_options.intersection_update(self.options) + if (self.x % 10) == 0: + if cell.options.issubset(solid_tiles): + new_options.intersection_update(solid_tiles) + if cell.options.issubset(open_tiles): + new_options.intersection_update(open_tiles) + return self.__set_new_options(new_options) + + def update_options_down(self, cell: "Cell") -> bool: + new_options = set() + for tile in cell.options: + new_options.update(cell.tileset.tiles[tile].down) + new_options.intersection_update(self.options) + if (self.y % 8) == 0: + if cell.options.issubset(solid_tiles): + new_options.intersection_update(solid_tiles) + if cell.options.issubset(open_tiles): + new_options.intersection_update(open_tiles) + return self.__set_new_options(new_options) + + def update_options_left(self, cell: "Cell") -> bool: + new_options = set() + for tile in cell.options: + new_options.update(cell.tileset.tiles[tile].left) + new_options.intersection_update(self.options) + if (self.x % 10) == 9: + if cell.options.issubset(solid_tiles): + new_options.intersection_update(solid_tiles) + if cell.options.issubset(open_tiles): + new_options.intersection_update(open_tiles) + return self.__set_new_options(new_options) + + def __repr__(self): + return f"Cell<{self.options}>" + + +class WFCMap: + def __init__(self, the_map: Map, tilesets, *, step_callback=None): + self.cell_data = {} + self.on_step = step_callback + self.w = the_map.w * 10 + self.h = the_map.h * 8 + + for y in range(self.h): + for x in range(self.w): + tileset = tilesets[the_map.get(x//10, y//8).tileset_id] + new_cell = Cell(x, y, tileset, tileset.all.copy()) + self.cell_data[(new_cell.x, new_cell.y)] = new_cell + for y in range(self.h): + self.cell_data[(0, y)].init_options.intersection_update(solid_tiles) + self.cell_data[(self.w-1, y)].init_options.intersection_update(solid_tiles) + for x in range(self.w): + self.cell_data[(x, 0)].init_options.intersection_update(solid_tiles) + self.cell_data[(x, self.h-1)].init_options.intersection_update(solid_tiles) + + for x in range(0, self.w, 10): + for y in range(self.h): + self.cell_data[(x, y)].init_options.intersection_update(vertical_edge_tiles) + for x in range(9, self.w, 10): + for y in range(self.h): + self.cell_data[(x, y)].init_options.intersection_update(vertical_edge_tiles) + for y in range(0, self.h, 8): + for x in range(self.w): + self.cell_data[(x, y)].init_options.intersection_update(horizontal_edge_tiles) + for y in range(7, self.h, 8): + for x in range(self.w): + self.cell_data[(x, y)].init_options.intersection_update(horizontal_edge_tiles) + + for sy in range(the_map.h): + for sx in range(the_map.w): + the_map.get(sx, sy).room_type.seed(self, sx*10, sy*8) + + for sy in range(the_map.h): + for sx in range(the_map.w): + room = the_map.get(sx, sy) + room.edge_left.seed(self, sx * 10, sy * 8) + room.edge_right.seed(self, sx * 10 + 9, sy * 8) + room.edge_up.seed(self, sx * 10, sy * 8) + room.edge_down.seed(self, sx * 10, sy * 8 + 7) + + def initialize(self): + for y in range(self.h): + for x in range(self.w): + cell = self.cell_data[x, y] + cell.options = cell.init_options.copy() + if self.on_step: + self.on_step(self) + propegation_set = set() + for y in range(self.h): + for x in range(self.w): + propegation_set.add((x, y)) + self.propegate(propegation_set) + for y in range(self.h): + for x in range(self.w): + cell = self.cell_data[x, y] + cell.init_options = cell.options.copy() + + def clear(self): + for y in range(self.h): + for x in range(self.w): + cell = self.cell_data[(x, y)] + if cell.result is None: + cell.options = cell.init_options.copy() + + propegation_set = set() + for y in range(self.h): + for x in range(self.w): + cell = self.cell_data[(x, y)] + if cell.result is not None: + propegation_set.add((x, y)) + self.propegate(propegation_set) + + def random_pick(self, cell): + pick_list = list(cell.options) + if not pick_list: + raise ContradictionException(cell.x, cell.y) + freqs = {} + if (cell.x - 1, cell.y) in self.cell_data and len(self.cell_data[(cell.x - 1, cell.y)].options) == 1: + tile_id = next(iter(self.cell_data[(cell.x - 1, cell.y)].options)) + for k, v in self.cell_data[(cell.x - 1, cell.y)].tileset.tiles[tile_id].right_freq.items(): + freqs[k] = freqs.get(k, 0) + v + if (cell.x + 1, cell.y) in self.cell_data and len(self.cell_data[(cell.x + 1, cell.y)].options) == 1: + tile_id = next(iter(self.cell_data[(cell.x + 1, cell.y)].options)) + for k, v in self.cell_data[(cell.x + 1, cell.y)].tileset.tiles[tile_id].left_freq.items(): + freqs[k] = freqs.get(k, 0) + v + if (cell.x, cell.y - 1) in self.cell_data and len(self.cell_data[(cell.x, cell.y - 1)].options) == 1: + tile_id = next(iter(self.cell_data[(cell.x, cell.y - 1)].options)) + for k, v in self.cell_data[(cell.x, cell.y - 1)].tileset.tiles[tile_id].down_freq.items(): + freqs[k] = freqs.get(k, 0) + v + if (cell.x, cell.y + 1) in self.cell_data and len(self.cell_data[(cell.x, cell.y + 1)].options) == 1: + tile_id = next(iter(self.cell_data[(cell.x, cell.y + 1)].options)) + for k, v in self.cell_data[(cell.x, cell.y + 1)].tileset.tiles[tile_id].up_freq.items(): + freqs[k] = freqs.get(k, 0) + v + if freqs: + weights_list = [freqs.get(n, 1) for n in pick_list] + else: + weights_list = [cell.tileset.tiles[n].frequency for n in pick_list] + return random.choices(pick_list, weights_list)[0] + + def build(self, start_x, start_y, w, h): + cell_todo_list = [] + for y in range(start_y, start_y + h): + for x in range(start_x, start_x+w): + cell_todo_list.append(self.cell_data[(x, y)]) + + while cell_todo_list: + cell_todo_list.sort(key=lambda c: len(c.options)) + l0 = len(cell_todo_list[0].options) + idx = 1 + while idx < len(cell_todo_list) and len(cell_todo_list[idx].options) == l0: + idx += 1 + idx = random.randint(0, idx - 1) + cell = cell_todo_list[idx] + if self.on_step: + self.on_step(self, cur=(cell.x, cell.y)) + pick = self.random_pick(cell) + cell_todo_list.pop(idx) + cell.options = {pick} + self.propegate({(cell.x, cell.y)}) + + for y in range(start_y, start_y + h): + for x in range(start_x, start_x + w): + self.cell_data[(x, y)].result = next(iter(self.cell_data[(x, y)].options)) + + def propegate(self, propegation_set): + while propegation_set: + xy = next(iter(propegation_set)) + propegation_set.remove(xy) + + cell = self.cell_data[xy] + if not cell.options: + raise ContradictionException(cell.x, cell.y) + x, y = xy + if (x, y + 1) in self.cell_data and self.cell_data[(x, y + 1)].update_options_down(cell): + propegation_set.add((x, y + 1)) + if (x + 1, y) in self.cell_data and self.cell_data[(x + 1, y)].update_options_right(cell): + propegation_set.add((x + 1, y)) + if (x, y - 1) in self.cell_data and self.cell_data[(x, y - 1)].update_options_up(cell): + propegation_set.add((x, y - 1)) + if (x - 1, y) in self.cell_data and self.cell_data[(x - 1, y)].update_options_left(cell): + propegation_set.add((x - 1, y)) + + def store_tile_data(self, the_map: Map): + for sy in range(the_map.h): + for sx in range(the_map.w): + tiles = [] + for y in range(8): + for x in range(10): + cell = self.cell_data[(x+sx*10, y+sy*8)] + if cell.result is not None: + tiles.append(cell.result) + elif len(cell.options) == 0: + tiles.append(1) + else: + tiles.append(2) + the_map.get(sx, sy).tiles = tiles + + def dump_option_count(self): + for y in range(self.h): + for x in range(self.w): + print(f"{len(self.cell_data[(x, y)].options):2x}", end="") + print() + print() diff --git a/worlds/ladx/LADXR/patches/aesthetics.py b/worlds/ladx/LADXR/patches/aesthetics.py new file mode 100644 index 000000000000..ff8cd5d8564a --- /dev/null +++ b/worlds/ladx/LADXR/patches/aesthetics.py @@ -0,0 +1,436 @@ +from ..assembler import ASM +from ..utils import formatText, setReplacementName +from ..roomEditor import RoomEditor +from .. import entityData +import os +import bsdiff4 + +def imageTo2bpp(filename): + import PIL.Image + baseimg = PIL.Image.new('P', (1,1)) + baseimg.putpalette(( + 128, 0, 128, + 0, 0, 0, + 128, 128, 128, + 255, 255, 255, + )) + img = PIL.Image.open(filename) + img = img.quantize(colors=4, palette=baseimg) + print (f"Palette: {img.getpalette()}") + assert (img.size[0] % 8) == 0 + tileheight = 8 if img.size[1] == 8 else 16 + assert (img.size[1] % tileheight) == 0 + + cols = img.size[0] // 8 + rows = img.size[1] // tileheight + result = bytearray(rows * cols * tileheight * 2) + index = 0 + for ty in range(rows): + for tx in range(cols): + for y in range(tileheight): + a = 0 + b = 0 + for x in range(8): + c = img.getpixel((tx * 8 + x, ty * 16 + y)) + if c & 1: + a |= 0x80 >> x + if c & 2: + b |= 0x80 >> x + result[index] = a + result[index+1] = b + index += 2 + return result + + +def updateGraphics(rom, bank, offset, data): + if offset + len(data) > 0x4000: + updateGraphics(rom, bank, offset, data[:0x4000-offset]) + updateGraphics(rom, bank + 1, 0, data[0x4000 - offset:]) + else: + rom.banks[bank][offset:offset+len(data)] = data + if bank < 0x34: + rom.banks[bank-0x20][offset:offset + len(data)] = data + + +def gfxMod(rom, filename): + if os.path.exists(filename + ".names"): + for line in open(filename + ".names", "rt"): + if ":" in line: + k, v = line.strip().split(":", 1) + setReplacementName(k, v) + + ext = os.path.splitext(filename)[1].lower() + if ext == ".bin": + updateGraphics(rom, 0x2C, 0, open(filename, "rb").read()) + elif ext in (".png", ".bmp"): + updateGraphics(rom, 0x2C, 0, imageTo2bpp(filename)) + elif ext == ".bdiff": + updateGraphics(rom, 0x2C, 0, prepatch(rom, 0x2C, 0, filename)) + elif ext == ".json": + import json + data = json.load(open(filename, "rt")) + + for patch in data: + if "gfx" in patch: + updateGraphics(rom, int(patch["bank"], 16), int(patch["offset"], 16), imageTo2bpp(os.path.join(os.path.dirname(filename), patch["gfx"]))) + if "name" in patch: + setReplacementName(patch["item"], patch["name"]) + else: + updateGraphics(rom, 0x2C, 0, imageTo2bpp(filename)) + + +def createGfxImage(rom, filename): + import PIL.Image + bank_count = 8 + img = PIL.Image.new("P", (32 * 8, 32 * 8 * bank_count)) + img.putpalette(( + 128, 0, 128, + 0, 0, 0, + 128, 128, 128, + 255, 255, 255, + )) + for bank_nr in range(bank_count): + bank = rom.banks[0x2C + bank_nr] + for tx in range(32): + for ty in range(16): + for y in range(16): + a = bank[tx * 32 + ty * 32 * 32 + y * 2] + b = bank[tx * 32 + ty * 32 * 32 + y * 2 + 1] + for x in range(8): + c = 0 + if a & (0x80 >> x): + c |= 1 + if b & (0x80 >> x): + c |= 2 + img.putpixel((tx*8+x, bank_nr * 32 * 8 + ty*16+y), c) + img.save(filename) + +def prepatch(rom, bank, offset, filename): + bank_count = 8 + base_sheet = [] + result = [] + for bank_nr in range(bank_count): + base_sheet[0x4000 * bank_nr:0x4000 * (bank_nr + 1) - 1] = rom.banks[0x2C + bank_nr] + with open(filename, "rb") as patch: + file = patch.read() + result = bsdiff4.patch(src_bytes=bytes(base_sheet), patch_bytes=file) + return result + +def noSwordMusic(rom): + # Skip no-sword music override + # Instead of loading the sword level, we put the value 1 in the A register, indicating we have a sword. + rom.patch(2, 0x0151, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True) + rom.patch(2, 0x3AEF, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True) + rom.patch(3, 0x0996, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True) + rom.patch(3, 0x0B35, ASM("ld a, [$DB44]"), ASM("ld a, $01"), fill_nop=True) + + +def removeNagMessages(rom): + # Remove "this object is heavy, bla bla", and other nag messages when touching an object + rom.patch(0x02, 0x32BB, ASM("ld a, [$C14A]"), ASM("ld a, $01"), fill_nop=True) # crystal blocks + rom.patch(0x02, 0x32EC, ASM("ld a, [$C5A6]"), ASM("ld a, $01"), fill_nop=True) # cracked blocks + rom.patch(0x02, 0x32D3, ASM("jr nz, $25"), ASM("jr $25"), fill_nop=True) # stones/pots + rom.patch(0x02, 0x2B88, ASM("jr nz, $0F"), ASM("jr $0F"), fill_nop=True) # ice blocks + + +def removeLowHPBeep(rom): + rom.patch(2, 0x233A, ASM("ld hl, $FFF3\nld [hl], $04"), b"", fill_nop=True) # Remove health beep + + +def slowLowHPBeep(rom): + rom.patch(2, 0x2338, ASM("ld a, $30"), ASM("ld a, $60")) # slow slow hp beep + + +def removeFlashingLights(rom): + # Remove the switching between two backgrounds at mamu, always show the spotlights. + rom.patch(0x00, 0x01EB, ASM("ldh a, [$E7]\nrrca\nand $80"), ASM("ld a, $80"), fill_nop=True) + # Remove flashing colors from shopkeeper killing you after stealing and the mad batter giving items. + rom.patch(0x24, 0x3B77, ASM("push bc"), ASM("ret")) + + +def forceLinksPalette(rom, index): + # This forces the link sprite into a specific palette index ignoring the tunic options. + rom.patch(0, 0x1D8C, + ASM("ld a, [$DC0F]\nand a\njr z, $03\ninc a"), + ASM("ld a, $%02X" % (index)), fill_nop=True) + rom.patch(0, 0x1DD2, + ASM("ld a, [$DC0F]\nand a\njr z, $03\ninc a"), + ASM("ld a, $%02X" % (index)), fill_nop=True) + # Fix the waking up from bed palette + if index == 1: + rom.patch(0x21, 0x33FC, "A222", "FF05") + elif index == 2: + rom.patch(0x21, 0x33FC, "A222", "3F14") + elif index == 3: + rom.patch(0x21, 0x33FC, "A222", "037E") + for n in range(6): + rom.patch(0x05, 0x1261 + n * 2, "00", f"{index:02x}") + + +def fastText(rom): + rom.patch(0x00, 0x24CA, ASM("jp $2485"), ASM("call $2485")) + + +def noText(rom): + for idx in range(len(rom.texts)): + if not isinstance(rom.texts[idx], int) and (idx < 0x217 or idx > 0x21A): + rom.texts[idx] = rom.texts[idx][-1:] + + +def reduceMessageLengths(rom, rnd): + # Into text from Marin. Got to go fast, so less text. (This intro text is very long) + rom.texts[0x01] = formatText(rnd.choice([ + "Let's a go!", + "Remember, sword goes on A!", + "Avoid the heart piece of shame!", + "Marin? No, this is Zelda. Welcome to Hyrule", + "Why are you in my bed?", + "This is not a Mario game!", + "MuffinJets was here...", + "Remember, there are no bugs in LADX", + "#####, #####, you got to wake up!\nDinner is ready.", + "Go find the stepladder", + "Pizza power!", + "Eastmost penninsula is the secret", + "There is no cow level", + "You cannot lift rocks with your bear hands", + "Thank you, daid!", + "There, there now. Just relax. You've been asleep for almost nine hours now." + ])) + + # Reduce length of a bunch of common texts + rom.texts[0xEA] = formatText("You've got a Guardian Acorn!") + rom.texts[0xEB] = rom.texts[0xEA] + rom.texts[0xEC] = rom.texts[0xEA] + rom.texts[0x08] = formatText("You got a Piece of Power!") + rom.texts[0xEF] = formatText("You found a {SEASHELL}!") + rom.texts[0xA7] = formatText("You've got the {COMPASS}!") + + rom.texts[0x07] = formatText("You need the {NIGHTMARE_KEY}!") + rom.texts[0x8C] = formatText("You need a {KEY}!") # keyhole block + + rom.texts[0x09] = formatText("Ahhh... It has the Sleepy {TOADSTOOL}, it does! We'll mix it up something in a jiffy, we will!") + rom.texts[0x0A] = formatText("The last thing I kin remember was bitin' into a big juicy {TOADSTOOL}... Then, I had the darndest dream... I was a raccoon! Yeah, sounds strange, but it sure was fun!") + rom.texts[0x0F] = formatText("You pick the {TOADSTOOL}... As you hold it over your head, a mellow aroma flows into your nostrils.") + rom.texts[0x13] = formatText("You've learned the ^{SONG1}!^ This song will always remain in your heart!") + rom.texts[0x18] = formatText("Will you give me 28 {RUPEES} for my secret?", ask="Give Don't") + rom.texts[0x19] = formatText("How about it? 42 {RUPEES} for my little secret...", ask="Give Don't") + rom.texts[0x1e] = formatText("...You're so cute! I'll give you a 7 {RUPEE} discount!") + rom.texts[0x2d] = formatText("{ARROWS_10}\n10 {RUPEES}!", ask="Buy Don't") + rom.texts[0x32] = formatText("{SHIELD}\n20 {RUPEES}!", ask="Buy Don't") + rom.texts[0x33] = formatText("Ten {BOMB}\n10 {RUPEES}", ask="Buy Don't") + rom.texts[0x3d] = formatText("It's a {SHIELD}! There is space for your name!") + rom.texts[0x42] = formatText("It's 30 {RUPEES}! You can play the game three more times with this!") + rom.texts[0x45] = formatText("How about some fishing, little buddy? I'll only charge you 10 {RUPEES}...", ask="Fish Not Now") + rom.texts[0x4b] = formatText("Wow! Nice Fish! It's a lunker!! I'll give you a 20 {RUPEE} prize! Try again?", ask="Cast Not Now") + rom.texts[0x4e] = formatText("You're short of {RUPEES}? Don't worry about it. You just come back when you have more money, little buddy.") + rom.texts[0x4f] = formatText("You've got a {HEART_PIECE}! Press SELECT on the Subscreen to see.") + rom.texts[0x8e] = formatText("Well, it's an {OCARINA}, but you don't know how to play it...") + rom.texts[0x90] = formatText("You found the {POWER_BRACELET}! At last, you can pick up pots and stones!") + rom.texts[0x91] = formatText("You got your {SHIELD} back! Press the button and repel enemies with it!") + rom.texts[0x93] = formatText("You've got the {HOOKSHOT}! Its chain stretches long when you use it!") + rom.texts[0x94] = formatText("You've got the {MAGIC_ROD}! Now you can burn things! Burn it! Burn, baby burn!") + rom.texts[0x95] = formatText("You've got the {PEGASUS_BOOTS}! If you hold down the Button, you can dash!") + rom.texts[0x96] = formatText("You've got the {OCARINA}! You should learn to play many songs!") + rom.texts[0x97] = formatText("You've got the {FEATHER}! It feels like your body is a lot lighter!") + rom.texts[0x98] = formatText("You've got a {SHOVEL}! Now you can feel the joy of digging!") + rom.texts[0x99] = formatText("You've got some {MAGIC_POWDER}! Try sprinkling it on a variety of things!") + rom.texts[0x9b] = formatText("You found your {SWORD}! It must be yours because it has your name engraved on it!") + rom.texts[0x9c] = formatText("You've got the {FLIPPERS}! If you press the B Button while you swim, you can dive underwater!") + rom.texts[0x9e] = formatText("You've got a new {SWORD}! You should put your name on it right away!") + rom.texts[0x9f] = formatText("You've got a new {SWORD}! You should put your name on it right away!") + rom.texts[0xa0] = formatText("You found the {MEDICINE}! You should apply this and see what happens!") + rom.texts[0xa1] = formatText("You've got the {TAIL_KEY}! Now you can open the Tail Cave gate!") + rom.texts[0xa2] = formatText("You've got the {SLIME_KEY}! Now you can open the gate in Ukuku Prairie!") + rom.texts[0xa3] = formatText("You've got the {ANGLER_KEY}!") + rom.texts[0xa4] = formatText("You've got the {FACE_KEY}!") + rom.texts[0xa5] = formatText("You've got the {BIRD_KEY}!") + rom.texts[0xa6] = formatText("At last, you got a {MAP}! Press the START Button to look at it!") + rom.texts[0xa8] = formatText("You found a {STONE_BEAK}! Let's find the owl statue that belongs to it.") + rom.texts[0xa9] = formatText("You've got the {NIGHTMARE_KEY}! Now you can open the door to the Nightmare's Lair!") + rom.texts[0xaa] = formatText("You got a {KEY}! You can open a locked door.") + rom.texts[0xab] = formatText("You got 20 {RUPEES}! JOY!", center=True) + rom.texts[0xac] = formatText("You got 50 {RUPEES}! Very Nice!", center=True) + rom.texts[0xad] = formatText("You got 100 {RUPEES}! You're Happy!", center=True) + rom.texts[0xae] = formatText("You got 200 {RUPEES}! You're Ecstatic!", center=True) + rom.texts[0xdc] = formatText("Ribbit! Ribbit! I'm Mamu, on vocals! But I don't need to tell you that, do I? Everybody knows me! Want to hang out and listen to us jam? For 300 Rupees, we'll let you listen to a previously unreleased cut! What do you do?", ask="Pay Leave") + rom.texts[0xe8] = formatText("You've found a {GOLD_LEAF}! Press START to see how many you've collected!") + rom.texts[0xed] = formatText("You've got the Mirror Shield! You can now turnback the beams you couldn't block before!") + rom.texts[0xee] = formatText("You've got a more Powerful {POWER_BRACELET}! Now you can almost lift a whale!") + rom.texts[0xf0] = formatText("Want to go on a raft ride for a hundred {RUPEES}?", ask="Yes No Way") + + +def allowColorDungeonSpritesEverywhere(rom): + # Set sprite set numbers $01-$40 to map to the color dungeon sprites + rom.patch(0x00, 0x2E6F, "00", "15") + # Patch the spriteset loading code to load the 4 entries from the normal table instead of skipping this for color dungeon specific exception weirdness + rom.patch(0x00, 0x0DA4, ASM("jr nc, $05"), ASM("jr nc, $41")) + rom.patch(0x00, 0x0DE5, ASM(""" + ldh a, [$F7] + cp $FF + jr nz, $06 + ld a, $01 + ldh [$91], a + jr $40 + """), ASM(""" + jr $0A ; skip over the rest of the code + cp $FF ; check if color dungeon + jp nz, $0DAB + inc d + jp $0DAA + """), fill_nop=True) + # Disable color dungeon specific tile load hacks + rom.patch(0x00, 0x06A7, ASM("jr nz, $22"), ASM("jr $22")) + rom.patch(0x00, 0x2E77, ASM("jr nz, $0B"), ASM("jr $0B")) + + # Finally fill in the sprite data for the color dungeon + for n in range(22): + data = bytearray() + for m in range(4): + idx = rom.banks[0x20][0x06AA + 44 * m + n * 2] + bank = rom.banks[0x20][0x06AA + 44 * m + n * 2 + 1] + if idx == 0 and bank == 0: + v = 0xFF + elif bank == 0x35: + v = idx - 0x40 + elif bank == 0x31: + v = idx + elif bank == 0x2E: + v = idx + 0x40 + else: + assert False, "%02x %02x" % (idx, bank) + data += bytes([v]) + rom.room_sprite_data_indoor[0x200 + n] = data + + # Patch the graphics loading code to use DMA and load all sets that need to be reloaded, not just the first and last + rom.patch(0x00, 0x06FA, 0x07AF, ASM(""" + ;We enter this code with the right bank selected for tile data copy, + ;d = tile row (source addr = (d*$100+$4000)) + ;e = $00 + ;$C197 = index of sprite set to update (target addr = ($8400 + $100 * [$C197])) + ld a, d + add a, $40 + ldh [$51], a + xor a + ldh [$52], a + ldh [$54], a + ld a, [$C197] + add a, $84 + ldh [$53], a + ld a, $0F + ldh [$55], a + + ; See if we need to do anything next + ld a, [$C10E] ; check the 2nd update flag + and a + jr nz, getNext + ldh [$91], a ; no 2nd update flag, so clear primary update flag + ret + getNext: + ld hl, $C197 + inc [hl] + res 2, [hl] + ld a, [$C10D] + cp [hl] + ret nz + xor a ; clear the 2nd update flag when we prepare to update the last spriteset + ld [$C10E], a + ret + """), fill_nop=True) + rom.patch(0x00, 0x0738, "00" * (0x073E - 0x0738), ASM(""" + ; we get here by some color dungeon specific code jumping to this position + ; We still need that color dungeon specific code as it loads background tiles + xor a + ldh [$91], a + ldh [$93], a + ret + """)) + rom.patch(0x00, 0x073E, "00" * (0x07AF - 0x073E), ASM(""" + ;If we get here, only the 2nd flag is filled and the primary is not. So swap those around. + ld a, [$C10D] ;copy the index number + ld [$C197], a + xor a + ld [$C10E], a ; clear the 2nd update flag + inc a + ldh [$91], a ; set the primary update flag + ret + """), fill_nop=True) + + +def updateSpriteData(rom): + # Change the special sprite change exceptions + rom.patch(0x00, 0x0DAD, 0x0DDB, ASM(""" + ; Check for indoor + ld a, d + and a + jr nz, noChange + ldh a, [$F6] ; hMapRoom + cp $C9 + jr nz, sirenRoomEnd + ld a, [$D8C9] ; wOverworldRoomStatus + ROOM_OW_SIREN + and $20 + jr z, noChange + ld hl, $7837 + jp $0DFE +sirenRoomEnd: + ldh a, [$F6] ; hMapRoom + cp $D8 + jr nz, noChange + ld a, [$D8FD] ; wOverworldRoomStatus + ROOM_OW_WALRUS + and $20 + jr z, noChange + ld hl, $783B + jp $0DFE +noChange: + """), fill_nop=True) + rom.patch(0x20, 0x3837, "A4FF8BFF", "A461FF72") + rom.patch(0x20, 0x383B, "A44DFFFF", "A4C5FF70") + + # For each room update the sprite load data based on which entities are in there. + for room_nr in range(0x316): + if room_nr == 0x2FF: + continue + values = [None, None, None, None] + if room_nr == 0x00E: # D7 entrance opening + values[2] = 0xD6 + values[3] = 0xD7 + if 0x211 <= room_nr <= 0x21E: # D7 throwing ball thing. + values[0] = 0x66 + r = RoomEditor(rom, room_nr) + for obj in r.objects: + if obj.type_id == 0xC5 and room_nr < 0x100: # Pushable Gravestone + values[3] = 0x82 + for x, y, entity in r.entities: + sprite_data = entityData.SPRITE_DATA[entity] + if callable(sprite_data): + sprite_data = sprite_data(r) + if sprite_data is None: + continue + for m in range(0, len(sprite_data), 2): + idx, value = sprite_data[m:m+2] + if values[idx] is None: + values[idx] = value + elif isinstance(values[idx], set) and isinstance(value, set): + values[idx] = values[idx].intersection(value) + assert len(values[idx]) > 0 + elif isinstance(values[idx], set) and value in values[idx]: + values[idx] = value + elif isinstance(value, set) and values[idx] in value: + pass + elif values[idx] == value: + pass + else: + assert False, "Room: %03x cannot load graphics for entity: %02x (Index: %d Failed: %s, Active: %s)" % (room_nr, entity, idx, value, values[idx]) + + data = bytearray() + for v in values: + if isinstance(v, set): + v = next(iter(v)) + elif v is None: + v = 0xff + data.append(v) + + if room_nr < 0x100: + rom.room_sprite_data_overworld[room_nr] = data + else: + rom.room_sprite_data_indoor[room_nr - 0x100] = data diff --git a/worlds/ladx/LADXR/patches/bank34.py b/worlds/ladx/LADXR/patches/bank34.py new file mode 100644 index 000000000000..22abd48b393d --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank34.py @@ -0,0 +1,125 @@ +import os +import binascii +from ..assembler import ASM +from ..utils import formatText + +ItemNameLookupTable = 0x0100 +ItemNameLookupSize = 2 +TotalRoomCount = 0x316 + +AnItemText = "an item" +ItemNameStringBufferStart = ItemNameLookupTable + \ + TotalRoomCount * ItemNameLookupSize + + +def addBank34(rom, item_list): + my_path = os.path.dirname(__file__) + rom.patch(0x34, 0x0000, ItemNameLookupTable, ASM(""" + ; Get the pointer in the lookup table, doubled as it's two bytes + ld hl, $2080 + push de + call OffsetPointerByRoomNumber + pop de + add hl, hl + + ldi a, [hl] ; hl = *hl + ld h, [hl] + ld l, a + + ; If there's no data, bail + ld a, l + or h + jp z, SwitchBackTo3E + + ld de, wCustomMessage + ; Copy "Got " to de + ld a, 71 + ld [de], a + inc de + ld a, 111 + ld [de], a + inc de + ld a, 116 + ld [de], a + inc de + ld a, 32 + ld [de], a + inc de + ; Copy in our item name + call MessageCopyString + SwitchBackTo3E: + ; Bail + ld a, $3e ; Set bank number + jp $080C ; switch bank + + ; this should be shared but I got link errors + OffsetPointerByRoomNumber: + ldh a, [$F6] ; map room + ld e, a + ld a, [$DBA5] ; is indoor + ld d, a + ldh a, [$F7] ; mapId + cp $FF + jr nz, .notColorDungeon + + ld d, $03 + jr .notCavesA + + .notColorDungeon: + cp $1A + jr nc, .notCavesA + cp $06 + jr c, .notCavesA + inc d + .notCavesA: + add hl, de + ret + """ + open(os.path.join(my_path, "bank3e.asm/message.asm"), "rt").read(), 0x4000), fill_nop=True) + + nextItemLookup = ItemNameStringBufferStart + nameLookup = { + + } + + name = AnItemText + + def add_or_get_name(name): + nonlocal nextItemLookup + if name in nameLookup: + return nameLookup[name] + if len(name) + 1 + nextItemLookup >= 0x4000: + return nameLookup[AnItemText] + asm = ASM(f'db "{name}", $ff\n') + rom.patch(0x34, nextItemLookup, None, asm) + patch_len = len(binascii.unhexlify(asm)) + nameLookup[name] = nextItemLookup + 0x4000 + nextItemLookup += patch_len + return nameLookup[name] + + item_text_addr = add_or_get_name(AnItemText) + #error_text_addr = add_or_get_name("Please report this check to #bug-reports in the AP discord") + def swap16(x): + assert x <= 0xFFFF + return (x >> 8) | ((x & 0xFF) << 8) + + def to_hex_address(x): + return f"{swap16(x):04x}" + + # Set defaults for every room + for i in range(TotalRoomCount): + rom.patch(0x34, ItemNameLookupTable + i * + ItemNameLookupSize, None, to_hex_address(0)) + + for item in item_list: + if not item.custom_item_name: + continue + assert item.room < TotalRoomCount, item.room + # Item names of exactly 255 characters will cause overwrites to occur in the text box + # assert len(item.custom_item_name) < 0x100 + # Custom text is only 95 bytes long, restrict to 50 + addr = add_or_get_name(item.custom_item_name[:50]) + rom.patch(0x34, ItemNameLookupTable + item.room * + ItemNameLookupSize, None, to_hex_address(addr)) + if item.extra: + rom.patch(0x34, ItemNameLookupTable + item.extra * + ItemNameLookupSize, None, to_hex_address(addr)) \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/bowwow.asm b/worlds/ladx/LADXR/patches/bank3e.asm/bowwow.asm new file mode 100644 index 000000000000..3480838e2f23 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.asm/bowwow.asm @@ -0,0 +1,303 @@ +CheckIfLoadBowWow: + ; Check has bowwow flag + ld a, [$DB56] + cp $01 + jr nz, .noLoadBowwow + + ldh a, [$F6] ; load map number + cp $22 + jr z, .loadBowwow + cp $23 + jr z, .loadBowwow + cp $24 + jr z, .loadBowwow + cp $32 + jr z, .loadBowwow + cp $33 + jr z, .loadBowwow + cp $34 + jr z, .loadBowwow + +.noLoadBowwow: + ld e, $00 + ret + +.loadBowwow: + ld e, $01 + ret + + +; Special handler for when Bowwow tries to eat an entity. +; Our target entity index is loaded in BC. +BowwowEat: + ; Load the entity type into A + ld hl, $C3A0 ; entity type + add hl, bc + ld a, [hl] + + ; Check if we need special handling for bosses + cp $59 ; Moldorm + jr z, BowwowHurtEnemy + cp $5C ; Genie + jr z, BowwowEatGenie + cp $5B ; SlimeEye + jp z, BowwowEatSlimeEye + cp $65 ; AnglerFish + jr z, BowwowHurtEnemy + cp $5D ; SlimeEel + jp z, BowwowEatSlimeEel + cp $5A ; Facade + jr z, BowwowHurtEnemy + cp $63 ; Eagle + jr z, BowwowHurtEnemy + cp $62 ; Hot head + jp z, BowwowEatHotHead + cp $F9 ; Hardhit beetle + jr z, BowwowHurtEnemy + cp $E6 ; Nightmare (all forms) + jp z, BowwowEatNightmare + + ; Check for special handling for minibosses + cp $87 ; Lanmola + jr z, BowwowHurtEnemy + ; cp $88 ; Armos knight + ; No special handling, just eat him, solves the fight real quick. + cp $81 ; rolling bones + jr z, BowwowHurtEnemy + cp $89 ; Hinox + jr z, BowwowHurtEnemy + cp $8E ; Cue ball + jr z, BowwowHurtEnemy + ;cp $5E ; Gnoma + ;jr z, BowwowHurtEnemy + cp $5F ; Master stalfos + jr z, BowwowHurtEnemy + cp $92 ; Smasher + jp z, BowwowEatSmasher + cp $BC ; Grim Creeper + jp z, BowwowEatGrimCreeper + cp $BE ; Blaino + jr z, BowwowHurtEnemy + cp $F8 ; Giant buzz blob + jr z, BowwowHurtEnemy + cp $F4 ; Avalaunch + jr z, BowwowHurtEnemy + + ; Some enemies + cp $E9 ; Color dungeon shell + jr z, BowwowHurtEnemy + cp $EA ; Color dungeon shell + jr z, BowwowHurtEnemy + cp $EB ; Color dungeon shell + jr z, BowwowHurtEnemy + + ; Play SFX + ld a, $03 + ldh [$F2], a + ; Call normal "destroy entity and drop item" handler + jp $3F50 + +BowwowHurtEnemy: + ; Hurt enemy with damage type zero (sword) + ld a, $00 + ld [$C19E], a + rst $18 + ; Play SFX + ld a, $03 + ldh [$F2], a + ret + +BowwowEatGenie: + ; Get private state to find out if this is a bottle or the genie + ld hl, $C2B0 + add hl, bc + ld a, [hl] + ; Prepare loading state from hl + ld hl, $C290 + add hl, bc + + cp $00 + jr z, .bottle + cp $01 + jr z, .ghost + ret + +.ghost: + ; Get current state + ld a, [hl] + cp $04 ; Flying around without bottle + jr z, BowwowHurtEnemy + ret + +.bottle: + ; Get current state + ld a, [hl] + cp $03 ; Hopping around in bottle + jr z, BowwowHurtEnemy + ret + +BowwowEatSlimeEye: + ; On set privateCountdown2 to $0C to split, when privateState1 is $04 and state is $03 + ld hl, $C290 ; state + add hl, bc + ld a, [hl] + cp $03 + jr nz, .skipSplit + + ld hl, $C2B0 ; private state1 + add hl, bc + ld a, [hl] + cp $04 + jr nz, .skipSplit + + ld hl, $C300 ; private countdown 2 + add hl, bc + ld [hl], $0C + +.skipSplit: + jp BowwowHurtEnemy + +BowwowEatSlimeEel: + ; Get private state to find out if this is the tail or the head + ld hl, $C2B0 + add hl, bc + ld a, [hl] + cp $01 ; not the head, so, skip. + ret nz + + ; Check if we are pulled out of the wall + ld hl, $C290 + add hl, bc + ld a, [hl] + cp $03 ; pulled out of the wall + jr nz, .knockOutOfWall + + ld hl, $D204 + ld a, [hl] + cp $07 + jr nc, .noExtraDamage + inc [hl] +.noExtraDamage: + jp BowwowHurtEnemy + +.knockOutOfWall: + ld [hl], $03 ; set state to $03 + ld hl, $C210 ; Y position + add hl, bc + ld a, [hl] + ld [hl], $60 + cp $48 + jp nc, BowwowHurtEnemy + ld [hl], $30 + jp BowwowHurtEnemy + + +BowwowEatHotHead: + ; Load health of hothead + ld hl, $C360 + add hl, bc + ld a, [hl] + cp $20 + jr c, .lowHp + ld [hl], $20 +.lowHp: + jp BowwowHurtEnemy + +BowwowEatSmasher: + ; Check if this is the ball or the monster + ld hl, $C440 + add hl, bc + ld a, [hl] + and a + ret nz + jp BowwowHurtEnemy + +BowwowEatGrimCreeper: + ; Check if this is the main enemy or the smaller ones. Only kill the small ones + ld hl, $C2B0 + add hl, bc + ld a, [hl] + and a + ret z + jp BowwowHurtEnemy + +BowwowEatNightmare: + ; Check if this is the staircase. + ld hl, $C390 + add hl, bc + ld a, [hl] + cp $02 + ret z + + ; Prepare loading state from hl + ld hl, $C290 + add hl, bc + + ld a, [$D219] ; which form has the nightmare + cp $01 + jr z, .slimeForm + cp $02 + jr z, .agahnimForm + cp $03 ; moldormForm + jp z, BowwowHurtEnemy + cp $04 ; ganon and lanmola + jp z, BowwowHurtEnemy + cp $05 ; dethl + jp z, BowwowHurtEnemy + ; 0 is the intro form + ret + +.slimeForm: + ld a, [hl] + cp $02 + jr z, .canHurtSlime + cp $03 + ret nz + +.canHurtSlime: + ; We need quite some custom handling, normally the nightmare checks very directly if you use powder. + ; No idea why this insta kills the slime form... + ; Change state to hurt state + ld [hl], $07 + ; Set flash count + ld hl, $C420 + add hl, bc + ld [hl], $14 + ; play proper sfx + ld a, $07 + ldh [$F3], a + ld a, $37 + ldh [$F2], a + ; No idea why this is done, but it happens when you use powder on the slime + ld a, $03 + ld [$D220], a + ret + +.agahnimForm: + ld a, [hl] + ; only damage in states 2 to 4 + cp $02 + ret c + cp $04 + ret nc + + ; Decrease health + ld a, [$D220] + inc a + ld [$D220], a + ; If dead, do stuff + cp $04 + jr c, .agahnimNotDeadYet + ld [hl], $07 + ld hl, $C2E0 + add hl, bc + ld [hl], $C0 + ld a, $36 + ldh [$F2], a +.agahnimNotDeadYet: + ld hl, $C420 + add hl, bc + ld [hl], $14 + ld a, $07 + ldh [$F3], a + ret diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm b/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm new file mode 100644 index 000000000000..717a2def1d94 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm @@ -0,0 +1,993 @@ +RenderChestItem: + ldh a, [$F1] ; active sprite + and $80 + jr nz, .renderLargeItem + + ld de, ItemSpriteTable + call $3C77 ; RenderActiveEntitySprite + ret +.renderLargeItem: + ld de, LargeItemSpriteTable + dec d + dec d + call $3BC0 ; RenderActiveEntitySpritePair + + ; If we are an instrument + ldh a, [$F1] + cp $8E + ret c + cp $96 + ret nc + + ; But check if we are not state >3 before that, else the fade-out at the instrument room breaks. + ldh a, [$F0] ; hActiveEntityState + cp $03 + ret nc + + ; Call the color cycling code + xor a + ld [$DC82], a + ld [$DC83], a + ld a, $3e + call $0AD2 + ret + +GiveItemFromChestMultiworld: + call IncreaseCheckCounter + ; Check our "item is for other player" flag + ld hl, $7300 + call OffsetPointerByRoomNumber + ld a, [hl] + ld hl, $0055 + cp [hl] + ret nz + +GiveItemFromChest: + ldh a, [$F1] ; Load active sprite variant + + rst 0 ; JUMP TABLE + dw ChestPowerBracelet; CHEST_POWER_BRACELET + dw ChestShield ; CHEST_SHIELD + dw ChestBow ; CHEST_BOW + dw ChestWithItem ; CHEST_HOOKSHOT + dw ChestWithItem ; CHEST_MAGIC_ROD + dw ChestWithItem ; CHEST_PEGASUS_BOOTS + dw ChestWithItem ; CHEST_OCARINA + dw ChestWithItem ; CHEST_FEATHER + dw ChestWithItem ; CHEST_SHOVEL + dw ChestMagicPowder ; CHEST_MAGIC_POWDER_BAG + dw ChestBomb ; CHEST_BOMB + dw ChestSword ; CHEST_SWORD + dw Flippers ; CHEST_FLIPPERS + dw NoItem ; CHEST_MAGNIFYING_LENS + dw ChestWithItem ; Boomerang (used to be unused) + dw SlimeKey ; ?? right side of your trade quest item + dw Medicine ; CHEST_MEDICINE + dw TailKey ; CHEST_TAIL_KEY + dw AnglerKey ; CHEST_ANGLER_KEY + dw FaceKey ; CHEST_FACE_KEY + dw BirdKey ; CHEST_BIRD_KEY + dw GoldenLeaf ; CHEST_GOLD_LEAF + dw ChestWithCurrentDungeonItem ; CHEST_MAP + dw ChestWithCurrentDungeonItem ; CHEST_COMPASS + dw ChestWithCurrentDungeonItem ; CHEST_STONE_BEAK + dw ChestWithCurrentDungeonItem ; CHEST_NIGHTMARE_KEY + dw ChestWithCurrentDungeonItem ; CHEST_SMALL_KEY + dw AddRupees50 ; CHEST_RUPEES_50 + dw AddRupees20 ; CHEST_RUPEES_20 + dw AddRupees100 ; CHEST_RUPEES_100 + dw AddRupees200 ; CHEST_RUPEES_200 + dw AddRupees500 ; CHEST_RUPEES_500 + dw AddSeashell ; CHEST_SEASHELL + dw NoItem ; CHEST_MESSAGE + dw NoItem ; CHEST_GEL + dw AddKey ; KEY1 + dw AddKey ; KEY2 + dw AddKey ; KEY3 + dw AddKey ; KEY4 + dw AddKey ; KEY5 + dw AddKey ; KEY6 + dw AddKey ; KEY7 + dw AddKey ; KEY8 + dw AddKey ; KEY9 + dw AddMap ; MAP1 + dw AddMap ; MAP2 + dw AddMap ; MAP3 + dw AddMap ; MAP4 + dw AddMap ; MAP5 + dw AddMap ; MAP6 + dw AddMap ; MAP7 + dw AddMap ; MAP8 + dw AddMap ; MAP9 + dw AddCompass ; COMPASS1 + dw AddCompass ; COMPASS2 + dw AddCompass ; COMPASS3 + dw AddCompass ; COMPASS4 + dw AddCompass ; COMPASS5 + dw AddCompass ; COMPASS6 + dw AddCompass ; COMPASS7 + dw AddCompass ; COMPASS8 + dw AddCompass ; COMPASS9 + dw AddStoneBeak ; STONE_BEAK1 + dw AddStoneBeak ; STONE_BEAK2 + dw AddStoneBeak ; STONE_BEAK3 + dw AddStoneBeak ; STONE_BEAK4 + dw AddStoneBeak ; STONE_BEAK5 + dw AddStoneBeak ; STONE_BEAK6 + dw AddStoneBeak ; STONE_BEAK7 + dw AddStoneBeak ; STONE_BEAK8 + dw AddStoneBeak ; STONE_BEAK9 + dw AddNightmareKey ; NIGHTMARE_KEY1 + dw AddNightmareKey ; NIGHTMARE_KEY2 + dw AddNightmareKey ; NIGHTMARE_KEY3 + dw AddNightmareKey ; NIGHTMARE_KEY4 + dw AddNightmareKey ; NIGHTMARE_KEY5 + dw AddNightmareKey ; NIGHTMARE_KEY6 + dw AddNightmareKey ; NIGHTMARE_KEY7 + dw AddNightmareKey ; NIGHTMARE_KEY8 + dw AddNightmareKey ; NIGHTMARE_KEY9 + dw AddToadstool ; Toadstool + dw NoItem ; $51 + dw NoItem ; $52 + dw NoItem ; $53 + dw NoItem ; $54 + dw NoItem ; $55 + dw NoItem ; $56 + dw NoItem ; $57 + dw NoItem ; $58 + dw NoItem ; $59 + dw NoItem ; $5A + dw NoItem ; $5B + dw NoItem ; $5C + dw NoItem ; $5D + dw NoItem ; $5E + dw NoItem ; $5F + dw NoItem ; $60 + dw NoItem ; $61 + dw NoItem ; $62 + dw NoItem ; $63 + dw NoItem ; $64 + dw NoItem ; $65 + dw NoItem ; $66 + dw NoItem ; $67 + dw NoItem ; $68 + dw NoItem ; $69 + dw NoItem ; $6A + dw NoItem ; $6B + dw NoItem ; $6C + dw NoItem ; $6D + dw NoItem ; $6E + dw NoItem ; $6F + dw NoItem ; $70 + dw NoItem ; $71 + dw NoItem ; $72 + dw NoItem ; $73 + dw NoItem ; $74 + dw NoItem ; $75 + dw NoItem ; $76 + dw NoItem ; $77 + dw NoItem ; $78 + dw NoItem ; $79 + dw NoItem ; $7A + dw NoItem ; $7B + dw NoItem ; $7C + dw NoItem ; $7D + dw NoItem ; $7E + dw NoItem ; $7F + dw PieceOfHeart ; Heart piece + dw GiveBowwow + dw Give10Arrows + dw Give1Arrow + dw UpgradeMaxPowder + dw UpgradeMaxBombs + dw UpgradeMaxArrows + dw GiveRedTunic + dw GiveBlueTunic + dw GiveExtraHeart + dw TakeHeart + dw GiveSong1 + dw GiveSong2 + dw GiveSong3 + dw GiveInstrument + dw GiveInstrument + dw GiveInstrument + dw GiveInstrument + dw GiveInstrument + dw GiveInstrument + dw GiveInstrument + dw GiveInstrument + dw GiveRooster + dw GiveTradeItem1 + dw GiveTradeItem2 + dw GiveTradeItem3 + dw GiveTradeItem4 + dw GiveTradeItem5 + dw GiveTradeItem6 + dw GiveTradeItem7 + dw GiveTradeItem8 + dw GiveTradeItem9 + dw GiveTradeItem10 + dw GiveTradeItem11 + dw GiveTradeItem12 + dw GiveTradeItem13 + dw GiveTradeItem14 + +NoItem: + ret + +ChestPowerBracelet: + ld hl, $DB43 ; power bracelet level + jr ChestIncreaseItemLevel + +ChestShield: + ld hl, $DB44 ; shield level + jr ChestIncreaseItemLevel + +ChestSword: + ld hl, $DB4E ; sword level + jr ChestIncreaseItemLevel + +ChestIncreaseItemLevel: + ld a, [hl] + cp $02 + jr z, DoNotIncreaseItemLevel + inc [hl] +DoNotIncreaseItemLevel: + jp ChestWithItem + +ChestBomb: + ld a, [$DB4D] ; bomb count + add a, $10 + daa + ld hl, $DB77 ; max bombs + cp [hl] + jr c, .bombsNotFull + ld a, [hl] +.bombsNotFull: + ld [$DB4D], a + jp ChestWithItem + +ChestBow: + ld a, [$DB45] + cp $20 + jp nc, ChestWithItem + ld a, $20 + ld [$DB45], a + jp ChestWithItem + +ChestMagicPowder: + ; Reset the toadstool state + ld a, $0B + ldh [$A5], a + xor a + ld [$DB4B], a ; has toadstool + + ld a, [$DB4C] ; powder count + add a, $10 + daa + ld hl, $DB76 ; max powder + cp [hl] + jr c, .magicPowderNotFull + ld a, [hl] +.magicPowderNotFull: + ld [$DB4C], a + jp ChestWithItem + + +Flippers: + ld a, $01 + ld [wHasFlippers], a + ret + +Medicine: + ld a, $01 + ld [wHasMedicine], a + ret + +TailKey: + ld a, $01 + ld [$DB11], a + ret + +AnglerKey: + ld a, $01 + ld [$DB12], a + ret + +FaceKey: + ld a, $01 + ld [$DB13], a + ret + +BirdKey: + ld a, $01 + ld [$DB14], a + ret + +SlimeKey: + ld a, $01 + ld [$DB15], a + ret + +GoldenLeaf: + ld hl, wGoldenLeaves + inc [hl] + ret + +AddSeaShell: + ld a, [wSeashellsCount] + inc a + daa + ld [wSeashellsCount], a + ret + +PieceOfHeart: +#IF HARD_MODE + ld a, $FF + ld [$DB93], a +#ENDIF + + ld a, [$DB5C] + inc a + cp $04 + jr z, .FullHeart + ld [$DB5C], a + ret +.FullHeart: + xor a + ld [$DB5C], a + jp GiveExtraHeart + +GiveBowwow: + ld a, $01 + ld [$DB56], a + ret + +ChestInventoryTable: + db $03 ; CHEST_POWER_BRACELET + db $04 ; CHEST_SHIELD + db $05 ; CHEST_BOW + db $06 ; CHEST_HOOKSHOT + db $07 ; CHEST_MAGIC_ROD + db $08 ; CHEST_PEGASUS_BOOTS + db $09 ; CHEST_OCARINA + db $0A ; CHEST_FEATHER + db $0B ; CHEST_SHOVEL + db $0C ; CHEST_MAGIC_POWDER_BAG + db $02 ; CHEST_BOMB + db $01 ; CHEST_SWORD + db $00 ; - (flippers slot) + db $00 ; - (magnifier lens slot) + db $0D ; Boomerang + +ChestWithItem: + ldh a, [$F1] ; Load active sprite variant + ld d, $00 + ld e, a + ld hl, ChestInventoryTable + add hl, de + ld d, [hl] + call $3E6B ; Give Inventory + ret + +ChestWithCurrentDungeonItem: + sub $16 ; a -= CHEST_MAP + ld e, a + ld d, $00 + ld hl, $DBCC ; hasDungeonMap + add hl, de + inc [hl] + call $2802 ; Sync current dungeon items with dungeon specific table + ret + +AddToadstool: + ld d, $0E + call $3E6B ; Give Inventory + ret + +AddKey: + sub $23 ; Make 'A' target dungeon index + ld de, $0004 + jr AddDungeonItem + +AddMap: + sub $2C ; Make 'A' target dungeon index + ld de, $0000 + jr AddDungeonItem + +AddCompass: + sub $35 ; Make 'A' target dungeon index + ld de, $0001 + jr AddDungeonItem + +AddStoneBeak: + sub $3E ; Make 'A' target dungeon index + ld de, $0002 + jr AddDungeonItem + +AddNightmareKey: + sub $47 ; Make 'A' target dungeon index + ld de, $0003 + jr AddDungeonItem + +AddDungeonItem: + cp $08 + jr z, .colorDungeon + ; hl = dungeonitems + type_type + dungeon * 8 + ld hl, $DB16 + add hl, de + push de + ld e, a + add hl, de + add hl, de + add hl, de + add hl, de + add hl, de + pop de + inc [hl] + ; Check if we are in this specific dungeon, and then increase the copied counters as well. + ld hl, $FFF7 ; is current map == target map + cp [hl] + ret nz + ld a, [$DBA5] ; is indoor + and a + ret z + + ld hl, $DBCC + add hl, de + inc [hl] + ret +.colorDungeon: + ; Special case for the color dungeon, which is in a different location in memory. + ld hl, $DDDA + add hl, de + inc [hl] + ldh a, [$F7] ; is current map == color dungeon + cp $ff + ret nz + ld hl, $DBCC + add hl, de + inc [hl] + ret + +AddRupees20: + xor a + ld h, $14 + jr AddRupees + +AddRupees50: + xor a + ld h, $32 + jr AddRupees + +AddRupees100: + xor a + ld h, $64 + jr AddRupees + +AddRupees200: + xor a + ld h, $C8 + jr AddRupees + +AddRupees500: + ld a, $01 + ld h, $F4 + jr AddRupees + +AddRupees: + ld [$DB8F], a + ld a, h + ld [$DB90], a + ld a, $18 + ld [$C3CE], a + ret + +Give1Arrow: + ld a, [$DB45] + inc a + jp FinishGivingArrows + +Give10Arrows: + ld a, [$DB45] + add a, $0A +FinishGivingArrows: + daa + ld [$DB45], a + ld hl, $DB78 + cp [hl] + ret c + ld a, [hl] + ld [$DB45], a + ret + +UpgradeMaxPowder: + ld a, $40 + ld [$DB76], a + ; If we have no powder, we should not increase the current amount, as that would prevent + ; The toadstool from showing up. + ld a, [$DB4C] + and a + ret z + ld a, $40 + ld [$DB4C], a + ret + +UpgradeMaxBombs: + ld a, $60 + ld [$DB77], a + ld [$DB4D], a + ret + +UpgradeMaxArrows: + ld a, $60 + ld [$DB78], a + ld [$DB45], a + ret + +GiveRedTunic: + ld a, $01 + ld [$DC0F], a + ; We use DB6D to store which tunics we have available. + ld a, [wCollectedTunics] + or $01 + ld [wCollectedTunics], a + ret + +GiveBlueTunic: + ld a, $02 + ld [$DC0F], a + ; We use DB6D to store which tunics we have available. + ld a, [wCollectedTunics] + or $02 + ld [wCollectedTunics], a + ret + +GiveExtraHeart: + ; Regen all health + ld hl, $DB93 + ld [hl], $FF + ; Increase max health if health is lower then 14 hearts + ld hl, $DB5B + ld a, $0E + cp [hl] + ret z + inc [hl] + ret + +TakeHeart: + ; First, reduce the max HP + ld hl, $DB5B + ld a, [hl] + cp $01 + ret z + dec a + ld [$DB5B], a + + ; Next, check if we need to reduce our actual HP to keep it below the maximum. + rlca + rlca + rlca + sub $01 + ld hl, $DB5A + cp [hl] + jr nc, .noNeedToReduceHp + ld [hl], a +.noNeedToReduceHp: + ; Finally, give all health back. + ld hl, $DB93 + ld [hl], $FF + ret + +GiveSong1: + ld hl, $DB49 + set 2, [hl] + ld a, $00 + ld [$DB4A], a + ret + +GiveSong2: + ld hl, $DB49 + set 1, [hl] + ld a, $01 + ld [$DB4A], a + ret + +GiveSong3: + ld hl, $DB49 + set 0, [hl] + ld a, $02 + ld [$DB4A], a + ret + +GiveInstrument: + ldh a, [$F1] ; Load active sprite variant + sub $8E + ld d, $00 + ld e, a + ld hl, $db65 ; has instrument table + add hl, de + set 1, [hl] + ret + +GiveRooster: + ld d, $0F + call $3E6B ; Give Inventory (rooster item) + + ;ld a, $01 + ;ld [$DB7B], a ; has rooster + ldh a, [$F9] ; do not spawn rooster in sidescroller + and a + ret z + + ld a, $D5 ; ENTITY_ROOSTER + call $3B86 ; SpawnNewEntity_trampoline + ldh a, [$98] ; LinkX + ld hl, $C200 ; wEntitiesPosXTable + add hl, de + ld [hl], a + ldh a, [$99] ; LinkY + ld hl, $C210 ; wEntitiesPosYTable + add hl, de + ld [hl], a + + ret + +GiveTradeItem1: + ld hl, wTradeSequenceItem + set 0, [hl] + ret +GiveTradeItem2: + ld hl, wTradeSequenceItem + set 1, [hl] + ret +GiveTradeItem3: + ld hl, wTradeSequenceItem + set 2, [hl] + ret +GiveTradeItem4: + ld hl, wTradeSequenceItem + set 3, [hl] + ret +GiveTradeItem5: + ld hl, wTradeSequenceItem + set 4, [hl] + ret +GiveTradeItem6: + ld hl, wTradeSequenceItem + set 5, [hl] + ret +GiveTradeItem7: + ld hl, wTradeSequenceItem + set 6, [hl] + ret +GiveTradeItem8: + ld hl, wTradeSequenceItem + set 7, [hl] + ret +GiveTradeItem9: + ld hl, wTradeSequenceItem2 + set 0, [hl] + ret +GiveTradeItem10: + ld hl, wTradeSequenceItem2 + set 1, [hl] + ret +GiveTradeItem11: + ld hl, wTradeSequenceItem2 + set 2, [hl] + ret +GiveTradeItem12: + ld hl, wTradeSequenceItem2 + set 3, [hl] + ret +GiveTradeItem13: + ld hl, wTradeSequenceItem2 + set 4, [hl] + ret +GiveTradeItem14: + ld hl, wTradeSequenceItem2 + set 5, [hl] + ret + +ItemMessageMultiworld: + ; Check our "item is for other player" flag + ld hl, $7300 + call OffsetPointerByRoomNumber + ld a, [hl] + ld hl, $0055 + cp [hl] + jr nz, ItemMessageForOtherPlayer + +ItemMessage: + ; Fill the custom message slot with this item message. + call BuildItemMessage + ldh a, [$F1] + ld d, $00 + ld e, a + ld hl, ItemMessageTable + add hl, de + ld a, [hl] + cp $90 + jr z, .powerBracelet + cp $3D + jr z, .shield + jp $2385 ; Opendialog in $000-$0FF range + +.powerBracelet: + ; Check the power bracelet level, and give a different message when we get the lv2 bracelet + ld hl, $DB43 ; power bracelet level + bit 1, [hl] + jp z, $2385 ; Opendialog in $000-$0FF range + ld a, $EE + jp $2385 ; Opendialog in $000-$0FF range + +.shield: + ; Check the shield level, and give a different message when we get the lv2 shield + ld hl, $DB44 ; shield level + bit 1, [hl] + jp z, $2385 ; Opendialog in $000-$0FF range + ld a, $ED + jp $2385 ; Opendialog in $000-$0FF range + +ItemMessageForOtherPlayer: + push bc + push hl + push af + call BuildRemoteItemMessage + ld hl, SpaceFor + call MessageCopyString + pop af + call MessageAddPlayerName + pop hl + pop bc + ;dec de + ld a, $C9 + jp $2385 ; Opendialog in $000-$0FF range + +ItemSpriteTable: + db $82, $15 ; CHEST_POWER_BRACELET + db $86, $15 ; CHEST_SHIELD + db $88, $14 ; CHEST_BOW + db $8A, $14 ; CHEST_HOOKSHOT + db $8C, $14 ; CHEST_MAGIC_ROD + db $98, $16 ; CHEST_PEGASUS_BOOTS + db $10, $1F ; CHEST_OCARINA + db $12, $1D ; CHEST_FEATHER + db $96, $17 ; CHEST_SHOVEL + db $0E, $1C ; CHEST_MAGIC_POWDER_BAG + db $80, $15 ; CHEST_BOMB + db $84, $15 ; CHEST_SWORD + db $94, $15 ; CHEST_FLIPPERS + db $9A, $10 ; CHEST_MAGNIFYING_LENS + db $24, $1C ; Boomerang + db $4E, $1C ; Slime key + db $A0, $14 ; CHEST_MEDICINE + db $30, $1C ; CHEST_TAIL_KEY + db $32, $1C ; CHEST_ANGLER_KEY + db $34, $1C ; CHEST_FACE_KEY + db $36, $1C ; CHEST_BIRD_KEY + db $3A, $1C ; CHEST_GOLD_LEAF + db $40, $1C ; CHEST_MAP + db $42, $1D ; CHEST_COMPASS + db $44, $1C ; CHEST_STONE_BEAK + db $46, $1C ; CHEST_NIGHTMARE_KEY + db $4A, $1F ; CHEST_SMALL_KEY + db $A6, $15 ; CHEST_RUPEES_50 (normal blue) + db $38, $19 ; CHEST_RUPEES_20 (red) + db $38, $18 ; CHEST_RUPEES_100 (green) + db $38, $1A ; CHEST_RUPEES_200 (yellow) + db $38, $1A ; CHEST_RUPEES_500 (yellow) + db $9E, $14 ; CHEST_SEASHELL + db $8A, $14 ; CHEST_MESSAGE + db $A0, $14 ; CHEST_GEL + db $4A, $1D ; KEY1 + db $4A, $1D ; KEY2 + db $4A, $1D ; KEY3 + db $4A, $1D ; KEY4 + db $4A, $1D ; KEY5 + db $4A, $1D ; KEY6 + db $4A, $1D ; KEY7 + db $4A, $1D ; KEY8 + db $4A, $1D ; KEY9 + db $40, $1C ; MAP1 + db $40, $1C ; MAP2 + db $40, $1C ; MAP3 + db $40, $1C ; MAP4 + db $40, $1C ; MAP5 + db $40, $1C ; MAP6 + db $40, $1C ; MAP7 + db $40, $1C ; MAP8 + db $40, $1C ; MAP9 + db $42, $1D ; COMPASS1 + db $42, $1D ; COMPASS2 + db $42, $1D ; COMPASS3 + db $42, $1D ; COMPASS4 + db $42, $1D ; COMPASS5 + db $42, $1D ; COMPASS6 + db $42, $1D ; COMPASS7 + db $42, $1D ; COMPASS8 + db $42, $1D ; COMPASS9 + db $44, $1C ; STONE_BEAK1 + db $44, $1C ; STONE_BEAK2 + db $44, $1C ; STONE_BEAK3 + db $44, $1C ; STONE_BEAK4 + db $44, $1C ; STONE_BEAK5 + db $44, $1C ; STONE_BEAK6 + db $44, $1C ; STONE_BEAK7 + db $44, $1C ; STONE_BEAK8 + db $44, $1C ; STONE_BEAK9 + db $46, $1C ; NIGHTMARE_KEY1 + db $46, $1C ; NIGHTMARE_KEY2 + db $46, $1C ; NIGHTMARE_KEY3 + db $46, $1C ; NIGHTMARE_KEY4 + db $46, $1C ; NIGHTMARE_KEY5 + db $46, $1C ; NIGHTMARE_KEY6 + db $46, $1C ; NIGHTMARE_KEY7 + db $46, $1C ; NIGHTMARE_KEY8 + db $46, $1C ; NIGHTMARE_KEY9 + db $4C, $1C ; Toadstool + +LargeItemSpriteTable: + db $AC, $02, $AC, $22 ; heart piece + db $54, $0A, $56, $0A ; bowwow + db $2A, $41, $2A, $61 ; 10 arrows + db $2A, $41, $2A, $61 ; single arrow + db $0E, $1C, $22, $0C ; powder upgrade + db $00, $0D, $22, $0C ; bomb upgrade + db $08, $1C, $22, $0C ; arrow upgrade + db $48, $0A, $48, $2A ; red tunic + db $48, $0B, $48, $2B ; blue tunic + db $2A, $0C, $2A, $2C ; heart container + db $2A, $0F, $2A, $2F ; bad heart container + db $70, $09, $70, $29 ; song 1 + db $72, $0B, $72, $2B ; song 2 + db $74, $08, $74, $28 ; song 3 + db $80, $0E, $82, $0E ; Instrument1 + db $84, $0E, $86, $0E ; Instrument2 + db $88, $0E, $8A, $0E ; Instrument3 + db $8C, $0E, $8E, $0E ; Instrument4 + db $90, $0E, $92, $0E ; Instrument5 + db $94, $0E, $96, $0E ; Instrument6 + db $98, $0E, $9A, $0E ; Instrument7 + db $9C, $0E, $9E, $0E ; Instrument8 + db $A6, $2B, $A4, $2B ; Rooster + db $1A, $0E, $1C, $0E ; TradeItem1 + db $B0, $0C, $B2, $0C ; TradeItem2 + db $B4, $0C, $B6, $0C ; TradeItem3 + db $B8, $0C, $BA, $0C ; TradeItem4 + db $BC, $0C, $BE, $0C ; TradeItem5 + db $C0, $0C, $C2, $0C ; TradeItem6 + db $C4, $0C, $C6, $0C ; TradeItem7 + db $C8, $0C, $CA, $0C ; TradeItem8 + db $CC, $0C, $CE, $0C ; TradeItem9 + db $D0, $0C, $D2, $0C ; TradeItem10 + db $D4, $0D, $D6, $0D ; TradeItem11 + db $D8, $0D, $DA, $0D ; TradeItem12 + db $DC, $0D, $DE, $0D ; TradeItem13 + db $E0, $0D, $E2, $0D ; TradeItem14 + +ItemMessageTable: + db $90, $3D, $89, $93, $94, $95, $96, $97, $98, $99, $9A, $9B, $9C, $9D, $D9, $A2 + db $A0, $A1, $A3, $A4, $A5, $E8, $A6, $A7, $A8, $A9, $AA, $AC, $AB, $AD, $AE, $C9 + db $EF, $BE, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9 + db $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9 + ; $40 + db $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9 + db $0F, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + ; $80 + db $4F, $C8, $CA, $CB, $E2, $E3, $E4, $CC, $CD, $2A, $2B, $C9, $C9, $C9, $C9, $C9 + db $C9, $C9, $C9, $C9, $C9, $C9, $B8, $44, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9 + db $C9, $C9, $C9, $C9, $9D + +RenderDroppedKey: + ;TODO: See EntityInitKeyDropPoint for a few special cases to unload. + +RenderHeartPiece: + ; Check if our chest type is already loaded + ld hl, $C2C0 + add hl, bc + ld a, [hl] + and a + jr nz, .droppedKeyTypeLoaded + inc [hl] + + ;Load the chest type from the chest table. + ld hl, $7800 + call OffsetPointerByRoomNumber + + ld a, [hl] + ldh [$F1], a ; set currentEntitySpriteVariant + call $3B0C ; SetEntitySpriteVariant + + and $80 + ld hl, $C340 + add hl, bc + ld a, [hl] + jr z, .singleSprite + ; We potentially need to fix the physics flags table to allocate 2 sprites for us + and $F8 + or $02 + ld [hl], a + jr .droppedKeyTypeLoaded +.singleSprite: + and $F8 + or $01 + ld [hl], a +.droppedKeyTypeLoaded: + jp RenderChestItem + + +OffsetPointerByRoomNumber: + ldh a, [$F6] ; map room + ld e, a + ld a, [$DBA5] ; is indoor + ld d, a + ldh a, [$F7] ; mapId + cp $FF + jr nz, .notColorDungeon + + ld d, $03 + jr .notCavesA + +.notColorDungeon: + cp $1A + jr nc, .notCavesA + cp $06 + jr c, .notCavesA + inc d +.notCavesA: + add hl, de + ret + +GiveItemAndMessageForRoom: + ;Load the chest type from the chest table. + ld hl, $7800 + call OffsetPointerByRoomNumber + ld a, [hl] + ldh [$F1], a + call GiveItemFromChest + jp ItemMessage + +GiveItemAndMessageForRoomMultiworld: + ;Load the chest type from the chest table. + ld hl, $7800 + call OffsetPointerByRoomNumber + ld a, [hl] + ldh [$F1], a + call GiveItemFromChestMultiworld + jp ItemMessageMultiworld + +RenderItemForRoom: + ;Load the chest type from the chest table. + ld hl, $7800 + call OffsetPointerByRoomNumber + ld a, [hl] + ldh [$F1], a + jp RenderChestItem + +; Increase the amount of checks we completed, unless we are on the multichest room. +IncreaseCheckCounter: + ldh a, [$F6] ; map room + cp $F2 + jr nz, .noMultiChest + ld a, [$DBA5] ; is indoor + and a + jr z, .noMultiChest + ldh a, [$F7] ; mapId + cp $0A + ret z + +.noMultiChest: + call $27D0 ; Enable SRAM + ld hl, $B010 +.loop: + ld a, [hl] + and a ; clear carry flag + inc a + daa + ldi [hl], a + ret nc + jr .loop diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/itemnames.asm b/worlds/ladx/LADXR/patches/bank3e.asm/itemnames.asm new file mode 100644 index 000000000000..8495d0898b17 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.asm/itemnames.asm @@ -0,0 +1,494 @@ + +BuildRemoteItemMessage: + ld de, wCustomMessage + call CustomItemMessageThreeFour + ld a, $A0 ; low of wCustomMessage + cp e + ret nz + +BuildItemMessage: + ld hl, ItemNamePointers + ldh a, [$F1] + ld d, $00 + ld e, a + add hl, de + add hl, de + ldi a, [hl] + ld h, [hl] + ld l, a + ld de, wCustomMessage + jp MessageCopyString + + ; And then see if the custom item message func wants to override + + ; add hl, de + + +CustomItemMessageThreeFour: + ; the stack _should_ have the address to return to here, so we can just pop it when we're done + ld a, $34 ; Set bank number + ld hl, $4000 ; Set next address + push hl + jp $080C ; switch bank + +FoundItemForOtherPlayerPostfix: + db m" for player X", $ff +GotItemFromOtherPlayerPostfix: + db m" from player X", $ff +SpaceFrom: + db " from ", $ff, $ff +SpaceFor: + db " for ", $ff, $ff +MessagePad: + jr .start ; goto start +.loop: + ld a, $20 ; a = ' ' + ld [de], a ; *de = ' ' + inc de ; de++ + ld a, $ff ; a = 0xFF + ld [de], a ; *de = 0xff +.start: + ld a, e ; a = de & 0xF + and $0F ; a &= 0x0xF + jr nz, .loop ; if a != 0, goto loop + ret + +MessageAddTargetPlayer: + call MessagePad + ld hl, FoundItemForOtherPlayerPostfix + call MessageCopyString + ret + +MessageAddFromPlayerOld: + call MessagePad + ld hl, GotItemFromOtherPlayerPostfix + call MessageCopyString + ret + +; hahaha none of this follows calling conventions +MessageAddPlayerName: + ; call MessagePad + ld h, 0 ; bc = a, hl = a + ld l, a + ld b, 0 + ld c, a + add hl, hl ; 2 + add hl, hl ; 4 + add hl, hl ; 8 + add hl, hl ; 16 + add hl, bc ; 17 + ld bc, MultiNamePointers + add hl, bc ; hl = MultiNamePointers + wLinkGiveItemFrom * 17 + call MessageCopyString + ret + +ItemNamePointers: + dw ItemNamePowerBracelet + dw ItemNameShield + dw ItemNameBow + dw ItemNameHookshot + dw ItemNameMagicRod + dw ItemNamePegasusBoots + dw ItemNameOcarina + dw ItemNameFeather + dw ItemNameShovel + dw ItemNameMagicPowder + dw ItemNameBomb + dw ItemNameSword + dw ItemNameFlippers + dw ItemNameNone + dw ItemNameBoomerang + dw ItemNameSlimeKey + dw ItemNameMedicine + dw ItemNameTailKey + dw ItemNameAnglerKey + dw ItemNameFaceKey + dw ItemNameBirdKey + dw ItemNameGoldLeaf + dw ItemNameMap + dw ItemNameCompass + dw ItemNameStoneBeak + dw ItemNameNightmareKey + dw ItemNameSmallKey + dw ItemNameRupees50 + dw ItemNameRupees20 + dw ItemNameRupees100 + dw ItemNameRupees200 + dw ItemNameRupees500 + dw ItemNameSeashell + dw ItemNameMessage + dw ItemNameGel + dw ItemNameKey1 + dw ItemNameKey2 + dw ItemNameKey3 + dw ItemNameKey4 + dw ItemNameKey5 + dw ItemNameKey6 + dw ItemNameKey7 + dw ItemNameKey8 + dw ItemNameKey9 + dw ItemNameMap1 + dw ItemNameMap2 + dw ItemNameMap3 + dw ItemNameMap4 + dw ItemNameMap5 + dw ItemNameMap6 + dw ItemNameMap7 + dw ItemNameMap8 + dw ItemNameMap9 + dw ItemNameCompass1 + dw ItemNameCompass2 + dw ItemNameCompass3 + dw ItemNameCompass4 + dw ItemNameCompass5 + dw ItemNameCompass6 + dw ItemNameCompass7 + dw ItemNameCompass8 + dw ItemNameCompass9 + dw ItemNameStoneBeak1 + dw ItemNameStoneBeak2 + dw ItemNameStoneBeak3 + dw ItemNameStoneBeak4 + dw ItemNameStoneBeak5 + dw ItemNameStoneBeak6 + dw ItemNameStoneBeak7 + dw ItemNameStoneBeak8 + dw ItemNameStoneBeak9 + dw ItemNameNightmareKey1 + dw ItemNameNightmareKey2 + dw ItemNameNightmareKey3 + dw ItemNameNightmareKey4 + dw ItemNameNightmareKey5 + dw ItemNameNightmareKey6 + dw ItemNameNightmareKey7 + dw ItemNameNightmareKey8 + dw ItemNameNightmareKey9 + dw ItemNameToadstool + dw ItemNameNone ; 0x51 + dw ItemNameNone ; 0x52 + dw ItemNameNone ; 0x53 + dw ItemNameNone ; 0x54 + dw ItemNameNone ; 0x55 + dw ItemNameNone ; 0x56 + dw ItemNameNone ; 0x57 + dw ItemNameNone ; 0x58 + dw ItemNameNone ; 0x59 + dw ItemNameNone ; 0x5a + dw ItemNameNone ; 0x5b + dw ItemNameNone ; 0x5c + dw ItemNameNone ; 0x5d + dw ItemNameNone ; 0x5e + dw ItemNameNone ; 0x5f + dw ItemNameNone ; 0x60 + dw ItemNameNone ; 0x61 + dw ItemNameNone ; 0x62 + dw ItemNameNone ; 0x63 + dw ItemNameNone ; 0x64 + dw ItemNameNone ; 0x65 + dw ItemNameNone ; 0x66 + dw ItemNameNone ; 0x67 + dw ItemNameNone ; 0x68 + dw ItemNameNone ; 0x69 + dw ItemNameNone ; 0x6a + dw ItemNameNone ; 0x6b + dw ItemNameNone ; 0x6c + dw ItemNameNone ; 0x6d + dw ItemNameNone ; 0x6e + dw ItemNameNone ; 0x6f + dw ItemNameNone ; 0x70 + dw ItemNameNone ; 0x71 + dw ItemNameNone ; 0x72 + dw ItemNameNone ; 0x73 + dw ItemNameNone ; 0x74 + dw ItemNameNone ; 0x75 + dw ItemNameNone ; 0x76 + dw ItemNameNone ; 0x77 + dw ItemNameNone ; 0x78 + dw ItemNameNone ; 0x79 + dw ItemNameNone ; 0x7a + dw ItemNameNone ; 0x7b + dw ItemNameNone ; 0x7c + dw ItemNameNone ; 0x7d + dw ItemNameNone ; 0x7e + dw ItemNameNone ; 0x7f + dw ItemNameHeartPiece ; 0x80 + dw ItemNameBowwow + dw ItemName10Arrows + dw ItemNameSingleArrow + dw ItemNamePowderUpgrade + dw ItemNameBombUpgrade + dw ItemNameArrowUpgrade + dw ItemNameRedTunic + dw ItemNameBlueTunic + dw ItemNameHeartContainer + dw ItemNameBadHeartContainer + dw ItemNameSong1 + dw ItemNameSong2 + dw ItemNameSong3 + dw ItemInstrument1 + dw ItemInstrument2 + dw ItemInstrument3 + dw ItemInstrument4 + dw ItemInstrument5 + dw ItemInstrument6 + dw ItemInstrument7 + dw ItemInstrument8 + dw ItemRooster + dw ItemTradeQuest1 + dw ItemTradeQuest2 + dw ItemTradeQuest3 + dw ItemTradeQuest4 + dw ItemTradeQuest5 + dw ItemTradeQuest6 + dw ItemTradeQuest7 + dw ItemTradeQuest8 + dw ItemTradeQuest9 + dw ItemTradeQuest10 + dw ItemTradeQuest11 + dw ItemTradeQuest12 + dw ItemTradeQuest13 + dw ItemTradeQuest14 + +ItemNameNone: + db m"NONE", $ff + +ItemNamePowerBracelet: + db m"Got the {POWER_BRACELET}", $ff +ItemNameShield: + db m"Got a {SHIELD}", $ff +ItemNameBow: + db m"Got the {BOW}", $ff +ItemNameHookshot: + db m"Got the {HOOKSHOT}", $ff +ItemNameMagicRod: + db m"Got the {MAGIC_ROD}", $ff +ItemNamePegasusBoots: + db m"Got the {PEGASUS_BOOTS}", $ff +ItemNameOcarina: + db m"Got the {OCARINA}", $ff +ItemNameFeather: + db m"Got the {FEATHER}", $ff +ItemNameShovel: + db m"Got the {SHOVEL}", $ff +ItemNameMagicPowder: + db m"Got {MAGIC_POWDER}", $ff +ItemNameBomb: + db m"Got {BOMB}", $ff +ItemNameSword: + db m"Got a {SWORD}", $ff +ItemNameFlippers: + db m"Got the {FLIPPERS}", $ff +ItemNameBoomerang: + db m"Got the {BOOMERANG}", $ff +ItemNameSlimeKey: + db m"Got the {SLIME_KEY}", $ff +ItemNameMedicine: + db m"Got some {MEDICINE}", $ff +ItemNameTailKey: + db m"Got the {TAIL_KEY}", $ff +ItemNameAnglerKey: + db m"Got the {ANGLER_KEY}", $ff +ItemNameFaceKey: + db m"Got the {FACE_KEY}", $ff +ItemNameBirdKey: + db m"Got the {BIRD_KEY}", $ff +ItemNameGoldLeaf: + db m"Got the {GOLD_LEAF}", $ff +ItemNameMap: + db m"Got the {MAP}", $ff +ItemNameCompass: + db m"Got the {COMPASS}", $ff +ItemNameStoneBeak: + db m"Got the {STONE_BEAK}", $ff +ItemNameNightmareKey: + db m"Got the {NIGHTMARE_KEY}", $ff +ItemNameSmallKey: + db m"Got a {KEY}", $ff +ItemNameRupees50: + db m"Got 50 {RUPEES}", $ff +ItemNameRupees20: + db m"Got 20 {RUPEES}", $ff +ItemNameRupees100: + db m"Got 100 {RUPEES}", $ff +ItemNameRupees200: + db m"Got 200 {RUPEES}", $ff +ItemNameRupees500: + db m"Got 500 {RUPEES}", $ff +ItemNameSeashell: + db m"Got a {SEASHELL}", $ff +ItemNameGel: + db m"Got a Zol Attack", $ff +ItemNameMessage: + db m"Got ... nothing?", $ff +ItemNameKey1: + db m"Got a {KEY1}", $ff +ItemNameKey2: + db m"Got a {KEY2}", $ff +ItemNameKey3: + db m"Got a {KEY3}", $ff +ItemNameKey4: + db m"Got a {KEY4}", $ff +ItemNameKey5: + db m"Got a {KEY5}", $ff +ItemNameKey6: + db m"Got a {KEY6}", $ff +ItemNameKey7: + db m"Got a {KEY7}", $ff +ItemNameKey8: + db m"Got a {KEY8}", $ff +ItemNameKey9: + db m"Got a {KEY9}", $ff +ItemNameMap1: + db m"Got the {MAP1}", $ff +ItemNameMap2: + db m"Got the {MAP2}", $ff +ItemNameMap3: + db m"Got the {MAP3}", $ff +ItemNameMap4: + db m"Got the {MAP4}", $ff +ItemNameMap5: + db m"Got the {MAP5}", $ff +ItemNameMap6: + db m"Got the {MAP6}", $ff +ItemNameMap7: + db m"Got the {MAP7}", $ff +ItemNameMap8: + db m"Got the {MAP8}", $ff +ItemNameMap9: + db m"Got the {MAP9}", $ff +ItemNameCompass1: + db m"Got the {COMPASS1}", $ff +ItemNameCompass2: + db m"Got the {COMPASS2}", $ff +ItemNameCompass3: + db m"Got the {COMPASS3}", $ff +ItemNameCompass4: + db m"Got the {COMPASS4}", $ff +ItemNameCompass5: + db m"Got the {COMPASS5}", $ff +ItemNameCompass6: + db m"Got the {COMPASS6}", $ff +ItemNameCompass7: + db m"Got the {COMPASS7}", $ff +ItemNameCompass8: + db m"Got the {COMPASS8}", $ff +ItemNameCompass9: + db m"Got the {COMPASS9}", $ff +ItemNameStoneBeak1: + db m"Got the {STONE_BEAK1}", $ff +ItemNameStoneBeak2: + db m"Got the {STONE_BEAK2}", $ff +ItemNameStoneBeak3: + db m"Got the {STONE_BEAK3}", $ff +ItemNameStoneBeak4: + db m"Got the {STONE_BEAK4}", $ff +ItemNameStoneBeak5: + db m"Got the {STONE_BEAK5}", $ff +ItemNameStoneBeak6: + db m"Got the {STONE_BEAK6}", $ff +ItemNameStoneBeak7: + db m"Got the {STONE_BEAK7}", $ff +ItemNameStoneBeak8: + db m"Got the {STONE_BEAK8}", $ff +ItemNameStoneBeak9: + db m"Got the {STONE_BEAK9}", $ff +ItemNameNightmareKey1: + db m"Got the {NIGHTMARE_KEY1}", $ff +ItemNameNightmareKey2: + db m"Got the {NIGHTMARE_KEY2}", $ff +ItemNameNightmareKey3: + db m"Got the {NIGHTMARE_KEY3}", $ff +ItemNameNightmareKey4: + db m"Got the {NIGHTMARE_KEY4}", $ff +ItemNameNightmareKey5: + db m"Got the {NIGHTMARE_KEY5}", $ff +ItemNameNightmareKey6: + db m"Got the {NIGHTMARE_KEY6}", $ff +ItemNameNightmareKey7: + db m"Got the {NIGHTMARE_KEY7}", $ff +ItemNameNightmareKey8: + db m"Got the {NIGHTMARE_KEY8}", $ff +ItemNameNightmareKey9: + db m"Got the {NIGHTMARE_KEY9}", $ff +ItemNameToadstool: + db m"Got the {TOADSTOOL}", $ff + +ItemNameHeartPiece: + db m"Got the {HEART_PIECE}", $ff +ItemNameBowwow: + db m"Got the {BOWWOW}", $ff +ItemName10Arrows: + db m"Got {ARROWS_10}", $ff +ItemNameSingleArrow: + db m"Got the {SINGLE_ARROW}", $ff +ItemNamePowderUpgrade: + db m"Got the {MAX_POWDER_UPGRADE}", $ff +ItemNameBombUpgrade: + db m"Got the {MAX_BOMBS_UPGRADE}", $ff +ItemNameArrowUpgrade: + db m"Got the {MAX_ARROWS_UPGRADE}", $ff +ItemNameRedTunic: + db m"Got the {RED_TUNIC}", $ff +ItemNameBlueTunic: + db m"Got the {BLUE_TUNIC}", $ff +ItemNameHeartContainer: + db m"Got a {HEART_CONTAINER}", $ff +ItemNameBadHeartContainer: + db m"Got the {BAD_HEART_CONTAINER}", $ff +ItemNameSong1: + db m"Got the {SONG1}", $ff +ItemNameSong2: + db m"Got {SONG2}", $ff +ItemNameSong3: + db m"Got {SONG3}", $ff + +ItemInstrument1: + db m"You've got the {INSTRUMENT1}", $ff +ItemInstrument2: + db m"You've got the {INSTRUMENT2}", $ff +ItemInstrument3: + db m"You've got the {INSTRUMENT3}", $ff +ItemInstrument4: + db m"You've got the {INSTRUMENT4}", $ff +ItemInstrument5: + db m"You've got the {INSTRUMENT5}", $ff +ItemInstrument6: + db m"You've got the {INSTRUMENT6}", $ff +ItemInstrument7: + db m"You've got the {INSTRUMENT7}", $ff +ItemInstrument8: + db m"You've got the {INSTRUMENT8}", $ff + +ItemRooster: + db m"You've got the {ROOSTER}", $ff + +ItemTradeQuest1: + db m"You've got the Yoshi Doll", $ff +ItemTradeQuest2: + db m"You've got the Ribbon", $ff +ItemTradeQuest3: + db m"You've got the Dog Food", $ff +ItemTradeQuest4: + db m"You've got the Bananas", $ff +ItemTradeQuest5: + db m"You've got the Stick", $ff +ItemTradeQuest6: + db m"You've got the Honeycomb", $ff +ItemTradeQuest7: + db m"You've got the Pineapple", $ff +ItemTradeQuest8: + db m"You've got the Hibiscus", $ff +ItemTradeQuest9: + db m"You've got the Letter", $ff +ItemTradeQuest10: + db m"You've got the Broom", $ff +ItemTradeQuest11: + db m"You've got the Fishing Hook", $ff +ItemTradeQuest12: + db m"You've got the Necklace", $ff +ItemTradeQuest13: + db m"You've got the Scale", $ff +ItemTradeQuest14: + db m"You've got the Magnifying Lens", $ff + +MultiNamePointers: \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/link.asm b/worlds/ladx/LADXR/patches/bank3e.asm/link.asm new file mode 100644 index 000000000000..266dd5fc5b61 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.asm/link.asm @@ -0,0 +1,89 @@ +; Handle the serial link cable +#IF HARDWARE_LINK +; FF> = Idle +; D6> = Read: D0><[L] D1><[H] [HL]> +; D9> = Write: D8><[L] D9><[H] DA><[^DATA] DB><[DATA] +; DD> = OrW: D8><[L] D9><[H] DA><[^DATA] DB><[DATA] (used to set flags without requiring a slow read,modify,write race condition) + +handleSerialLink: + ; Check if we got a byte from hardware + ldh a, [$01] + + cp $D6 + jr z, serialReadMem + cp $D9 + jr z, serialWriteMem + cp $DD + jr z, serialOrMem + +finishSerialLink: + ; Do a new idle transfer. + ld a, $E4 + ldh [$01], a + ld a, $81 + ldh [$02], a + ret + +serialReadMem: + ld a, $D0 + call serialTransfer + ld h, a + ld a, $D1 + call serialTransfer + ld l, a + ld a, [hl] + call serialTransfer + jr finishSerialLink + +serialWriteMem: + ld a, $D8 + call serialTransfer + ld h, a + ld a, $D9 + call serialTransfer + ld l, a + ld a, $DA + call serialTransfer + cpl + ld c, a + ld a, $DB + call serialTransfer + cp c + jr nz, finishSerialLink + ld [hl], a + jr finishSerialLink + +serialOrMem: + ld a, $D8 + call serialTransfer + ld h, a + ld a, $D9 + call serialTransfer + ld l, a + ld a, $DA + call serialTransfer + cpl + ld c, a + ld a, $DB + call serialTransfer + cp c + jr nz, finishSerialLink + ld c, a + ld a, [hl] + or c + ld [hl], a + jr finishSerialLink + +; Transfer A to the serial link and wait for it to be done and return the result in A +serialTransfer: + ldh [$01], a + ld a, $81 + ldh [$02], a +.loop: + ldh a, [$02] + and $80 + jr nz, .loop + ldh a, [$01] + ret + +#ENDIF diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/message.asm b/worlds/ladx/LADXR/patches/bank3e.asm/message.asm new file mode 100644 index 000000000000..33062c6e9bca --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.asm/message.asm @@ -0,0 +1,16 @@ +MessageCopyString: +.loop: + ldi a, [hl] + ld [de], a + cp $ff + ret z + inc de + jr .loop + +MessageAddSpace: + ld a, $20 + ld [de], a + inc de + ld a, $ff + ld [de], a + ret diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/multiworld.asm b/worlds/ladx/LADXR/patches/bank3e.asm/multiworld.asm new file mode 100644 index 000000000000..d7804cba6b12 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.asm/multiworld.asm @@ -0,0 +1,355 @@ +; Handle the multiworld link + +MainLoop: +#IF HARDWARE_LINK + call handleSerialLink +#ENDIF + ; Check if the gameplay is world + ld a, [$DB95] + cp $0B + ret nz + ; Check if the world subtype is the normal one + ld a, [$DB96] + cp $07 + ret nz + ; Check if we are moving between rooms + ld a, [$C124] + and a + ret nz + ; Check if link is in a normal walking/swimming state + ld a, [$C11C] + cp $02 + ret nc + ; Check if a dialog is open + ld a, [$C19F] + and a + ret nz + ; Check if interaction is blocked + ldh a, [$A1] + and a + ret nz + + ld a, [wLinkSpawnDelay] + and a + jr z, .allowSpawn + dec a + ld [wLinkSpawnDelay], a + jr .noSpawn + +.allowSpawn: + ld a, [wZolSpawnCount] + and a + call nz, LinkSpawnSlime + ld a, [wCuccoSpawnCount] + and a + call nz, LinkSpawnCucco + ld a, [wDropBombSpawnCount] + and a + call nz, LinkSpawnBomb +.noSpawn: + + ; Have an item to give? + ld hl, wLinkStatusBits + bit 0, [hl] + ret z + + ; Give an item to the player + ld a, [wLinkGiveItem] + ; if zol: + cp $22 ; zol item + jr z, LinkGiveSlime + ; if special item + cp $F0 + jr nc, HandleSpecialItem + ; tmpChestItem = a + ldh [$F1], a + ; Give the item + call GiveItemFromChest + ; Paste the item text + call BuildItemMessage + ; Paste " from " + ld hl, SpaceFrom + call MessageCopyString + ; Paste the player name + ld a, [wLinkGiveItemFrom] + call MessageAddPlayerName + ld a, $C9 + ; hl = $wLinkStatusBits + ld hl, wLinkStatusBits + ; clear the 0 bit of *hl + res 0, [hl] + ; OpenDialog() + jp $2385 ; Opendialog in $000-$0FF range + +LinkGiveSlime: + ld a, $05 + ld [wZolSpawnCount], a + ld hl, wLinkStatusBits + res 0, [hl] + ret + +HandleSpecialItem: + ld hl, wLinkStatusBits + res 0, [hl] + + and $0F + rst 0 + dw SpecialSlimeStorm + dw SpecialCuccoParty + dw SpecialPieceOfPower + dw SpecialHealth + dw SpecialRandomTeleport + dw .ret + dw .ret + dw .ret + dw .ret + dw .ret + dw .ret + dw .ret + dw .ret + dw .ret + dw .ret + dw .ret +.ret: + ret + +SpecialSlimeStorm: + ld a, $20 + ld [wZolSpawnCount], a + ret +SpecialCuccoParty: + ld a, $20 + ld [wCuccoSpawnCount], a + ret +SpecialPieceOfPower: + ; Give the piece of power and the music + ld a, $01 + ld [$D47C], a + ld a, $27 + ld [$D368], a + ld a, $49 + ldh [$BD], a + ldh [$BF], a + ret +SpecialHealth: + ; Regen all health + ld hl, $DB93 + ld [hl], $FF + ret + +LinkSpawnSlime: + ld a, $1B + ld e, $08 + call $3B98 ; SpawnNewEntity in range + ret c + + ; Place somewhere random + call placeRandom + + ld hl, $C310 + add hl, de + ld [hl], $7F + + ld hl, wZolSpawnCount + dec [hl] + + call $280D + and $03 + ld [wLinkSpawnDelay], a + ret + +LinkSpawnCucco: + ld a, $6C + ld e, $04 + call $3B98 ; SpawnNewEntity in range + ret c + + ; Place where link is at. + ld hl, $C200 + add hl, de + ldh a, [$98] + ld [hl], a + ld hl, $C210 + add hl, de + ldh a, [$99] + ld [hl], a + + ; Set the "hits till cucco killer attack" much lower + ld hl, $C2B0 + add hl, de + ld a, $21 + ld [hl], a + + ld hl, wCuccoSpawnCount + dec [hl] + + call $280D + and $07 + ld [wLinkSpawnDelay], a + ret + +LinkSpawnBomb: + ld a, $02 + ld e, $08 + call $3B98 ; SpawnNewEntity in range + ret c + + call placeRandom + + ld hl, $C310 ; z pos + add hl, de + ld [hl], $4F + + ld hl, $C430 ; wEntitiesOptions1Table + add hl, de + res 0, [hl] + ld hl, $C2E0 ; wEntitiesTransitionCountdownTable + add hl, de + ld [hl], $80 + ld hl, $C440 ; wEntitiesPrivateState4Table + add hl, de + ld [hl], $01 + + ld hl, wDropBombSpawnCount + dec [hl] + + call $280D + and $1F + ld [wLinkSpawnDelay], a + ret + +placeRandom: + ; Place somewhere random + ld hl, $C200 + add hl, de + call $280D ; random number + and $7F + add a, $08 + ld [hl], a + ld hl, $C210 + add hl, de + call $280D ; random number + and $3F + add a, $20 + ld [hl], a + ret + +SpecialRandomTeleport: + xor a + ; Warp data + ld [$D401], a + ld [$D402], a + call $280D ; random number + ld [$D403], a + ld hl, RandomTeleportPositions + ld d, $00 + ld e, a + add hl, de + ld e, [hl] + ld a, e + and $0F + swap a + add a, $08 + ld [$D404], a + ld a, e + and $F0 + add a, $10 + ld [$D405], a + + ldh a, [$98] + swap a + and $0F + ld e, a + ldh a, [$99] + sub $08 + and $F0 + or e + ld [$D416], a ; wWarp0PositionTileIndex + + call $0C7D + ld a, $07 + ld [$DB96], a ; wGameplaySubtype + + ret + +Data_004_7AE5: ; @TODO Palette data + db $33, $62, $1A, $01, $FF, $0F, $FF, $7F + + +Deathlink: + ; Spawn the entity + ld a, $CA ; $7AF3: $3E $CA + call $3B86 ; $7AF5: $CD $86 $3B ;SpawnEntityTrampoline + ld a, $26 ; $7AF8: $3E $26 ; + ldh [$F4], a ; $7AFA: $E0 $F4 ; set noise + ; Set posX = linkX + ldh a, [$98] ; LinkX + ld hl, $C200 ; wEntitiesPosXTable + add hl, de + ld [hl], a + ; set posY = linkY - 54 + ldh a, [$99] ; LinkY + sub a, 54 + ld hl, $C210 ; wEntitiesPosYTable + add hl, de + ld [hl], a + ; wEntitiesPrivateState3Table + ld hl, $C2D0 ; $7B0A: $21 $D0 $C2 + add hl, de ; $7B0D: $19 + ld [hl], $01 ; $7B0E: $36 $01 + ; wEntitiesTransitionCountdownTable + ld hl, $C2E0 ; $7B10: $21 $E0 $C2 + add hl, de ; $7B13: $19 + ld [hl], $C0 ; $7B14: $36 $C0 + ; GetEntityTransitionCountdown + call $0C05 ; $7B16: $CD $05 $0C + ld [hl], $C0 ; $7B19: $36 $C0 + ; IncrementEntityState + call $3B12 ; $7B1B: $CD $12 $3B + + ; Remove medicine + xor a ; $7B1E: $AF + ld [$DB0D], a ; $7B1F: $EA $0D $DB ; ld [wHasMedicine], a + ; Reduce health by a lot + ld a, $FF ; $7B22: $3E $FF + ld [$DB94], a ; $7B24: $EA $94 $DB ; ld [wSubtractHealthBuffer], a + + ld hl, $DC88 ; $7B2C: $21 $88 $DC + ; Set palette + ld de, Data_004_7AE5 ; $7B2F: $11 $E5 $7A + +loop_7B32: + ld a, [de] ; $7B32: $1A + ; ld [hl+], a ; $7B33: $22 + db $22 + inc de ; $7B34: $13 + ld a, l ; $7B35: $7D + and $07 ; $7B36: $E6 $07 + jr nz, loop_7B32 ; $7B38: $20 $F8 + + ld a, $02 ; $7B3A: $3E $02 + ld [$DDD1], a ; $7B3C: $EA $D1 $DD + + ret + +; probalby wants +; ld a, $02 ; $7B40: $3E $02 + ;ldh [hLinkInteractiveMotionBlocked], a + +RandomTeleportPositions: + db $55, $54, $54, $54, $55, $55, $55, $54, $65, $55, $54, $65, $56, $56, $55, $55 + db $55, $45, $65, $54, $55, $55, $55, $55, $55, $55, $55, $58, $43, $57, $55, $55 + db $55, $55, $55, $55, $55, $54, $55, $53, $54, $56, $65, $65, $56, $55, $57, $65 + db $45, $55, $55, $55, $55, $55, $55, $55, $48, $45, $43, $34, $35, $35, $36, $34 + db $65, $55, $55, $54, $54, $54, $55, $54, $56, $65, $55, $55, $55, $55, $54, $54 + db $55, $55, $55, $55, $56, $55, $55, $54, $55, $55, $55, $53, $45, $35, $53, $46 + db $56, $55, $55, $55, $53, $55, $54, $54, $55, $55, $55, $54, $44, $55, $55, $54 + db $55, $55, $45, $55, $55, $54, $45, $45, $63, $55, $65, $55, $45, $45, $44, $54 + db $56, $56, $54, $55, $54, $55, $55, $55, $55, $55, $55, $56, $54, $55, $65, $56 + db $54, $54, $55, $65, $56, $54, $55, $56, $55, $55, $55, $66, $65, $65, $55, $56 + db $65, $55, $55, $75, $55, $55, $55, $54, $55, $55, $65, $57, $55, $54, $53, $45 + db $55, $56, $55, $55, $55, $45, $54, $55, $54, $55, $56, $55, $55, $55, $55, $54 + db $55, $55, $65, $55, $55, $54, $53, $58, $55, $05, $58, $55, $55, $55, $74, $55 + db $55, $55, $55, $55, $46, $55, $55, $56, $55, $55, $55, $54, $55, $45, $55, $55 + db $55, $55, $54, $55, $55, $55, $65, $55, $55, $46, $55, $55, $56, $55, $55, $55 + db $55, $55, $54, $55, $55, $55, $45, $36, $53, $51, $57, $53, $56, $54, $45, $46 diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/owl.asm b/worlds/ladx/LADXR/patches/bank3e.asm/owl.asm new file mode 100644 index 000000000000..35bc53f59e18 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.asm/owl.asm @@ -0,0 +1,63 @@ +HandleOwlStatue: + call GetRoomStatusAddressInHL + ld a, [hl] + and $20 + ret nz + ld a, [hl] + or $20 + ld [hl], a + + ld hl, $7B16 + call OffsetPointerByRoomNumber + ld a, [hl] + ldh [$F1], a + call ItemMessage + call GiveItemFromChest + ret + + + +GetRoomStatusAddressInHL: + ld a, [$DBA5] ; isIndoor + ld d, a + ld hl, $D800 + ldh a, [$F6] ; room nr + ld e, a + ldh a, [$F7] ; map nr + cp $FF + jr nz, .notColorDungeon + + ld d, $00 + ld hl, $DDE0 + jr .notIndoorB + +.notColorDungeon: + cp $1A + jr nc, .notIndoorB + + cp $06 + jr c, .notIndoorB + + inc d + +.notIndoorB: + add hl, de + ret + + +RenderOwlStatueItem: + ldh a, [$F6] ; map room + cp $B2 + jr nz, .NotYipYip + ; Add 2 to room to set room pointer to an empty room for trade items + add a, 2 + ldh [$F6], a + call RenderItemForRoom + ldh a, [$F6] ; map room + ; ...and undo it + sub a, 2 + ldh [$F6], a + ret +.NotYipYip: + call RenderItemForRoom + ret diff --git a/worlds/ladx/LADXR/patches/bank3e.py b/worlds/ladx/LADXR/patches/bank3e.py new file mode 100644 index 000000000000..d2b31adf9187 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.py @@ -0,0 +1,225 @@ +import os +import binascii +from ..assembler import ASM +from ..utils import formatText + + +def hasBank3E(rom): + return rom.banks[0x3E][0] != 0x00 + +def generate_name(l, i): + if i < len(l): + name = l[i] + else: + name = f"player {i}" + name = name[:16] + assert(len(name) <= 16) + return 'db "' + name + '"' + ', $ff' * (17 - len(name)) + '\n' + + +# Bank $3E is used for large chunks of custom code. +# Mainly for new chest and dropped items handling. +def addBank3E(rom, seed, player_id, player_name_list): + # No default text for getting the bow, so use an unused slot. + rom.texts[0x89] = formatText("Found the {BOW}!") + rom.texts[0xD9] = formatText("Found the {BOOMERANG}!") # owl text slot reuse + rom.texts[0xBE] = rom.texts[0x111] # owl text slot reuse to get the master skull message in the first dialog group + rom.texts[0xC8] = formatText("Found {BOWWOW}! Which monster put him in a chest? He is a good boi, and waits for you at the Swamp.") + rom.texts[0xC9] = 0xC0A0 # Custom message slot + rom.texts[0xCA] = formatText("Found {ARROWS_10}!") + rom.texts[0xCB] = formatText("Found a {SINGLE_ARROW}... joy?") + + # Create a trampoline to bank 0x3E in bank 0x00. + # There is very little room in bank 0, so we set this up as a single trampoline for multiple possible usages. + # the A register is preserved and can directly be used as a jumptable in page 3E. + # Trampoline at rst 8 + # the A register is preserved and can directly be used as a jumptable in page 3E. + rom.patch(0, 0x0008, "0000000000000000000000000000", ASM(""" + ld h, a + ld a, [$DBAF] + push af + ld a, $3E + call $080C ; switch bank + ld a, h + jp $4000 + """), fill_nop=True) + + # Special trampoline to jump to the damage-entity code, we use this from bowwow to damage instead of eat. + rom.patch(0x00, 0x0018, "000000000000000000000000000000", ASM(""" + ld a, $03 + ld [$2100], a + call $71C0 + ld a, [$DBAF] + ld [$2100], a + ret + """)) + + my_path = os.path.dirname(__file__) + rom.patch(0x3E, 0x0000, 0x2F00, ASM(""" + call MainJumpTable + pop af + jp $080C ; switch bank and return to normal code. + +MainJumpTable: + rst 0 ; JUMP TABLE + dw MainLoop ; 0 + dw RenderChestItem ; 1 + dw GiveItemFromChest ; 2 + dw ItemMessage ; 3 + dw RenderDroppedKey ; 4 + dw RenderHeartPiece ; 5 + dw GiveItemFromChestMultiworld ; 6 + dw CheckIfLoadBowWow ; 7 + dw BowwowEat ; 8 + dw HandleOwlStatue ; 9 + dw ItemMessageMultiworld ; A + dw GiveItemAndMessageForRoom ; B + dw RenderItemForRoom ; C + dw StartGameMarinMessage ; D + dw GiveItemAndMessageForRoomMultiworld ; E + dw RenderOwlStatueItem ; F + dw UpdateInventoryMenu ; 10 + dw LocalOnlyItemAndMessage ; 11 +StartGameMarinMessage: + ; Injection to reset our frame counter + call $27D0 ; Enable SRAM + ld hl, $B000 + xor a + ldi [hl], a ;subsecond counter + ld a, $08 ;(We set the counter to 8 seconds, as it takes 8 seconds before link wakes up and marin talks to him) + ldi [hl], a ;second counter + xor a + ldi [hl], a ;minute counter + ldi [hl], a ;hour counter + + ld hl, $B010 + ldi [hl], a ;check counter low + ldi [hl], a ;check counter high + + ; Show the normal message + ld a, $01 + jp $2385 + +TradeSequenceItemData: + ; tile attributes + db $0D, $0A, $0D, $0D, $0E, $0E, $0D, $0D, $0D, $0E, $09, $0A, $0A, $0D + ; tile index + db $1A, $B0, $B4, $B8, $BC, $C0, $C4, $C8, $CC, $D0, $D4, $D8, $DC, $E0 + +UpdateInventoryMenu: + ld a, [wTradeSequenceItem] + ld hl, wTradeSequenceItem2 + or [hl] + ret z + + ld hl, TradeSequenceItemData + ld a, [$C109] + ld e, a + ld d, $00 + add hl, de + + ; Check if we need to increase the counter + ldh a, [$E7] ; frame counter + and $0F + jr nz, .noInc + ld a, e + inc a + cp 14 + jr nz, .noWrap + xor a +.noWrap: + ld [$C109], a +.noInc: + + ; Check if we have the item + ld b, e + inc b + ld a, $01 + + ld de, wTradeSequenceItem +.shiftLoop: + dec b + jr z, .shiftLoopDone + sla a + jr nz, .shiftLoop + ; switching to second byte + ld de, wTradeSequenceItem2 + ld a, $01 + jr .shiftLoop +.shiftLoopDone: + ld b, a + ld a, [de] + and b + ret z ; skip this item + + ld b, [hl] + push hl + + ; Write the tile attribute data + ld a, $01 + ldh [$4F], a + + ld hl, $9C6E + call WriteToVRAM + inc hl + call WriteToVRAM + ld de, $001F + add hl, de + call WriteToVRAM + inc hl + call WriteToVRAM + + ; Write the tile data + xor a + ldh [$4F], a + + pop hl + ld de, 14 + add hl, de + ld b, [hl] + + ld hl, $9C6E + call WriteToVRAM + inc b + inc b + inc hl + call WriteToVRAM + ld de, $001F + add hl, de + dec b + call WriteToVRAM + inc hl + inc b + inc b + call WriteToVRAM + ret + +WriteToVRAM: + ldh a, [$41] + and $02 + jr nz, WriteToVRAM + ld [hl], b + ret +LocalOnlyItemAndMessage: + call GiveItemFromChest + call ItemMessage + ret + """ + open(os.path.join(my_path, "bank3e.asm/multiworld.asm"), "rt").read() + + open(os.path.join(my_path, "bank3e.asm/link.asm"), "rt").read() + + open(os.path.join(my_path, "bank3e.asm/chest.asm"), "rt").read() + + open(os.path.join(my_path, "bank3e.asm/bowwow.asm"), "rt").read() + + open(os.path.join(my_path, "bank3e.asm/message.asm"), "rt").read() + + open(os.path.join(my_path, "bank3e.asm/itemnames.asm"), "rt").read() + + "".join(generate_name(["The Server"] + player_name_list, i ) for i in range(100)) # allocate + + 'db "another world", $ff\n' + + open(os.path.join(my_path, "bank3e.asm/owl.asm"), "rt").read(), 0x4000), fill_nop=True) + # 3E:3300-3616: Multiworld flags per room (for both chests and dropped keys) + # 3E:3800-3B16: DroppedKey item types + # 3E:3B16-3E2C: Owl statue or trade quest items + + # Put 20 rupees in all owls by default. + rom.patch(0x3E, 0x3B16, "00" * 0x316, "1C" * 0x316) + + + # Prevent the photo album from crashing due to serial interrupts + rom.patch(0x28, 0x00D2, ASM("ld a, $09"), ASM("ld a, $01")) diff --git a/worlds/ladx/LADXR/patches/bank3f.py b/worlds/ladx/LADXR/patches/bank3f.py new file mode 100644 index 000000000000..8c6b86a7f355 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3f.py @@ -0,0 +1,386 @@ +from ..assembler import ASM +from .. import utils + + +def addBank3F(rom): + # Bank3F is used to initialize the tile data in VRAM:1 at the start of the rom. + # The normal rom does not use this tile data to maintain GB compatibility. + rom.patch(0, 0x0150, ASM(""" + cp $11 ; is running on Game Boy Color? + jr nz, notGBC + ldh a, [$4d] + and $80 ; do we need to switch the CPU speed? + jr nz, speedSwitchDone + ; switch to GBC speed + ld a, $30 + ldh [$00], a + ld a, $01 + ldh [$4d], a + xor a + ldh [$ff], a + stop + db $00 + + speedSwitchDone: + xor a + ldh [$70], a + ld a, $01 ; isGBC = true + jr Init + + notGBC: + xor a ; isGBC = false + Init: + """), ASM(""" + ; Check if we are a color gameboy, we require a color version now. + cp $11 + jr nz, notGBC + + ; Switch to bank $3F to run our custom initializer + ld a, $3F + ld [$2100], a + call $4000 + ; Switch back to bank 0 after loading our own initializer + ld a, $01 + ld [$2100], a + + ; set a to 1 to indicate GBC + ld a, $01 + jr Init + notGBC: + xor a + Init: + """), fill_nop=True) + + rom.patch(0x3F, 0x0000, None, ASM(""" + ; switch speed + ld a, $30 + ldh [$00], a + ld a, $01 + ldh [$4d], a + xor a + ldh [$ff], a + stop + db $00 + + ; Switch VRAM bank + ld a, $01 + ldh [$4F], a + + call $28CF ; display off + + ; Use the GBC DMA to transfer our tile data + ld a, $68 + ldh [$51], a + ld a, $00 + ldh [$52], a + + ld a, $80 + ldh [$53], a + ld a, $00 + ldh [$54], a + + ld a, $7F + ldh [$55], a + + waitTillTransferDone: + ldh a, [$55] + and $80 + jr z, waitTillTransferDone + + ld a, $70 + ldh [$51], a + ld a, $00 + ldh [$52], a + + ld a, $88 + ldh [$53], a + ld a, $00 + ldh [$54], a + + ld a, $7F + ldh [$55], a + + waitTillTransferDone2: + ldh a, [$55] + and $80 + jr z, waitTillTransferDone2 + + ld a, $68 + ldh [$51], a + ld a, $00 + ldh [$52], a + + ld a, $90 + ldh [$53], a + ld a, $00 + ldh [$54], a + + ld a, $7F + ldh [$55], a + + waitTillTransferDone3: + ldh a, [$55] + and $80 + jr z, waitTillTransferDone3 + + ; Switch VRAM bank back + ld a, $00 + ldh [$4F], a + + ; Switch the display back on, else the later code hangs + ld a, $80 + ldh [$40], a + + speedSwitchDone: + xor a + ldh [$70], a + + ; Check if we are running on a bad emulator + ldh [$02], a + ldh a, [$02] + and $7c + cp $7c + jr nz, badEmu + + ; Enable the timer to run 32 times per second + xor a + ldh [$06], a + ld a, $04 + ldh [$07], a + + ; Set SB to $FF to indicate we have no data from hardware + ld a, $FF + ldh [$01], a + ret +badEmu: + xor a + ldh [$40], a ; switch display off + ; Load some palette + ld a, $80 + ldh [$68], a + xor a + ldh [$69], a + ldh [$69], a + ldh [$69], a + ldh [$69], a + + ; Load a different gfx tile for the first gfx + cpl + ld hl, $8000 + ld c, $10 +.loop: + ldi [hl], a + dec c + jr nz, .loop + + ld a, $01 + ld [$9800], a + ld [$9820], a + ld [$9840], a + ld [$9860], a + ld [$9880], a + + ld [$9801], a + ld [$9841], a + ld [$9881], a + + ld [$9822], a + ld [$9862], a + + ld [$9824], a + ld [$9844], a + ld [$9864], a + ld [$9884], a + + ld [$9805], a + ld [$9845], a + + ld [$9826], a + ld [$9846], a + ld [$9866], a + ld [$9886], a + + ld [$9808], a + ld [$9828], a + ld [$9848], a + ld [$9868], a + ld [$9888], a + + ld [$9809], a + ld [$9889], a + + ld [$982A], a + ld [$984A], a + ld [$986A], a + + ld [$9900], a + ld [$9920], a + ld [$9940], a + ld [$9960], a + ld [$9980], a + + ld [$9901], a + ld [$9941], a + ld [$9981], a + + ld [$9903], a + ld [$9923], a + ld [$9943], a + ld [$9963], a + ld [$9983], a + + ld [$9904], a + ld [$9925], a + ld [$9906], a + + ld [$9907], a + ld [$9927], a + ld [$9947], a + ld [$9967], a + ld [$9987], a + + ld [$9909], a + ld [$9929], a + ld [$9949], a + ld [$9969], a + ld [$9989], a + + ld [$998A], a + + ld [$990B], a + ld [$992B], a + ld [$994B], a + ld [$996B], a + ld [$998B], a + + ; lcd on + ld a, $91 + ldh [$40], a +blockBadEmu: + di + jr blockBadEmu + + """)) + + # Copy all normal item graphics + rom.banks[0x3F][0x2800:0x2B00] = rom.banks[0x2C][0x0800:0x0B00] # main items + rom.banks[0x3F][0x2B00:0x2C00] = rom.banks[0x2C][0x0C00:0x0D00] # overworld key items + rom.banks[0x3F][0x2C00:0x2D00] = rom.banks[0x32][0x3D00:0x3E00] # dungeon key items + # Create ruppee for palettes 0-3 + rom.banks[0x3F][0x2B80:0x2BA0] = rom.banks[0x3F][0x2A60:0x2A80] + for n in range(0x2B80, 0x2BA0, 2): + rom.banks[0x3F][n+1] ^= rom.banks[0x3F][n] + + # Create capacity upgrade arrows + rom.banks[0x3F][0x2A30:0x2A40] = utils.createTileData(""" + 33 + 3113 + 311113 +33311333 + 3113 + 3333 +""") + rom.banks[0x3F][0x2A20:0x2A30] = rom.banks[0x3F][0x2A30:0x2A40] + for n in range(0x2A20, 0x2A40, 2): + rom.banks[0x3F][n] |= rom.banks[0x3F][n + 1] + + # Add the slime key and mushroom which are not in the above sets + rom.banks[0x3F][0x2CC0:0x2D00] = rom.banks[0x2C][0x28C0:0x2900] + # Add tunic sprites as well. + rom.banks[0x3F][0x2C80:0x2CA0] = rom.banks[0x35][0x0F00:0x0F20] + + # Add the bowwow sprites + rom.banks[0x3F][0x2D00:0x2E00] = rom.banks[0x2E][0x2400:0x2500] + + # Zol sprites, so we can have zol anywhere from a chest + rom.banks[0x3F][0x2E00:0x2E60] = rom.banks[0x2E][0x1120:0x1180] + # Patch gel(zol) entity to load sprites from the 2nd bank + rom.patch(0x06, 0x3C09, "5202522254025422" "5200522054005420", "600A602A620A622A" "6008602862086228") + rom.patch(0x07, 0x329B, "FFFFFFFF" "FFFFFFFF" "54005420" "52005220" "56005600", + "FFFFFFFF" "FFFFFFFF" "62086228" "60086028" "64086408") + rom.patch(0x06, 0x3BFA, "56025622", "640A642A"); + + + # Cucco + rom.banks[0x3F][0x2E80:0x2F00] = rom.banks[0x32][0x2500:0x2580] + # Patch the cucco graphics to load from 2nd vram bank + rom.patch(0x05, 0x0514, + "5001" "5201" "5401" "5601" "5221" "5021" "5621" "5421", + "6809" "6A09" "6C09" "6E09" "6A29" "6829" "6E29" "6C29") + # Song symbols + rom.banks[0x3F][0x2F00:0x2F60] = utils.createTileData(""" + + + ... + . .222 + .2.2222 +.22.222. +.22222.3 +.2..22.3 + .33...3 + .33.3.3 + ..233.3 +.22.2333 +.222.233 + .222... + ... +""" + """ + + + .. + .22 + .223 + ..222 + .33.22 + .3..22 + .33.33 + ..23. + ..233. + .22.333 +.22..233 + .. .23 + .. +""" + """ + + + ... + .222. + .2.332 + .23.32 + .233.2 + .222222 +.2222222 +.2..22.2 +.2.3.222 +.22...22 + .2333.. + .23333 + .....""", " .23") + + # Ghost + rom.banks[0x3F][0x2F60:0x2FE0] = rom.banks[0x32][0x1800:0x1880] + + # Instruments + rom.banks[0x3F][0x3000:0x3200] = rom.banks[0x31][0x1000:0x1200] + # Patch the egg song event to use the 2nd vram sprites + rom.patch(0x19, 0x0BAC, + "5006520654065606" + "58065A065C065E06" + "6006620664066606" + "68066A066C066E06", + "800E820E840E860E" + "880E8A0E8C0E8E0E" + "900E920E940E960E" + "980E9A0E9C0E9E0E" + ) + + # Rooster + rom.banks[0x3F][0x3200:0x3300] = rom.banks[0x32][0x1D00:0x1E00] + rom.patch(0x19, 0x19BC, + "42234023" "46234423" "40034203" "44034603" "4C034C23" "4E034E23" "48034823" "4A034A23", + "A22BA02B" "A62BA42B" "A00BA20B" "A40BA60B" "AC0BAC2B" "AE0BAE2B" "A80BA82B" "AA0BAA2B") + # Replace some main item graphics with the rooster + rom.banks[0x2C][0x0900:0x0940] = utils.createTileData(utils.tileDataToString(rom.banks[0x32][0x1D00:0x1D40]), " 321") + + # Trade sequence items + rom.banks[0x3F][0x3300:0x3640] = rom.banks[0x2C][0x0400:0x0740] diff --git a/worlds/ladx/LADXR/patches/bingo.py b/worlds/ladx/LADXR/patches/bingo.py new file mode 100644 index 000000000000..05a48c698047 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bingo.py @@ -0,0 +1,1036 @@ +from ..backgroundEditor import BackgroundEditor +from ..roomEditor import RoomEditor, ObjectWarp +from ..assembler import ASM +from ..locations.constants import * +from ..utils import formatText + +# Few unused rooms that we can use the room status variables for to store data. +UNUSED_ROOMS = [0x15D, 0x17E, 0x17F, 0x1AD] +next_bit_flag_index = 0 + + +def getUnusedBitFlag(): + global next_bit_flag_index + addr = UNUSED_ROOMS[next_bit_flag_index // 8] + 0xD800 + bit_nr = next_bit_flag_index & 0x07 + mask = 1 << bit_nr + next_bit_flag_index += 1 + check_code = checkMemoryMask("$%04x" % (addr), "$%02x" % (mask)) + set_code = "ld hl, $%04x\nset %d, [hl]" % (addr, bit_nr) + return check_code, set_code + + +class Goal: + def __init__(self, description, code, tile_info, *, kill_code=None, group=None, extra_patches=None): + self.description = description + self.code = code + self.tile_info = tile_info + self.kill_code = kill_code + self.group = group + self.extra_patches = extra_patches or [] + + +class TileInfo: + def __init__(self, index1, index2=None, index3=None, index4=None, *, shift4=False, colormap=None, flipH=False): + self.index1 = index1 + self.index2 = index2 if index2 is not None else self.index1 + 1 + self.index3 = index3 + self.index4 = index4 + if self.index3 is None: + self.index3 = self.index1 if flipH else self.index1 + 2 + if self.index4 is None: + self.index4 = self.index2 if flipH else self.index1 + 3 + self.shift4 = shift4 + self.colormap = colormap + self.flipH = flipH + + def getTile(self, rom, idx): + return rom.banks[0x2C + idx // 0x400][(idx % 0x400) * 0x10:((idx % 0x400) + 1) * 0x10] + + def get(self, rom): + data = self.getTile(rom, self.index1) + self.getTile(rom, self.index2) + self.getTile(rom, + self.index3) + self.getTile( + rom, self.index4) + if self.shift4: + a = [] + b = [] + for c in data[0:32]: + a.append(c >> 4) + b.append((c << 4) & 0xFF) + data = bytes(a + b) + if self.flipH: + a = [] + for c in data[32:64]: + d = 0 + for bit in range(8): + if c & (1 << bit): + d |= 0x80 >> bit + a.append(d) + data = data[0:32] + bytes(a) + if self.colormap: + d = [] + for n in range(0, 64, 2): + a = data[n] + b = data[n + 1] + for bit in range(8): + col = 0 + if a & (1 << bit): + col |= 1 + if b & (1 << bit): + col |= 2 + col = self.colormap[col] + a &= ~(1 << bit) + b &= ~(1 << bit) + if col & 1: + a |= 1 << bit + if col & 2: + b |= 1 << bit + d.append(a) + d.append(b) + data = bytes(d) + return data + + +ITEM_TILES = { + BOMB: TileInfo(0x80, shift4=True), + POWER_BRACELET: TileInfo(0x82, shift4=True), + SWORD: TileInfo(0x84, shift4=True), + SHIELD: TileInfo(0x86, shift4=True), + BOW: TileInfo(0x88, shift4=True), + HOOKSHOT: TileInfo(0x8A, shift4=True), + MAGIC_ROD: TileInfo(0x8C, shift4=True), + MAGIC_POWDER: TileInfo(0x8E, shift4=True), + OCARINA: TileInfo(0x4E90, shift4=True), + FEATHER: TileInfo(0x4E92, shift4=True), + FLIPPERS: TileInfo(0x94, shift4=True), + SHOVEL: TileInfo(0x96, shift4=True), + PEGASUS_BOOTS: TileInfo(0x98, shift4=True), + SEASHELL: TileInfo(0x9E, shift4=True), + MEDICINE: TileInfo(0xA0, shift4=True), + BOOMERANG: TileInfo(0xA4, shift4=True), + TOADSTOOL: TileInfo(0x28C, shift4=True), + GOLD_LEAF: TileInfo(0xCA, shift4=True), +} + + +def checkMemoryEqualCode(location, value): + return """ + ld a, [%s] + cp %s + ret + """ % (location, value) + + +def checkMemoryNotZero(*locations): + if len(locations) == 1: + return """ + ld a, [%s] + and a + jp flipZ + """ % (locations[0]) + code = "" + for location in locations: + code += """ + ld a, [%s] + and a + jp z, flipZ + """ % (location) + code += "jp flipZ" + return code + + +def checkMemoryMask(location, mask): + if isinstance(location, tuple): + code = "" + for loc in location: + code += """ + ld a, [%s] + and %s + jp z, clearZ + """ % (loc, mask) + code += "jp setZ" + return code + return """ + ld a, [%s] + and %s + jp flipZ + """ % (location, mask) + + +def checkForSeashellsCode(count): + return """ + ld a, [wSeashellsCount] + cp $%02x + jp nc, setZ + ld a, [$DAE9] + and $10 + jp flipZ + """ % (count) + + +def checkMemoryEqualGreater(location, count): + return """ + ld a, [%s] + cp %s + jp nc, setZ + jp clearZ + """ % (location, count) + + +def InventoryGoal(item, *, memory_location=None, msg=None, group=None): + if memory_location is not None: + code = checkMemoryNotZero(memory_location) + elif item in INVENTORY_MAP: + code = """ + ld hl, $DB00 + ld e, INV_SIZE + + .checkLoop: + ldi a, [hl] + cp $%s + ret z ; item found, return with zero flag set to indicate goal done. + dec e + jr nz, .checkLoop + rra ; clear z flag + ret + """ % (INVENTORY_MAP[item]) + else: + code = """ + rra ; clear z flag + ret + """ + if msg is None: + msg = "Find the {%s}" % (item) + return Goal(msg, code, ITEM_TILES[item], group=group) + + +def KillGoal(description, entity_id, tile_info): + check_code, set_code = getUnusedBitFlag() + return Goal(description, check_code, tile_info, kill_code=""" + cp $%02x + jr nz, skip_%02x + %s + jp done + skip_%02x: + """ % (entity_id, entity_id, set_code, entity_id)) + + +def MonkeyGoal(description, tile_info): + check_code, set_code = getUnusedBitFlag() + return Goal(description, check_code, tile_info, extra_patches=[ + (0x15, 0x36EC, 0x36EF, ASM("jp $7FCE")), + (0x15, 0x3FCE, "00" * 8, ASM(""" + ld [hl], $FA + %s + ret + """ % (set_code))) + ]) + + +def BuzzBlobTalkGoal(description, tile_info): + check_code, set_code = getUnusedBitFlag() + return Goal(description, check_code, tile_info, extra_patches=[ + (0x18, 0x37C9, ASM("call $237C"), ASM("call $7FDE")), + (0x18, 0x3FDE, "00" * 11, ASM(""" + call $237C + ld [hl], $FA + %s + ret + """ % (set_code))) + ]) + + +def KillDethlGoal(description, tile_info): + check_code, set_code = getUnusedBitFlag() + return Goal(description, check_code, tile_info, extra_patches=[ + (0x15, 0x0606, 0x060B, ASM(set_code)), + ]) + + +def FishDaPondGoal(description, tile_info): + check_code, set_code = getUnusedBitFlag() + return Goal(description, check_code, tile_info, extra_patches=[ + (0x04, 0x21F7, 0x21FC, ASM(set_code)), + ]) + + +BINGO_GOALS = [ + InventoryGoal(BOOMERANG), + InventoryGoal(HOOKSHOT), + InventoryGoal(MAGIC_ROD), + InventoryGoal(PEGASUS_BOOTS), + InventoryGoal(FEATHER), + InventoryGoal(POWER_BRACELET), + Goal("Find the L2 {POWER_BRACELET}", checkMemoryEqualCode("$DB43", "2"), TileInfo(0x82, 0x83, 0x06, 0xB2)), + InventoryGoal(FLIPPERS, memory_location="wHasFlippers"), + InventoryGoal(OCARINA), + InventoryGoal(MEDICINE, memory_location="wHasMedicine", msg="Have the {MEDICINE}"), + InventoryGoal(BOW), + InventoryGoal(SHOVEL), + # InventoryGoal(MAGIC_POWDER), + InventoryGoal(TOADSTOOL, msg="Have the {TOADSTOOL}", group="witch"), + Goal("Find the L2 {SHIELD}", checkMemoryEqualCode("$DB44", "2"), TileInfo(0x86, 0x87, 0x06, 0xB2)), + Goal("Find 10 Secret Seashells", checkForSeashellsCode(10), ITEM_TILES[SEASHELL]), + Goal("Find the L2 {SWORD}", checkMemoryEqualCode("$DB4E", "2"), TileInfo(0x84, 0x85, 0x06, 0xB2)), + Goal("Find the {TAIL_KEY}", checkMemoryNotZero("$DB11"), TileInfo(0xC0, shift4=True)), + Goal("Find the {SLIME_KEY}", checkMemoryNotZero("$DB15"), TileInfo(0x28E, shift4=True)), + Goal("Find the {ANGLER_KEY}", checkMemoryNotZero("$DB12"), TileInfo(0xC2, shift4=True)), + Goal("Find the {FACE_KEY}", checkMemoryNotZero("$DB13"), TileInfo(0xC4, shift4=True)), + Goal("Find the {BIRD_KEY}", checkMemoryNotZero("$DB14"), TileInfo(0xC6, shift4=True)), + # {"description": "Marin's Cucco Killing Text"}, + # {"description": "Pick up Crane Game Owner"}, + BuzzBlobTalkGoal("Talk to a buzz blob", TileInfo(0x179C, colormap=[2, 3, 1, 0])), + # {"description": "Moblin King"}, + Goal("Turtle Rock Entrance Boss", checkMemoryMask("$D810", "$20"), + TileInfo(0x1413, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Kill Master Stalfos", checkMemoryMask("$D980", "$10"), TileInfo(0x1622, colormap=[2, 3, 1, 0])), + # {"description": "Gohma"}, + # {"description": "Grim Creeper"}, + # {"description": "Blaino"}, + KillDethlGoal("Kill Dethl", TileInfo(0x1B38, colormap=[2, 3, 1, 0])), + # {"description": "Rooster"}, + # {"description": "Marin"}, + # {"description": "Bow-wow"}, + # {"description": "Return Bow-wow"}, + # {"description": "8 Heart Pieces"}, + # {"description": "12 Heart Pieces"}, + Goal("{BOMB} upgrade", checkMemoryEqualCode("$DB77", "$60"), TileInfo(0x80, 0x81, 0x06, 0xA3)), + Goal("Arrow upgrade", checkMemoryEqualCode("$DB78", "$60"), TileInfo(0x88, 0x89, 0x06, 0xA3)), + Goal("{MAGIC_POWDER} upgrade", checkMemoryEqualCode("$DB76", "$40"), TileInfo(0x8E, 0x8F, 0x06, 0xA3)), + # {"description": "Steal From Shop 5 Times"}, + KillGoal("Kill the giant ghini", 0x11, TileInfo(0x08A6, colormap=[2, 3, 1, 0])), + Goal("Got the Ballad of the Wind Fish", checkMemoryMask("$DB49", "4"), + TileInfo(0x298, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Got the Manbo's Mambo", checkMemoryMask("$DB49", "2"), TileInfo(0x29A, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Got the Frog's Song of Soul", checkMemoryMask("$DB49", "1"), + TileInfo(0x29C, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Map and Compass in Tail Cave", checkMemoryNotZero("$DB16", "$DB17"), TileInfo(0x1BD0, index4=0xB1)), + Goal("Map and Compass in Bottle Grotto", checkMemoryNotZero("$DB1B", "$DB1C"), TileInfo(0x1BD0, index4=0xB2)), + Goal("Map and Compass in Key Cavern", checkMemoryNotZero("$DB20", "$DB21"), TileInfo(0x1BD0, index4=0xB3)), + Goal("Map and Compass in Angler's Tunnel", checkMemoryNotZero("$DB25", "$DB26"), TileInfo(0x1BD0, index4=0xB4)), + Goal("Map and Compass in Catfish's Maw", checkMemoryNotZero("$DB2A", "$DB2B"), TileInfo(0x1BD0, index4=0xB5)), + Goal("Map and Compass in Face Shrine", checkMemoryNotZero("$DB2F", "$DB30"), TileInfo(0x1BD0, index4=0xB6)), + Goal("Map and Compass in Eagle's Tower", checkMemoryNotZero("$DB34", "$DB35"), TileInfo(0x1BD0, index4=0xB7)), + Goal("Map and Compass in Turtle Rock", checkMemoryNotZero("$DB39", "$DB3A"), TileInfo(0x1BD0, index4=0xB8)), + Goal("Map and Compass in Color Dungeon", checkMemoryNotZero("$DDDA", "$DDDB"), TileInfo(0x1BD0, index4=0xB0)), + # {"description": "Talk to all Owl Statues in Tail Cave"}, + # {"description": "Talk to all Owl Statues in Bottle Grotto"}, + # {"description": "Talk to all Owl Statues in Key Cavern"}, + # {"description": "Talk to all Owl Statues in Angler's Tunnel"}, + # {"description": "Talk to all Owl Statues in Catfish's Maw"}, + # {"description": "Talk to all Owl Statues in Face Shrine"}, + # {"description": "Talk to all Owl Statues in Eagle's Tower"}, + # {"description": "Talk to all Owl Statues in Turtle Rock"}, + # {"description": "Talk to all Owl Statues in Color Dungeon"}, + # {"description": "Defeat 2 Sets of 3-Of-A-Kind (D1, D7)"}, + # {"description": "Stand 6 HorseHeads in D6"}, + Goal("Find the 5 Golden Leaves", checkMemoryEqualGreater("wGoldenLeaves", "5"), ITEM_TILES[GOLD_LEAF]), + # {"description": "Defeat Mad Bomber (outside Kanalet Castle)"}, + # {"description": "Totaka's Song in Richard's Villa"}, + Goal("Get the Yoshi Doll", checkMemoryMask("$DAA0", "$20"), TileInfo(0x9A)), + # {"description": "Save Papahl on the mountain"}, + Goal("Give the banana to Kiki", checkMemoryMask("$D87B", "$20"), TileInfo(0x1670, colormap=[2, 3, 1, 0])), + Goal("Have 99 or less rupees", checkMemoryEqualCode("$DB5D", "0"), TileInfo(0xA6, 0xA7, shift4=True), group="rupees"), + Goal("Have 900 or more rupees", checkMemoryEqualGreater("$DB5D", "9"), TileInfo(0xA6, 0xA7, 0xA6, 0xA7), group="rupees"), + MonkeyGoal("Bonk the Beach Monkey", TileInfo(0x1946, colormap=[2, 3, 1, 0])), + # {"description": "Kill an enemy after transforming"}, + + Goal("Got the Red Tunic", checkMemoryMask("wCollectedTunics", "1"), + TileInfo(0x2400, 0x0D11, 0x2400, 0x2401, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Got the Blue Tunic", checkMemoryMask("wCollectedTunics", "2"), + TileInfo(0x2400, 0x0D01, 0x2400, 0x2401, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Buy the first shop item", checkMemoryMask("$DAA1", "$10"), TileInfo(0x0880, colormap=[2, 3, 1, 0])), + Goal("Buy the second shop item", checkMemoryMask("$DAA1", "$20"), TileInfo(0x0880, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT1}", checkMemoryMask("$DB65", "2"), TileInfo(0x1500, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT2}", checkMemoryMask("$DB66", "2"), TileInfo(0x1504, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT3}", checkMemoryMask("$DB67", "2"), TileInfo(0x1508, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT4}", checkMemoryMask("$DB68", "2"), TileInfo(0x150C, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT5}", checkMemoryMask("$DB69", "2"), TileInfo(0x1510, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT6}", checkMemoryMask("$DB6A", "2"), TileInfo(0x1514, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT7}", checkMemoryMask("$DB6B", "2"), TileInfo(0x1518, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT8}", checkMemoryMask("$DB6C", "2"), TileInfo(0x151C, colormap=[2, 3, 1, 0])), + # {"description": "Moldorm", "group": "d1"}, + # {"description": "Genie in a Bottle", "group": "d2"}, + # {"description": "Slime Eyes", "group": "d3"}, + # {"description": "Angler Fish", "group": "d4"}, + # {"description": "Slime Eel", "group": "d5"}, + # {"description": "Facade", "group": "d6"}, + # {"description": "Evil Eagle", "group": "d7"}, + # {"description": "Hot Head", "group": "d8"}, + # {"description": "2 Followers at the same time", "group": "multifollower"}, + # {"description": "3 Followers at the same time", "group": "multifollower"}, + Goal("Visit the 4 Fountain Fairies", checkMemoryMask(("$D853", "$D9AC", "$D9F3", "$D9FB"), "$80"), + TileInfo(0x20, shift4=True, colormap=[2, 3, 1, 0])), + Goal("Have at least 8 Heart Containers", checkMemoryEqualGreater("$DB5B", "8"), TileInfo(0xAA, flipH=True), group="Health"), + Goal("Have at least 9 Heart Containers", checkMemoryEqualGreater("$DB5B", "9"), TileInfo(0xAA, flipH=True), group="Health"), + Goal("Have at least 10 Heart Containers", checkMemoryEqualGreater("$DB5B", "10"), TileInfo(0xAA, flipH=True), group="Health"), + Goal("Got photo 1: Here Stands A Brave Man", checkMemoryMask("$DC0C", "$01"), TileInfo(0x3008, 0x0D0F, colormap=[2, 3, 1, 0])), + Goal("Got photo 2: Looking over the sea with Marin", checkMemoryMask("$DC0C", "$02"), TileInfo(0x08F0, 0x0D0F, colormap=[2, 3, 1, 0])), + Goal("Got photo 3: Heads up!", checkMemoryMask("$DC0C", "$04"), TileInfo(0x0E6A, 0x0D0F, 0x0E6B, 0x0E7B)), + Goal("Got photo 4: Say Mushroom!", checkMemoryMask("$DC0C", "$08"), TileInfo(0x0E60, 0x0D0F, 0x0E61, 0x0E71)), + Goal("Got photo 5: Ulrira's Secret!", checkMemoryMask("$DC0C", "$10"), + TileInfo(0x1461, 0x1464, 0x1463, 0x0D0F, colormap=[2, 3, 1, 0])), + Goal("Got photo 6: Playing with Bowwow!", checkMemoryMask("$DC0C", "$20"), + TileInfo(0x1A42, 0x0D0F, 0x1A42, 0x1A43, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Got photo 7: Thief!", checkMemoryMask("$DC0C", "$40"), TileInfo(0x0880, 0x0D0F, colormap=[2, 3, 1, 0])), + Goal("Got photo 8: Be more careful next time!", checkMemoryMask("$DC0C", "$80"), TileInfo(0x14E0, index4=0x0D0F, colormap=[2, 3, 1, 0])), + Goal("Got photo 9: I Found Zora!", checkMemoryMask("$DC0D", "$01"), + TileInfo(0x1906, 0x0D0F, 0x1906, 0x1907, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Got photo 10: Richard at Kanalet Castle", checkMemoryMask("$DC0D", "$02"), TileInfo(0x15B0, 0x0D0F, colormap=[2, 3, 1, 0])), + Goal("Got photo 11: Ghost", checkMemoryMask("$DC0D", "$04"), TileInfo(0x1980, 0x0D0F, colormap=[2, 3, 1, 0])), + Goal("Got photo 12: Close Call", checkMemoryMask("$DC0D", "$08"), TileInfo(0x0FED, 0x0D0F, 0x0FED, 0x0FFD)), + # {"description": "Collect 4 Pictures", "group": "pics"}, + # {"description": "Collect 5 Pictures", "group": "pics"}, + # {"description": "Collect 6 Pictures", "group": "pics"}, + Goal("Open the 4 Overworld Warp Holes", checkMemoryMask(("$D801", "$D82C", "$D895", "$D8EC"), "$80"), + TileInfo(0x3E, 0x3E, 0x3E, 0x3E, colormap=[2, 1, 3, 0])), + Goal("Finish the Raft Minigame", checkMemoryMask("$D87F", "$80"), TileInfo(0x087C, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Kill the Ball and Chain Trooper", checkMemoryMask("$DAC6", "$10"), TileInfo(0x09A4, colormap=[2, 3, 1, 0])), + Goal("Destroy all Pillars with the Ball", checkMemoryMask(("$DA14", "$DA15", "$DA18", "$DA19"), "$20"), + TileInfo(0x166C, flipH=True)), + FishDaPondGoal("Fish the pond empty", TileInfo(0x0A00, colormap=[2, 3, 1, 0])), + KillGoal("Kill the Anti-Kirby", 0x91, TileInfo(0x1550, colormap=[2, 3, 1, 0])), + KillGoal("Kill a Rolling Bones", 0x81, TileInfo(0x0AB6, colormap=[2, 3, 1, 0])), + KillGoal("Kill a Hinox", 0x89, TileInfo(0x1542, colormap=[2, 3, 1, 0])), + KillGoal("Kill a Stone Hinox", 0xF4, TileInfo(0x2482, colormap=[2, 3, 1, 0])), + KillGoal("Kill a Dodongo", 0x60, TileInfo(0x0AA0, flipH=True, colormap=[2, 3, 1, 0])), + KillGoal("Kill a Cue Ball", 0x8E, TileInfo(0x1566, flipH=True, colormap=[2, 3, 1, 0])), + KillGoal("Kill a Smasher", 0x92, TileInfo(0x1576, colormap=[2, 3, 1, 0])), + + Goal("Save Marin on the Mountain Bridge", checkMemoryMask("$D808", "$10"), TileInfo(0x1A6C, colormap=[2, 3, 1, 0])), + Goal("Save Raccoon Tarin", checkMemoryMask("$D851", "$10"), TileInfo(0x1888, colormap=[2, 3, 1, 0])), + Goal("Trade the {TOADSTOOL} with the witch", checkMemoryMask("$DAA2", "$20"), TileInfo(0x0A30, colormap=[2, 3, 1, 0]), group="witch"), +] + + +def randomizeGoals(rnd, options): + goals = BINGO_GOALS.copy() + rnd.shuffle(goals) + has_group = set() + for n in range(len(goals)): + if goals[n].group: + if goals[n].group in has_group: + goals[n] = None + else: + has_group.add(goals[n].group) + goals = [goal for goal in goals if goal is not None] + return goals[:25] + + +def setBingoGoal(rom, goals, mode): + assert len(goals) == 25 + + for goal in goals: + for bank, addr, current, target in goal.extra_patches: + rom.patch(bank, addr, current, target) + + # Setup the bingo card visuals + be = BackgroundEditor(rom, 0x15) + ba = BackgroundEditor(rom, 0x15, attributes=True) + for y in range(18): + for x in range(20): + be.tiles[0x9800 + x + y * 0x20] = (x + (y & 2)) % 4 | ((y % 2) * 4) + ba.tiles[0x9800 + x + y * 0x20] = 0x01 + + for y in range(5): + for x in range(5): + idx = x + y * 5 + be.tiles[0x9843 + x * 3 + y * 3 * 0x20] = 8 + idx * 4 + be.tiles[0x9844 + x * 3 + y * 3 * 0x20] = 10 + idx * 4 + be.tiles[0x9863 + x * 3 + y * 3 * 0x20] = 9 + idx * 4 + be.tiles[0x9864 + x * 3 + y * 3 * 0x20] = 11 + idx * 4 + + ba.tiles[0x9843 + x * 3 + y * 3 * 0x20] = 0x03 + ba.tiles[0x9844 + x * 3 + y * 3 * 0x20] = 0x03 + ba.tiles[0x9863 + x * 3 + y * 3 * 0x20] = 0x03 + ba.tiles[0x9864 + x * 3 + y * 3 * 0x20] = 0x03 + be.store(rom) + ba.store(rom) + + tiles = rom.banks[0x30][0x3000:0x3040] + rom.banks[0x30][0x3100:0x3140] + for goal in goals: + tiles += goal.tile_info.get(rom) + rom.banks[0x30][0x3000:0x3000 + len(tiles)] = tiles + + # Patch the mural palette to have more useful entries for us + rom.patch(0x21, 0x36EE, ASM("dw $7C00, $7C00, $7C00, $7C00"), ASM("dw $7FFF, $56b5, $294a, $0000")) + rom.patch(0x21, 0x36F6, ASM("dw $7C00, $7C00, $7C00, $7C00"), ASM("dw $43f0, $32ac, $1946, $0000")) + + # Patch the face shrine mural handler stage 4, we want to jump to bank 0x0C, which normally contains + # DMG graphics, but gives us a lot of room for our own code and graphics. + rom.patch(0x01, 0x2B81, 0x2B99, ASM(""" + ld a, $0D + ld hl, $4000 + push hl + jp $080C ; switch bank + """), fill_nop=True) + # Fix that the mural is always centered on screen, instead offset it properly + rom.patch(0x18, 0x1E3D, ASM("add hl, bc\nld [hl], $50"), ASM("call $7FD6")) + rom.patch(0x18, 0x3FD6, "00" * 8, ASM(""" + add hl, bc + ld a, [hl] + and $F0 + add a, $0F + ld [hl], a + ret + """)) + + # In stage 5, just exit the mural without waiting for a button press. + rom.patch(0x01, 0x2B9E, ASM("jr z, $07"), "", fill_nop=True) + + # Our custom stage 4 + rom.patch(0x0D, 0x0000, 0x3000, ASM(""" +wState := $C3C4 ; Our internal state, guaranteed to be 0 on the first entry. +wCursorX := $D100 +wCursorY := $D101 + + call mainHandler + ; Make sure we return with bank 1 active. + ld a, $01 + jp $080C ; switch bank + +mainHandler: + ld a, [wState] + rst 0 + dw init + dw checkGoalDone + dw chooseSquare + dw waitDialogDone + dw finishGame + +init: + xor a + ld [wCursorX], a + ld [wCursorY], a + inc a + ld [wState], a + di + ldh [$4F], a ; 2nd vram bank + + ld hl, $9843 + ld c, 25 + ld b, $00 +.checkDoneLoop: + push bc + push hl + ld a, b + call goalCheck + pop hl + jr nz, .notDone +.statWait1: + ldh a, [$41] ;STAT + and $02 + jr nz, .statWait1 + ld a, $04 + ldi [hl], a + ldd [hl], a + + ld bc, $0020 + add hl, bc +.statWait2: + ldh a, [$41] ;STAT + and $02 + jr nz, .statWait2 + ld a, $04 + ldi [hl], a + ldd [hl], a + + ld bc, $FFE0 + add hl, bc +.notDone: + inc hl + inc hl + inc hl + ld a, l + and $1F + cp $12 + jr nz, .noRowSkip + ld bc, $0060 - 5*3 + add hl, bc +.noRowSkip: + pop bc + inc b + dec c + jr nz, .checkDoneLoop + + xor a + ldh [$4F], a ; 1st vram bank + ei + +checkGoalDone: + ld a, $02 + ld [wState], a + ; Check if the egg event is already triggered + ld a, [$D806] + and $10 + ret nz + + call checkAnyGoal + ret nz + + ; Goal done, give a message and goto state to finish the game + ld a, $04 + ld [wState], a + ld a, $E7 + call $2385 ; open dialog + ret + +chooseSquare: + ld hl, $C000 ; oam buffer + + ; Draw the cursor + ld a, [wCursorY] + call multiA24 + add a, $27 + ldi [hl], a + ld a, [wCursorX] + call multiA24 + add a, $24 + ldi [hl], a + ld a, $A2 + ldi [hl], a + ld a, $02 + ldi [hl], a + + ldh a, [$CC] ; button presses + bit 0, a + jr nz, .right + bit 1, a + jr nz, .left + bit 2, a + jr nz, .up + bit 3, a + jr nz, .down + bit 4, a + jr nz, .showText + bit 5, a + jr nz, exitMural + bit 7, a + jr nz, exitMural + ret + +.right: + ld a, [wCursorX] + cp $04 + ret z + inc a + ld [wCursorX], a + ret + +.left: + ld a, [wCursorX] + and a + ret z + dec a + ld [wCursorX], a + ret + +.down: + ld a, [wCursorY] + cp $04 + ret z + inc a + ld [wCursorY], a + ret + +.up: + ld a, [wCursorY] + and a + ret z + dec a + ld [wCursorY], a + ret + +.showText: + ld a, [wCursorY] + ld c, a + add a, a + add a, a + add a, c + ld c, a + ld a, [wCursorX] + add a, c + add a, a + ld l, a + ld h, $00 + ld de, messageTable + add hl, de + ldi a, [hl] + ld h, [hl] + ld l, a + ld de, wCustomMessage +.copyLoop: + ldi a, [hl] + ld [de], a + inc de + inc a + jr nz, .copyLoop + + ld a, $C9 + call $2385 ; open dialog + ld a, $03 + ld [wState], a + ret + +waitDialogDone: + ld a, [$C19F] ; dialog state + and a + ret nz + ld a, $02 ; choose square + ld [wState], a + ret + +finishGame: + ld a, [$C19F] ; dialog state + and a + ret nz + + ldh a, [$CC] ; button presses + and a + ret z + + ; Goto "credits" + xor a + ld [$DB96], a + inc a + ld [$DB95], a + ret + +exitMural: + ld hl, $DB96 ;gameplay subtype + inc [hl] + ret + +multiA24: + ld c, a + add a, a + add a, c + add a, a + add a, a + add a, a + ret + +flipZ: + jr nz, setZ +clearZ: + rra + ret +setZ: + cp a + ret + +checkAnyGoal: +#IF {mode} + call goalcheck_0 + ret nz + call goalcheck_1 + ret nz + call goalcheck_2 + ret nz + call goalcheck_3 + ret nz + call goalcheck_4 + ret nz + call goalcheck_5 + ret nz + call goalcheck_6 + ret nz + call goalcheck_7 + ret nz + call goalcheck_8 + ret nz + call goalcheck_9 + ret nz + call goalcheck_10 + ret nz + call goalcheck_11 + ret nz + call goalcheck_12 + ret nz + call goalcheck_13 + ret nz + call goalcheck_14 + ret nz + call goalcheck_15 + ret nz + call goalcheck_16 + ret nz + call goalcheck_17 + ret nz + call goalcheck_18 + ret nz + call goalcheck_19 + ret nz + call goalcheck_20 + ret nz + call goalcheck_21 + ret nz + call goalcheck_22 + ret nz + call goalcheck_23 + ret nz + call goalcheck_24 + ret +#ELSE + call checkGoalRow1 + ret z + call checkGoalRow2 + ret z + call checkGoalRow3 + ret z + call checkGoalRow4 + ret z + call checkGoalRow5 + ret z + call checkGoalCol1 + ret z + call checkGoalCol2 + ret z + call checkGoalCol3 + ret z + call checkGoalCol4 + ret z + call checkGoalCol5 + ret z + call checkGoalDiagonal0 + ret z + call checkGoalDiagonal1 + ret + +checkGoalRow1: + call goalcheck_0 + ret nz + call goalcheck_1 + ret nz + call goalcheck_2 + ret nz + call goalcheck_3 + ret nz + call goalcheck_4 + ret + +checkGoalRow2: + call goalcheck_5 + ret nz + call goalcheck_6 + ret nz + call goalcheck_7 + ret nz + call goalcheck_8 + ret nz + call goalcheck_9 + ret + +checkGoalRow3: + call goalcheck_10 + ret nz + call goalcheck_11 + ret nz + call goalcheck_12 + ret nz + call goalcheck_13 + ret nz + call goalcheck_14 + ret + +checkGoalRow4: + call goalcheck_15 + ret nz + call goalcheck_16 + ret nz + call goalcheck_17 + ret nz + call goalcheck_18 + ret nz + call goalcheck_19 + ret + +checkGoalRow5: + call goalcheck_20 + ret nz + call goalcheck_21 + ret nz + call goalcheck_22 + ret nz + call goalcheck_23 + ret nz + call goalcheck_24 + ret + +checkGoalCol1: + call goalcheck_0 + ret nz + call goalcheck_5 + ret nz + call goalcheck_10 + ret nz + call goalcheck_15 + ret nz + call goalcheck_20 + ret + +checkGoalCol2: + call goalcheck_1 + ret nz + call goalcheck_6 + ret nz + call goalcheck_11 + ret nz + call goalcheck_16 + ret nz + call goalcheck_21 + ret + +checkGoalCol3: + call goalcheck_2 + ret nz + call goalcheck_7 + ret nz + call goalcheck_12 + ret nz + call goalcheck_17 + ret nz + call goalcheck_22 + ret + +checkGoalCol4: + call goalcheck_3 + ret nz + call goalcheck_8 + ret nz + call goalcheck_13 + ret nz + call goalcheck_18 + ret nz + call goalcheck_23 + ret + +checkGoalCol5: + call goalcheck_4 + ret nz + call goalcheck_9 + ret nz + call goalcheck_14 + ret nz + call goalcheck_19 + ret nz + call goalcheck_24 + ret + +checkGoalDiagonal0: + call goalcheck_0 + ret nz + call goalcheck_6 + ret nz + call goalcheck_12 + ret nz + call goalcheck_18 + ret nz + call goalcheck_24 + ret + +checkGoalDiagonal1: + call goalcheck_4 + ret nz + call goalcheck_8 + ret nz + call goalcheck_12 + ret nz + call goalcheck_16 + ret nz + call goalcheck_20 + ret +#ENDIF + +messageTable: +""".format(mode=1 if mode == "bingo-full" else 0) + + "\n".join(["dw message_%d" % (n) for n in range(25)]) + "\n" + + "\n".join(["message_%d:\n db m\"%s\"" % (n, goal.description) for n, goal in + enumerate(goals)]) + "\n" + + """ + goalCheck: + rst 0 + """ + + "\n".join(["dw goalcheck_%d" % (n) for n in range(25)]) + "\n" + + "\n".join(["goalcheck_%d:\n %s\n" % (n, goal.code) for n, goal in + enumerate(goals)]) + "\n", 0x4000), fill_nop=True) + rom.texts[0xE7] = formatText("BINGO!\nPress any button to finish.") + + # Patch the game to call a bit of our code when an enemy is killed by patching into the drop item handling + rom.patch(0x00, 0x3F50, ASM("ld a, $03\nld [$C113], a\nld [$2100], a\ncall $55CF"), ASM(""" + ld a, $0D + ld [$C113], a + ld [$2100], a + call $7000 + """)) + rom.patch(0x0D, 0x3000, 0x4000, ASM(""" + ldh a, [$EB] ; active entity + """ + "\n".join([goal.kill_code for goal in goals if goal.kill_code is not None]) + """ +done: ; Return to normal item drop handler + ld a, $03 ;normal drop item handler bank + ld hl, $55CF ;normal drop item handler address + push hl + jp $080F ; switch bank + """, 0x7000), fill_nop=True) + + # Patch Dethl to warp you outside + rom.patch(0x15, 0x0682, 0x069B, ASM(""" + ld a, $0B + ld [$DB95], a + call $0C7D + + ld a, $07 + ld [$DB96], a + """), fill_nop=True) + re = RoomEditor(rom, 0x274) + re.objects += [ObjectWarp(0, 0, 0x06, 0x58, 0x40)] * 4 + re.store(rom) + # Patch the egg to be always open + rom.patch(0x00, 0x31f5, ASM("ld a, [$D806]\nand $10\njr z, $25"), ASM(""), fill_nop=True) + rom.patch(0x20, 0x2dea, ASM("ld a, [$D806]\nand $10\njr z, $29"), ASM(""), fill_nop=True) + + # Patch unused entity 4C into our bingo board. + rom.patch(0x03, 0x004C, "41", "82") + rom.patch(0x03, 0x0147, "00", "98") + rom.patch(0x20, 0x00e4, "000000", ASM("dw $5e1b\ndb $18")) + + # Add graphics for our bingo board to 2nd WRAM bank. + rom.banks[0x3F][0x3700:0x3780] = rom.banks[0x32][0x1500:0x1580] + rom.banks[0x3F][0x3728:0x373A] = b'\x55\xAA\x00\xFF\x55\xAA\x00\xFF\x55\xAA\x00\xFF\x55\xAA\x00\xFF\x00\xFF' + rom.banks[0x3F][0x3748:0x375A] = b'\x55\xAA\x00\xFF\x55\xAA\x00\xFF\x55\xAA\x00\xFF\x55\xAA\x00\xFF\x00\xFF' + rom.patch(0x18, 0x1E0B, + "00F85003" + "00005203" + "00085403" + "00105603", + "00F8F00B" + "0000F20B" + "0008F40B" + "0010F60B") + + # Add the bingo board to marins house + re = RoomEditor(rom, 0x2A3) + re.entities.append((2, 0, 0x4C)) + re.store(rom) + + # Add the bingo board to the room before the egg + re = RoomEditor(rom, 0x016) + re.removeObject(4, 5) + re.entities.append((3, 4, 0x4C)) + re.updateOverlay() + re.store(rom) + + # Remove the egg event from the egg room (no bomb triggers for you!) + re = RoomEditor(rom, 0x006) + re.entities = [] + re.store(rom) + + rom.texts[0xCF] = formatText(""" + Bingo! + Young lad, I mean... #####, the hero! + You have bingo! + You have proven your wisdom, courage and power! + ... ... ... ... + As part of the Wind Fish's spirit, I am the guardian of his dream world... + But one day, we decided to have a bingo game. + Then you, #####, came to win the bingo... + Thank you, #####... + My work is done... + The Wind Fish will wake soon. + Good bye...Bingo! + """) + rom.texts[0xCE] = rom.texts[0xCF] diff --git a/worlds/ladx/LADXR/patches/bomb.py b/worlds/ladx/LADXR/patches/bomb.py new file mode 100644 index 000000000000..4d9b2891d4c8 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bomb.py @@ -0,0 +1,20 @@ +from ..assembler import ASM + + +def onlyDropBombsWhenHaveBombs(rom): + rom.patch(0x03, 0x1FC5, ASM("call $608C"), ASM("call $50B2")) + # We use some of the unused chest code space here to remove the bomb if you do not have bombs in your inventory. + rom.patch(0x03, 0x10B2, 0x112A, ASM(""" + ld e, INV_SIZE + ld hl, $DB00 + ld a, $02 +loop: + cp [hl] + jr z, resume + dec e + inc hl + jr nz, loop + jp $3F8D ; unload entity +resume: + jp $608C + """), fill_nop=True) \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/bowwow.py b/worlds/ladx/LADXR/patches/bowwow.py new file mode 100644 index 000000000000..479f360514e3 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bowwow.py @@ -0,0 +1,207 @@ +from ..assembler import ASM +from ..roomEditor import RoomEditor + + +def fixBowwow(rom, everywhere=False): + ### BowWow patches + rom.patch(0x03, 0x1E0E, ASM("ld [$DB56], a"), "", fill_nop=True) # Do not mark BowWow as kidnapped after we complete dungeon 1. + rom.patch(0x15, 0x06B6, ASM("ld a, [$DB56]\ncp $80"), ASM("xor a"), fill_nop=True) # always load the moblin boss + rom.patch(0x03, 0x182D, ASM("ld a, [$DB56]\ncp $80"), ASM("ld a, [$DAE2]\nand $10")) # load the cave moblins if the chest is not opened + rom.patch(0x07, 0x3947, ASM("ld a, [$DB56]\ncp $80"), ASM("ld a, [$DAE2]\nand $10")) # load the cave moblin with sword if the chest is not opened + + # Modify the moblin cave to contain a chest at the end, which contains bowwow + re = RoomEditor(rom, 0x2E2) + re.removeEntities(0x6D) + re.changeObject(8, 3, 0xA0) + re.store(rom) + # Place bowwow in the chest table + rom.banks[0x14][0x560 + 0x2E2] = 0x81 + + # Patch bowwow follower sprite to be used from 2nd vram bank + rom.patch(0x05, 0x001C, + b"40034023" + b"42034223" + b"44034603" + b"48034A03" + b"46234423" + b"4A234823" + b"4C034C23", + b"500B502B" + b"520B522B" + b"540B560B" + b"580B5A0B" + b"562B542B" + b"5A2B582B" + b"5C0B5C2B") + # Patch to use the chain sprite from second vram bank (however, the chain bugs out various things) + rom.patch(0x05, 0x0282, + ASM("ld a, $4E\njr nz, $02\nld a, $7E\nld [de], a\ninc de\nld a, $00"), + ASM("ld a, $5E\nld [de], a\ninc de\nld a, $08"), fill_nop=True) + # Never load the bowwow tiles in the first VRAM bank, as we do not need them. + rom.patch(0x00, 0x2EB0, ASM("ld a, [$DB56]\ncp $01\nld a, $A4\njr z, $18"), "", fill_nop=True) + + # Patch the location where bowwow stores chain X/Y positions so it does not conflict with a lot of other things + rom.patch(0x05, 0x00BE, ASM("ld hl, $D100"), ASM("ld hl, $D180")) + rom.patch(0x05, 0x0275, ASM("ld hl, $D100"), ASM("ld hl, $D180")) + rom.patch(0x05, 0x03AD, ASM("ld [$D100], a"), ASM("ld [$D180], a")) + rom.patch(0x05, 0x03BD, ASM("ld de, $D100"), ASM("ld de, $D180")) + rom.patch(0x05, 0x049F, ASM("ld hl, $D100"), ASM("ld hl, $D180")) + rom.patch(0x05, 0x04C2, ASM("ld a, [$D100]"), ASM("ld a, [$D180]")) + rom.patch(0x05, 0x03C0, ASM("ld hl, $D101"), ASM("ld hl, $D181")) + rom.patch(0x05, 0x0418, ASM("ld [$D106], a"), ASM("ld [$D186], a")) + rom.patch(0x05, 0x0423, ASM("ld de, $D106"), ASM("ld de, $D186")) + rom.patch(0x05, 0x0426, ASM("ld hl, $D105"), ASM("ld hl, $D185")) + + rom.patch(0x19, 0x3A4E, ASM("ld hl, $D100"), ASM("ld hl, $D180")) + rom.patch(0x19, 0x3A5A, ASM("ld hl, $D110"), ASM("ld hl, $D190")) + + rom.patch(0x05, 0x00D9, ASM("ld hl, $D110"), ASM("ld hl, $D190")) + rom.patch(0x05, 0x026E, ASM("ld hl, $D110"), ASM("ld hl, $D190")) + rom.patch(0x05, 0x03BA, ASM("ld [$D110], a"), ASM("ld [$D190], a")) + rom.patch(0x05, 0x03DD, ASM("ld de, $D110"), ASM("ld de, $D190")) + rom.patch(0x05, 0x0480, ASM("ld hl, $D110"), ASM("ld hl, $D190")) + rom.patch(0x05, 0x04B5, ASM("ld a, [$D110]"), ASM("ld a, [$D190]")) + rom.patch(0x05, 0x03E0, ASM("ld hl, $D111"), ASM("ld hl, $D191")) + rom.patch(0x05, 0x0420, ASM("ld [$D116], a"), ASM("ld [$D196], a")) + rom.patch(0x05, 0x044d, ASM("ld de, $D116"), ASM("ld de, $D196")) + rom.patch(0x05, 0x0450, ASM("ld hl, $D115"), ASM("ld hl, $D195")) + + rom.patch(0x05, 0x0039, ASM("ld [$D154], a"), "", fill_nop=True) # normally this stores the index to bowwow, for the kiki fight + rom.patch(0x05, 0x013C, ASM("ld [$D150], a"), ASM("ld [$D197], a")) + rom.patch(0x05, 0x0144, ASM("ld [$D151], a"), ASM("ld [$D198], a")) + rom.patch(0x05, 0x02F9, ASM("ld [$D152], a"), ASM("ld [$D199], a")) + rom.patch(0x05, 0x0335, ASM("ld a, [$D152]"), ASM("ld a, [$D199]")) + rom.patch(0x05, 0x0485, ASM("ld a, [$D151]"), ASM("ld a, [$D198]")) + rom.patch(0x05, 0x04A4, ASM("ld a, [$D150]"), ASM("ld a, [$D197]")) + + # Patch the bowwow create code to call our custom check of we are in swamp function. + if everywhere: + # Load followers in dungeons, caves, etc + rom.patch(0x01, 0x1FC1, ASM("ret z"), "", fill_nop=True) + rom.patch(0x01, 0x1FC4, ASM("ret z"), "", fill_nop=True) + rom.patch(0x01, 0x1FC7, ASM("ret z"), "", fill_nop=True) + rom.patch(0x01, 0x1FCA, ASM("ret c"), "", fill_nop=True) # dungeon + # rom.patch(0x01, 0x1FBC, ASM("ret nz"), "", fill_nop=True) # sidescroller: TOFIX this breaks fishing minigame reward + else: + # Patch the bowwow create code to call our custom check of we are in swamp function. + rom.patch(0x01, 0x211F, ASM("ldh a, [$F6]\ncp $A7\nret z\nld a, [$DB56]\ncp $01\njr nz, $36"), ASM(""" + ld a, $07 + rst 8 + ld a, e + and a + ret z + """), fill_nop=True) + # Patch bowwow to not stay around when we move from map to map + rom.patch(0x05, 0x0049, 0x0054, ASM(""" + cp [hl] + jr z, Continue + ld hl, $C280 + add hl, bc + ld [hl], b + ret +Continue: + """), fill_nop=True) + + # Patch madam meow meow to not take bowwow + rom.patch(0x06, 0x1BD7, ASM("ld a, [$DB66]\nand $02"), ASM("ld a, $00\nand $02"), fill_nop=True) + + # Patch kiki not to react to bowwow, as bowwow is not with link at this map + rom.patch(0x07, 0x18A8, ASM("ld a, [$DB56]\ncp $01"), ASM("ld a, $00\ncp $01"), fill_nop=True) + + # Patch the color dungeon entrance not to check for bowwow + rom.patch(0x02, 0x340D, ASM("ld hl, $DB56\nor [hl]"), "", fill_nop=True) + + # Patch richard to ignore bowwow + rom.patch(0x06, 0x006C, ASM("ld a, [$DB56]"), ASM("xor a"), fill_nop=True) + + # Patch to modify how bowwow eats enemies, normally it just unloads them, but we call our handler in bank 3E + rom.patch(0x05, 0x03A0, 0x03A8, ASM(""" + push bc + ld b, d + ld c, e + ld a, $08 + rst 8 + pop bc + ret + """), fill_nop=True) + rom.patch(0x05, 0x0387, ASM("ld a, $03\nldh [$F2], a"), "", fill_nop=True) # remove the default chomp sfx + + # Various enemies + rom.banks[0x14][0x1218 + 0xC5] = 0x01 # Urchin + rom.banks[0x14][0x1218 + 0x93] = 0x01 # MadBomber + rom.banks[0x14][0x1218 + 0x51] = 0x01 # Swinging ball&chain golden leaf enemy + rom.banks[0x14][0x1218 + 0xF2] = 0x01 # Color dungeon flying hopper + rom.banks[0x14][0x1218 + 0xF3] = 0x01 # Color dungeon hopper + rom.banks[0x14][0x1218 + 0xE9] = 0x01 # Color dungeon shell + rom.banks[0x14][0x1218 + 0xEA] = 0x01 # Color dungeon shell + rom.banks[0x14][0x1218 + 0xEB] = 0x01 # Color dungeon shell + rom.banks[0x14][0x1218 + 0xEC] = 0x01 # Color dungeon thing + rom.banks[0x14][0x1218 + 0xED] = 0x01 # Color dungeon thing + rom.banks[0x14][0x1218 + 0xEE] = 0x01 # Color dungeon thing + rom.banks[0x14][0x1218 + 0x87] = 0x01 # Lanmola (for D4 key) + rom.banks[0x14][0x1218 + 0x88] = 0x01 # Armos knight (for D6 key) + rom.banks[0x14][0x1218 + 0x16] = 0x01 # Spark + rom.banks[0x14][0x1218 + 0x17] = 0x01 # Spark + rom.banks[0x14][0x1218 + 0x2C] = 0x01 # Spiked beetle + rom.banks[0x14][0x1218 + 0x90] = 0x01 # Three of a kind (screw these guys) + rom.banks[0x14][0x1218 + 0x18] = 0x01 # Pols voice + rom.banks[0x14][0x1218 + 0x50] = 0x01 # Boo buddy + rom.banks[0x14][0x1218 + 0xA2] = 0x01 # Pirana plant + rom.banks[0x14][0x1218 + 0x52] = 0x01 # Tractor device + rom.banks[0x14][0x1218 + 0x53] = 0x01 # Tractor device (D3) + rom.banks[0x14][0x1218 + 0x55] = 0x01 # Bounding bombite + rom.banks[0x14][0x1218 + 0x56] = 0x01 # Timer bombite + rom.banks[0x14][0x1218 + 0x57] = 0x01 # Pairod + rom.banks[0x14][0x1218 + 0x15] = 0x01 # Antifairy + rom.banks[0x14][0x1218 + 0xA0] = 0x01 # Peahat + rom.banks[0x14][0x1218 + 0x9C] = 0x01 # Star + rom.banks[0x14][0x1218 + 0xA1] = 0x01 # Snake + rom.banks[0x14][0x1218 + 0xBD] = 0x01 # Vire + rom.banks[0x14][0x1218 + 0xE4] = 0x01 # Moblin boss + + # Bosses + rom.banks[0x14][0x1218 + 0x59] = 0x01 # Moldorm + rom.banks[0x14][0x1218 + 0x5C] = 0x01 # Genie + rom.banks[0x14][0x1218 + 0x5B] = 0x01 # Slime Eye + rom.patch(0x04, 0x0AC4, ASM("ld [hl], $28"), ASM("ld [hl], $FF")) # give more time before slimeeye unsplits + rom.patch(0x04, 0x0B05, ASM("ld [hl], $50"), ASM("ld [hl], $FF")) # give more time before slimeeye unsplits + rom.banks[0x14][0x1218 + 0x65] = 0x01 # Angler fish + rom.banks[0x14][0x1218 + 0x5D] = 0x01 # Slime eel + rom.banks[0x14][0x1218 + 0x5A] = 0x01 # Facade + rom.banks[0x14][0x1218 + 0x63] = 0x01 # Eagle + rom.banks[0x14][0x1218 + 0x62] = 0x01 # Hot head + rom.banks[0x14][0x1218 + 0xF9] = 0x01 # Hardhit beetle + rom.banks[0x14][0x1218 + 0xE6] = 0x01 # Nightmare + + # Minibosses + rom.banks[0x14][0x1218 + 0x81] = 0x01 # Rolling bones + rom.banks[0x14][0x1218 + 0x89] = 0x01 # Hinox + rom.banks[0x14][0x1218 + 0x8E] = 0x01 # Cue ball + rom.banks[0x14][0x1218 + 0x5E] = 0x01 # Gnoma + rom.banks[0x14][0x1218 + 0x5F] = 0x01 # Master stalfos + rom.banks[0x14][0x1218 + 0x92] = 0x01 # Smasher + rom.banks[0x14][0x1218 + 0xBC] = 0x01 # Grim creeper + rom.banks[0x14][0x1218 + 0xBE] = 0x01 # Blaino + rom.banks[0x14][0x1218 + 0xF8] = 0x01 # Giant buzz blob + rom.banks[0x14][0x1218 + 0xF4] = 0x01 # Avalaunch + + # NPCs + rom.banks[0x14][0x1218 + 0x6F] = 0x01 # Dog + rom.banks[0x14][0x1218 + 0x6E] = 0x01 # Butterfly + rom.banks[0x14][0x1218 + 0x6C] = 0x01 # Cucco + rom.banks[0x14][0x1218 + 0x70] = 0x01 # Kid + rom.banks[0x14][0x1218 + 0x71] = 0x01 # Kid + rom.banks[0x14][0x1218 + 0x72] = 0x01 # Kid + rom.banks[0x14][0x1218 + 0x73] = 0x01 # Kid + rom.banks[0x14][0x1218 + 0xD0] = 0x01 # Animal + rom.banks[0x14][0x1218 + 0xD1] = 0x01 # Animal + rom.banks[0x14][0x1218 + 0xD2] = 0x01 # Animal + rom.banks[0x14][0x1218 + 0xD3] = 0x01 # Animal + + +def bowwowMapPatches(rom): + # Remove all the cystal things that can only be destroyed with a sword. + for n in range(0x100, 0x2FF): + re = RoomEditor(rom, n) + re.objects = list(filter(lambda obj: obj.type_id != 0xDD, re.objects)) + re.store(rom) diff --git a/worlds/ladx/LADXR/patches/chest.py b/worlds/ladx/LADXR/patches/chest.py new file mode 100644 index 000000000000..c092fc75f5bd --- /dev/null +++ b/worlds/ladx/LADXR/patches/chest.py @@ -0,0 +1,59 @@ +from ..assembler import ASM +from ..utils import formatText +from ..locations.constants import CHEST_ITEMS + + +def fixChests(rom): + # Patch the chest code, so it can give a lvl1 sword. + # Normally, there is some code related to the owl event when getting the tail key, + # as we patched out the owl. We use it to jump to our custom code in bank $3E to handle getting the item + rom.patch(0x03, 0x109C, ASM(""" + cp $11 ; if not tail key, skip + jr nz, end + push af + ld a, [$C501] + ld e, a + ld hl, $C2F0 + add hl, de + ld [hl], $38 + pop af + end: + ld e, a + cp $21 ; if is message chest or higher number, next instruction is to skip giving things. + """), ASM(""" + ld a, $06 ; GiveItemMultiworld + rst 8 + + and a ; clear the carry flag to always skip giving stuff. + """), fill_nop=True) + + # Instead of the normal logic to on which sprite data to show, we jump to our custom code in bank 3E. + rom.patch(0x07, 0x3C36, None, ASM(""" + ld a, $01 + rst 8 + jp $7C5E + """), fill_nop=True) + + # Instead of the normal logic of showing the proper dialog, we jump to our custom code in bank 3E. + rom.patch(0x07, 0x3C9C, None, ASM(""" + ld a, $0A ; showItemMessageMultiworld + rst 8 + jp $7CE9 + """)) + + # Sound to play is normally loaded from a table, which is no longer big enough. So always use the same sound. + rom.patch(0x07, 0x3C81, ASM(""" + add hl, de + ld a, [hl] + """), ASM("ld a, $01"), fill_nop=True) + + # Always spawn seashells even if you have the L2 sword + rom.patch(0x14, 0x192F, ASM("ld a, $1C"), ASM("ld a, $20")) + + rom.texts[0x9A] = formatText("You found 10 {BOMB}!") + + +def setMultiChest(rom, option): + room = 0x2F2 + addr = room + 0x560 + rom.banks[0x14][addr] = CHEST_ITEMS[option] diff --git a/worlds/ladx/LADXR/patches/core.py b/worlds/ladx/LADXR/patches/core.py new file mode 100644 index 000000000000..a202e661f945 --- /dev/null +++ b/worlds/ladx/LADXR/patches/core.py @@ -0,0 +1,539 @@ +from ..assembler import ASM +from ..entranceInfo import ENTRANCE_INFO +from ..roomEditor import RoomEditor, ObjectWarp, ObjectHorizontal +from ..backgroundEditor import BackgroundEditor +from .. import utils + + +def bugfixWrittingWrongRoomStatus(rom): + # The normal rom contains a pretty nasty bug where door closing triggers in D7/D8 can effect doors in + # dungeons D1-D6. This fix should prevent this. + rom.patch(0x02, 0x1D21, 0x1D3C, ASM("call $5B9F"), fill_nop=True) + +def fixEggDeathClearingItems(rom): + rom.patch(0x01, 0x1E79, ASM("cp $0A"), ASM("cp $08")) + +def fixWrongWarp(rom): + rom.patch(0x00, 0x18CE, ASM("cp $04"), ASM("cp $03")) + re = RoomEditor(rom, 0x2b) + for x in range(10): + re.removeObject(x, 7) + re.objects.append(ObjectHorizontal(0, 7, 0x2C, 10)) + while len(re.getWarps()) < 4: + re.objects.append(ObjectWarp(1, 3, 0x7a, 80, 124)) + re.store(rom) + +def bugfixBossroomTopPush(rom): + rom.patch(0x14, 0x14D9, ASM(""" + ldh a, [$99] + dec a + ldh [$99], a + """), ASM(""" + jp $7F80 + """), fill_nop=True) + rom.patch(0x14, 0x3F80, "00" * 0x80, ASM(""" + ldh a, [$99] + cp $50 + jr nc, up +down: + inc a + ldh [$99], a + jp $54DE +up: + dec a + ldh [$99], a + jp $54DE + """), fill_nop=True) + +def bugfixPowderBagSprite(rom): + rom.patch(0x03, 0x2055, "8E16", "0E1E") + +def easyColorDungeonAccess(rom): + re = RoomEditor(rom, 0x312) + re.entities = [(3, 1, 246), (6, 1, 247)] + re.store(rom) + +def removeGhost(rom): + ## Ghost patch + # Do not have the ghost follow you after dungeon 4 + rom.patch(0x03, 0x1E1B, ASM("LD [$DB79], A"), "", fill_nop=True) + +def alwaysAllowSecretBook(rom): + rom.patch(0x15, 0x3F23, ASM("ld a, [$DB0E]\ncp $0E"), ASM("xor a\ncp $00"), fill_nop=True) + rom.patch(0x15, 0x3F2A, 0x3F30, "", fill_nop=True) + +def cleanup(rom): + # Remove unused rooms to make some space in the rom + re = RoomEditor(rom, 0x2C4) + re.objects = [] + re.entities = [] + re.store(rom, 0x2C4) + re.store(rom, 0x2D4) + re.store(rom, 0x277) + re.store(rom, 0x278) + re.store(rom, 0x279) + re.store(rom, 0x1ED) + re.store(rom, 0x1FC) # Beta room + + rom.texts[0x02B] = b'' # unused text + + +def disablePhotoPrint(rom): + rom.patch(0x28, 0x07CC, ASM("ldh [$01], a\nldh [$02], a"), "", fill_nop=True) # do not reset the serial link + rom.patch(0x28, 0x0483, ASM("ld a, $13"), ASM("jr $EA", 0x4483)) # Do not print on A press, but jump to cancel + rom.patch(0x28, 0x0492, ASM("ld hl, $4439"), ASM("ret"), fill_nop=True) # Do not show the print/cancel overlay + +def fixMarinFollower(rom): + # Allow opening of D0 with marin + rom.patch(0x02, 0x3402, ASM("ld a, [$DB73]"), ASM("xor a"), fill_nop=True) + # Instead of uselessly checking for sidescroller rooms for follower spawns, check for color dungeon instead + rom.patch(0x01, 0x1FCB, 0x1FD3, ASM("cp $FF\nret z"), fill_nop=True) + # Do not load marin graphics in color dungeon + rom.patch(0x00, 0x2EA6, 0x2EB0, ASM("cp $FF\njp $2ED3"), fill_nop=True) + # Fix marin on taltal bridge causing a lockup if you have marin with you + # This changes the location where the index to the marin entity is stored from it's normal location + # To the memory normal reserved for progress on the egg maze (which is reset to 0 on a warp) + rom.patch(0x18, 0x1EF7, ASM("ld [$C50F], a"), ASM("ld [$C5AA], a")) + rom.patch(0x18, 0x2126, ASM("ld a, [$C50F]"), ASM("ld a, [$C5AA]")) + rom.patch(0x18, 0x2139, ASM("ld a, [$C50F]"), ASM("ld a, [$C5AA]")) + rom.patch(0x18, 0x214F, ASM("ld a, [$C50F]"), ASM("ld a, [$C5AA]")) + rom.patch(0x18, 0x2166, ASM("ld a, [$C50F]"), ASM("ld a, [$C5AA]")) + +def quickswap(rom, button): + rom.patch(0x00, 0x1094, ASM("jr c, $49"), ASM("jr nz, $49")) # prevent agressive key repeat + rom.patch(0x00, 0x10BC, # Patch the open minimap code to swap the your items instead + ASM("xor a\nld [$C16B], a\nld [$C16C], a\nld [$DB96], a\nld a, $07\nld [$DB95], a"), ASM(""" + ld a, [$DB%02X] + ld e, a + ld a, [$DB%02X] + ld [$DB%02X], a + ld a, e + ld [$DB%02X], a + ret + """ % (button, button + 2, button, button + 2))) + +def injectMainLoop(rom): + rom.patch(0x00, 0x0346, ASM(""" + ldh a, [$FE] + and a + jr z, $08 + """), ASM(""" + ; Call the mainloop handler + xor a + rst 8 + """), fill_nop=True) + +def warpHome(rom): + # Patch the S&Q menu to allow 3 options + rom.patch(0x01, 0x012A, 0x0150, ASM(""" + ld hl, $C13F + call $6BA8 ; make sound on keypress + ldh a, [$CC] ; load joystick status + and $04 ; if up + jr z, noUp + dec [hl] +noUp: + ldh a, [$CC] ; load joystick status + and $08 ; if down + jr z, noDown + inc [hl] +noDown: + + ld a, [hl] + cp $ff + jr nz, noWrapUp + ld a, $02 +noWrapUp: + cp $03 + jr nz, noWrapDown + xor a +noWrapDown: + ld [hl], a + jp $7E02 + """), fill_nop=True) + rom.patch(0x01, 0x3E02, 0x3E20, ASM(""" + swap a + add a, $48 + ld hl, $C018 + ldi [hl], a + ld a, $24 + ldi [hl], a + ld a, $BE + ldi [hl], a + ld [hl], $00 + ret + """), fill_nop=True) + + rom.patch(0x01, 0x00B7, ASM(""" + ld a, [$C13F] + cp $01 + jr z, $3B + """), ASM(""" + ld a, [$C13F] + jp $7E20 + """), fill_nop=True) + + re = RoomEditor(rom, 0x2a3) + warp = re.getWarps()[0] + + type = 0x00 + map = 0x00 + room = warp.room + x = warp.target_x + y = warp.target_y + + one_way = [ + 'd0', + 'd1', + 'd3', + 'd4', + 'd6', + 'd8', + 'animal_cave', + 'right_fairy', + 'rooster_grave', + 'prairie_left_cave2', + 'prairie_left_fairy', + 'armos_fairy', + 'boomerang_cave', + 'madbatter_taltal', + 'forest_madbatter', + ] + + one_way = {ENTRANCE_INFO[x].room for x in one_way} + + if warp.room in one_way: + # we're starting at a one way exit room + # warp indoors to avoid soft locks + type = 0x01 + map = 0x10 + room = 0xa3 + x = 0x50 + y = 0x7f + + rom.patch(0x01, 0x3E20, 0x3E6B, ASM(""" + ; First, handle save & quit + cp $01 + jp z, $40F9 + and a + jp z, $40BE ; return to normal "return to game" handling + + ld a, [$C509] ; Check if we have an item in the shop + and a + jp nz, $40BE ; return to normal "return to game" handling + + ld a, $0B + ld [$DB95], a + call $0C7D + + ; Replace warp0 tile data, and put link on that tile. + ld a, $%02x ; Type + ld [$D401], a + ld a, $%02x ; Map + ld [$D402], a + ld a, $%02x ; Room + ld [$D403], a + ld a, $%02x ; X + ld [$D404], a + ld a, $%02x ; Y + ld [$D405], a + + ldh a, [$98] + swap a + and $0F + ld e, a + ldh a, [$99] + sub $08 + and $F0 + or e + ld [$D416], a + + ld a, $07 + ld [$DB96], a + ret + jp $40BE ; return to normal "return to game" handling + """ % (type, map, room, x, y)), fill_nop=True) + + # Patch the RAM clear not to delete our custom dialog when we screen transition + rom.patch(0x01, 0x042C, "C629", "6B7E") + rom.patch(0x01, 0x3E6B, 0x3FFF, ASM(""" + ld bc, $A0 + call $29DC + ld bc, $1200 + ld hl, $C100 + call $29DF + ret + """), fill_nop=True) + # Patch the S&Q screen to have 3 options. + be = BackgroundEditor(rom, 0x0D) + for n in range(2, 18): + be.tiles[0x99C0 + n] = be.tiles[0x9980 + n] + be.tiles[0x99A0 + n] = be.tiles[0x9960 + n] + be.tiles[0x9980 + n] = be.tiles[0x9940 + n] + be.tiles[0x9960 + n] = be.tiles[0x98e0 + n] + be.tiles[0x9960 + 10] = 0xCE + be.tiles[0x9960 + 11] = 0xCF + be.tiles[0x9960 + 12] = 0xC4 + be.tiles[0x9960 + 13] = 0x7F + be.tiles[0x9960 + 14] = 0x7F + be.store(rom) + + sprite_data = [ + 0b00000000, + 0b01000100, + 0b01000101, + 0b01000101, + 0b01111101, + 0b01000101, + 0b01000101, + 0b01000100, + + 0b00000000, + 0b11100100, + 0b00010110, + 0b00010101, + 0b00010100, + 0b00010100, + 0b00010100, + 0b11100100, + ] + for n in range(32): + rom.banks[0x0F][0x08E0 + n] = sprite_data[n // 2] + + +def addFrameCounter(rom, check_count): + # Patch marin giving the start the game to jump to a custom handler + rom.patch(0x05, 0x1299, ASM("ld a, $01\ncall $2385"), ASM("push hl\nld a, $0D\nrst 8\npop hl"), fill_nop=True) + + # Add code that needs to be called every frame to tick our ingame time counter. + rom.patch(0x00, 0x0091, "00" * (0x100 - 0x91), ASM(""" + ld a, [$DB95] ;Get the gameplay type + dec a ; and if it was 1 + ret z ; we are at the credits and the counter should stop. + + ; Check if the timer expired + ld hl, $FF0F + bit 2, [hl] + ret z + res 2, [hl] + + ; Increase the "subsecond" counter, and continue if it "overflows" + call $27D0 ; Enable SRAM + ld hl, $B000 + ld a, [hl] + inc a + cp $20 + ld [hl], a + ret nz + xor a + ldi [hl], a + + ; Increase the seconds counter/minutes/hours counter +increaseSecMinHours: + ld a, [hl] + inc a + daa + ld [hl], a + cp $60 + ret nz + xor a + ldi [hl], a + jr increaseSecMinHours + """), fill_nop=True) + # Replace a cgb check with the call to our counter code. + rom.patch(0x00, 0x0367, ASM("ld a, $0C\ncall $0B0B"), ASM("call $0091\nld a, $2C")) + + # Do not switch to 8x8 sprite mode + rom.patch(0x17, 0x2E9E, ASM("res 2, [hl]"), "", fill_nop=True) + # We need to completely reorder link sitting on the raft to work with 16x8 sprites. + sprites = rom.banks[0x38][0x1600:0x1800] + sprites[0x1F0:0x200] = b'\x00' * 16 + for index, position in enumerate( + (0, 0x1F, + 1, 0x1F, 2, 0x1F, + 7, 8, + 3, 9, 4, 10, 5, 11, 6, 12, + 3, 13, 4, 14, 5, 15, 6, 16, + 3, 17, 4, 18, 5, 19, 6, 20, + )): + rom.banks[0x38][0x1600+index*0x10:0x1610+index*0x10] = sprites[position*0x10:0x10+position*0x10] + rom.patch(0x27, 0x376E, 0x3776, "00046601", fill_nop=True) + rom.patch(0x27, 0x384E, ASM("ld c, $08"), ASM("ld c, $04")) + rom.patch(0x27, 0x3776, 0x3826, + "FA046002" + "0208640402006204" + "0A106E030A086C030A006A030AF86803" + + "FA046002" + "0208640402006204" + "0A1076030A0874030A0072030AF87003" + + "FA046002" + "0208640402006204" + "0A107E030A087C030A007A030AF87803" + , fill_nop=True) + rom.patch(0x27, 0x382E, ASM("ld a, $6C"), ASM("ld a, $80")) # OAM start position + rom.patch(0x27, 0x384E, ASM("ld c, $08"), ASM("ld c, $04")) # Amount of overlay OAM data + rom.patch(0x27, 0x3826, 0x382E, ASM("dw $7776, $7792, $77AE, $7792")) # pointers to animation + rom.patch(0x27, 0x3846, ASM("ld c, $2C"), ASM("ld c, $1C")) # Amount of OAM data + + # TODO: fix flying windfish + # Upper line of credits roll into "TIME" + rom.patch(0x17, 0x069D, 0x0713, ASM(""" + ld hl, OAMData + ld de, $C000 ; OAM Buffer + ld bc, $0048 + call $2914 + ret +OAMData: + db $20, $18, $34, $00 ;T + db $20, $20, $20, $00 ;I + db $20, $28, $28, $00 ;M + db $20, $30, $18, $00 ;E + + db $20, $70, $16, $00 ;D + db $20, $78, $18, $00 ;E + db $20, $80, $10, $00 ;A + db $20, $88, $34, $00 ;T + db $20, $90, $1E, $00 ;H + + db $50, $18, $14, $00 ;C + db $50, $20, $1E, $00 ;H + db $50, $28, $18, $00 ;E + db $50, $30, $14, $00 ;C + db $50, $38, $24, $00 ;K + db $50, $40, $32, $00 ;S + + db $68, $38, $%02x, $00 ;0 + db $68, $40, $%02x, $00 ;0 + db $68, $48, $%02x, $00 ;0 + + """ % ((((check_count // 100) % 10) * 2) | 0x40, (((check_count // 10) % 10) * 2) | 0x40, ((check_count % 10) * 2) | 0x40), 0x469D), fill_nop=True) + # Lower line of credits roll into XX XX XX + rom.patch(0x17, 0x0784, 0x082D, ASM(""" + ld hl, OAMData + ld de, $C048 ; OAM Buffer + ld bc, $0038 + call $2914 + + call $27D0 ; Enable SRAM + ld hl, $C04A + ld a, [$B003] ; hours + call updateOAM + ld a, [$B002] ; minutes + call updateOAM + ld a, [$B001] ; seconds + call updateOAM + + ld a, [$DB58] ; death count high + call updateOAM + ld a, [$DB57] ; death count low + call updateOAM + + ld a, [$B011] ; check count high + call updateOAM + ld a, [$B010] ; check count low + call updateOAM + ret + +updateOAM: + ld de, $0004 + ld b, a + swap a + and $0F + add a, a + or $40 + ld [hl], a + add hl, de + + ld a, b + and $0F + add a, a + or $40 + ld [hl], a + add hl, de + ret +OAMData: + db $38, $18, $40, $00 ;0 (10 hours) + db $38, $20, $40, $00 ;0 (1 hours) + db $38, $30, $40, $00 ;0 (10 minutes) + db $38, $38, $40, $00 ;0 (1 minutes) + db $38, $48, $40, $00 ;0 (10 seconds) + db $38, $50, $40, $00 ;0 (1 seconds) + + db $00, $00, $40, $00 ;0 (1000 death) + db $38, $80, $40, $00 ;0 (100 death) + + db $38, $88, $40, $00 ;0 (10 death) + db $38, $90, $40, $00 ;0 (1 death) + + ; checks + db $00, $00, $40, $00 ;0 + db $68, $18, $40, $00 ;0 + db $68, $20, $40, $00 ;0 + db $68, $28, $40, $00 ;0 + + """, 0x4784), fill_nop=True) + + # Grab the "mostly" complete A-Z font + sprites = rom.banks[0x38][0x1100:0x1400] + for index, position in enumerate(( + 0x10, 0x20, # A + 0x11, 0x21, # B + 0x12, 0x12 | 0x100, # C + 0x13, 0x23, # D + 0x14, 0x24, # E + 0x14, 0x25, # F + 0x12, 0x22, # G + 0x20 | 0x100, 0x26, # H + 0x17, 0x17 | 0x100, # I + 0x28, 0x28, # J + 0x19, 0x29, # K + 0x06, 0x07, # L + 0x1A, 0x2A, # M + 0x1B, 0x2B, # N + 0x00, 0x00, # O? + 0x00, 0x00, # P? + #0x00, 0x00, # Q? + 0x11, 0x18, # R + 0x1C, 0x2C, # S + 0x1D, 0x2D, # T + 0x26, 0x10, # U + 0x00, 0x00, # V? + 0x1E, 0x2E, # W + #0x00, 0x00, # X? + #0x00, 0x00, # Y? + 0x27, 0x27, # Z + )): + sprite = sprites[(position&0xFF)*0x10:0x10+(position&0xFF)*0x10] + if position & 0x100: + for n in range(4): + sprite[n * 2], sprite[14 - n * 2] = sprite[14 - n * 2], sprite[n * 2] + sprite[n * 2 + 1], sprite[15 - n * 2] = sprite[15 - n * 2], sprite[n * 2 + 1] + rom.banks[0x38][0x1100+index*0x10:0x1110+index*0x10] = sprite + + + # Number graphics change for the end + tile_graphics = """ +........ ........ ........ ........ ........ ........ ........ ........ ........ ........ +.111111. ..1111.. .111111. .111111. ..11111. 11111111 .111111. 11111111 .111111. .111111. +11333311 .11331.. 11333311 11333311 .113331. 13333331 11333311 13333331 11333311 11333311 +13311331 113331.. 13311331 13311331 1133331. 13311111 13311331 11111331 13311331 13311331 +13311331 133331.. 13311331 11111331 1331331. 1331.... 13311331 ...11331 13311331 13311331 +13311331 133331.. 11111331 ....1331 1331331. 1331.... 13311111 ...13311 13311331 13311331 +13311331 111331.. ...13311 .1111331 1331331. 1331111. 1331.... ..11331. 13311331 13311331 +13311331 ..1331.. ..11331. .1333331 13313311 13333311 1331111. ..13311. 11333311 11333331 +13311331 ..1331.. ..13311. .1111331 13333331 13311331 13333311 .11331.. 13311331 .1111331 +13311331 ..1331.. .11331.. ....1331 11113311 11111331 13311331 .13311.. 13311331 ....1331 +13311331 ..1331.. .13311.. ....1331 ...1331. ....1331 13311331 11331... 13311331 ....1331 +13311331 ..1331.. 11331... 11111331 ...1331. 11111331 13311331 13311... 13311331 11111331 +13311331 ..1331.. 13311111 13311331 ...1331. 13311331 13311331 1331.... 13311331 13311331 +11333311 ..1331.. 13333331 11333311 ...1331. 11333311 11333311 1331.... 11333311 11333311 +.111111. ..1111.. 11111111 .111111. ...1111. .111111. .111111. 1111.... .111111. .111111. +........ ........ ........ ........ ........ ........ ........ ........ ........ ........ +""".strip() + for n in range(10): + gfx_high = "\n".join([line.split(" ")[n] for line in tile_graphics.split("\n")[:8]]) + gfx_low = "\n".join([line.split(" ")[n] for line in tile_graphics.split("\n")[8:]]) + rom.banks[0x38][0x1400+n*0x20:0x1410+n*0x20] = utils.createTileData(gfx_high) + rom.banks[0x38][0x1410+n*0x20:0x1420+n*0x20] = utils.createTileData(gfx_low) diff --git a/worlds/ladx/LADXR/patches/desert.py b/worlds/ladx/LADXR/patches/desert.py new file mode 100644 index 000000000000..e3f008661cf2 --- /dev/null +++ b/worlds/ladx/LADXR/patches/desert.py @@ -0,0 +1,7 @@ +from ..roomEditor import RoomEditor + + +def desertAccess(rom): + re = RoomEditor(rom, 0x0FD) + re.entities = [(6, 2, 0xC4)] + re.store(rom) diff --git a/worlds/ladx/LADXR/patches/droppedKey.py b/worlds/ladx/LADXR/patches/droppedKey.py new file mode 100644 index 000000000000..d24b8b76c7a9 --- /dev/null +++ b/worlds/ladx/LADXR/patches/droppedKey.py @@ -0,0 +1,134 @@ +from ..assembler import ASM + + +def fixDroppedKey(rom): + # Patch the rendering code to use the dropped key rendering code. + rom.patch(0x03, 0x1C99, None, ASM(""" + ld a, $04 + rst 8 + jp $5CA6 + """)) + + # Patch the key pickup code to use the chest pickup code. + rom.patch(0x03, 0x248F, None, ASM(""" + ldh a, [$F6] ; load room nr + cp $7C ; L4 Side-view room where the key drops + jr nz, notSpecialSideView + + ld hl, $D969 ; status of the room above the side-view where the key drops in dungeon 4 + set 4, [hl] +notSpecialSideView: + call $512A ; mark room as done + + ; Handle item effect + ld a, $06 ; giveItemMultiworld + rst 8 + + ldh a, [$F1] ; Load active sprite variant to see if this is just a normal small key + cp $1A + jr z, isAKey + + ;Show message (if not a key) + ld a, $0A ; showMessageMultiworld + rst 8 +isAKey: + ret + """)) + rom.patch(0x03, 0x24B7, "3E", "3E") # sanity check + + # Mark all dropped keys as keys by default. + for n in range(0x316): + rom.banks[0x3E][0x3800 + n] = 0x1A + # Set the proper angler key by default + rom.banks[0x3E][0x3800 + 0x0CE] = 0x12 + rom.banks[0x3E][0x3800 + 0x1F8] = 0x12 + # Set the proper bird key by default + rom.banks[0x3E][0x3800 + 0x27A] = 0x14 + # Set the proper face key by default + rom.banks[0x3E][0x3800 + 0x27F] = 0x13 + + # Set the proper hookshot key by default + rom.banks[0x3E][0x3800 + 0x180] = 0x03 + + # Set the proper golden leaves + rom.banks[0x3E][0x3800 + 0x058] = 0x15 + rom.banks[0x3E][0x3800 + 0x05a] = 0x15 + rom.banks[0x3E][0x3800 + 0x2d2] = 0x15 + rom.banks[0x3E][0x3800 + 0x2c5] = 0x15 + rom.banks[0x3E][0x3800 + 0x2c6] = 0x15 + + # Set the slime key drop. + rom.banks[0x3E][0x3800 + 0x0C6] = 0x0F + + # Set the heart pieces + rom.banks[0x3E][0x3800 + 0x000] = 0x80 + rom.banks[0x3E][0x3800 + 0x2A4] = 0x80 + rom.banks[0x3E][0x3800 + 0x2B1] = 0x80 # fishing game, unused + rom.banks[0x3E][0x3800 + 0x044] = 0x80 + rom.banks[0x3E][0x3800 + 0x2AB] = 0x80 + rom.banks[0x3E][0x3800 + 0x2DF] = 0x80 + rom.banks[0x3E][0x3800 + 0x2E5] = 0x80 + rom.banks[0x3E][0x3800 + 0x078] = 0x80 + rom.banks[0x3E][0x3800 + 0x2E6] = 0x80 + rom.banks[0x3E][0x3800 + 0x1E8] = 0x80 + rom.banks[0x3E][0x3800 + 0x1F2] = 0x80 + rom.banks[0x3E][0x3800 + 0x2BA] = 0x80 + + # Set the seashells + rom.banks[0x3E][0x3800 + 0x0A3] = 0x20 + rom.banks[0x3E][0x3800 + 0x2B2] = 0x20 + rom.banks[0x3E][0x3800 + 0x0A5] = 0x20 + rom.banks[0x3E][0x3800 + 0x0A6] = 0x20 + rom.banks[0x3E][0x3800 + 0x08B] = 0x20 + rom.banks[0x3E][0x3800 + 0x074] = 0x20 + rom.banks[0x3E][0x3800 + 0x0A4] = 0x20 + rom.banks[0x3E][0x3800 + 0x0D2] = 0x20 + rom.banks[0x3E][0x3800 + 0x0E9] = 0x20 + rom.banks[0x3E][0x3800 + 0x0B9] = 0x20 + rom.banks[0x3E][0x3800 + 0x0F8] = 0x20 + rom.banks[0x3E][0x3800 + 0x0A8] = 0x20 + rom.banks[0x3E][0x3800 + 0x0FF] = 0x20 + rom.banks[0x3E][0x3800 + 0x1E3] = 0x20 + rom.banks[0x3E][0x3800 + 0x0DA] = 0x20 + rom.banks[0x3E][0x3800 + 0x00C] = 0x20 + + # Set heart containers + rom.banks[0x3E][0x3800 + 0x106] = 0x89 + rom.banks[0x3E][0x3800 + 0x12B] = 0x89 + rom.banks[0x3E][0x3800 + 0x15A] = 0x89 + rom.banks[0x3E][0x3800 + 0x1FF] = 0x89 + rom.banks[0x3E][0x3800 + 0x185] = 0x89 + rom.banks[0x3E][0x3800 + 0x1BC] = 0x89 + rom.banks[0x3E][0x3800 + 0x2E8] = 0x89 + rom.banks[0x3E][0x3800 + 0x234] = 0x89 + + # Toadstool + rom.banks[0x3E][0x3800 + 0x050] = 0x50 + # Sword on beach + rom.banks[0x3E][0x3800 + 0x0F2] = 0x0B + # Sword upgrade + rom.banks[0x3E][0x3800 + 0x2E9] = 0x0B + + # Songs + rom.banks[0x3E][0x3800 + 0x092] = 0x8B # song 1 + rom.banks[0x3E][0x3800 + 0x0DC] = 0x8B # song 1 + rom.banks[0x3E][0x3800 + 0x2FD] = 0x8C # song 2 + rom.banks[0x3E][0x3800 + 0x2FB] = 0x8D # song 3 + + # Instruments + rom.banks[0x3E][0x3800 + 0x102] = 0x8E + rom.banks[0x3E][0x3800 + 0x12a] = 0x8F + rom.banks[0x3E][0x3800 + 0x159] = 0x90 + rom.banks[0x3E][0x3800 + 0x162] = 0x91 + rom.banks[0x3E][0x3800 + 0x182] = 0x92 + rom.banks[0x3E][0x3800 + 0x1b5] = 0x93 + rom.banks[0x3E][0x3800 + 0x22c] = 0x94 + rom.banks[0x3E][0x3800 + 0x230] = 0x95 + + # Start item + rom.banks[0x3E][0x3800 + 0x2a3] = 0x01 + + # Master stalfos overkill drops + rom.banks[0x3E][0x3800 + 0x195] = 0x1A + rom.banks[0x3E][0x3800 + 0x192] = 0x1A + rom.banks[0x3E][0x3800 + 0x184] = 0x1A diff --git a/worlds/ladx/LADXR/patches/dungeon.py b/worlds/ladx/LADXR/patches/dungeon.py new file mode 100644 index 000000000000..f99d99fe4991 --- /dev/null +++ b/worlds/ladx/LADXR/patches/dungeon.py @@ -0,0 +1,129 @@ +from ..roomEditor import RoomEditor, Object, ObjectHorizontal + + +KEY_DOORS = { + 0xEC: 0xF4, + 0xED: 0xF5, + 0xEE: 0xF6, + 0xEF: 0xF7, + 0xF8: 0xF4, +} + +def removeKeyDoors(rom): + for n in range(0x100, 0x316): + if n == 0x2FF: + continue + update = False + re = RoomEditor(rom, n) + for obj in re.objects: + if obj.type_id in KEY_DOORS: + obj.type_id = KEY_DOORS[obj.type_id] + update = True + if obj.type_id == 0xDE: # Keyblocks + obj.type_id = re.floor_object & 0x0F + update = True + if update: + re.store(rom) + + +def patchNoDungeons(rom): + def setMinimap(dungeon_nr, x, y, room): + for n in range(64): + if rom.banks[0x14][0x0220 + 64 * dungeon_nr + n] == room: + rom.banks[0x14][0x0220 + 64 * dungeon_nr + n] = 0xFF + rom.banks[0x14][0x0220 + 64 * dungeon_nr + x + y * 8] = room + #D1 + setMinimap(0, 3, 6, 0x06) + setMinimap(0, 3, 5, 0x02) + re = RoomEditor(rom, 0x117) + for n in range(1, 7): + re.removeObject(n, 0) + re.removeObject(0, n) + re.removeObject(9, n) + re.objects += [Object(4, 0, 0xf0)] + re.store(rom) + re = RoomEditor(rom, 0x11A) + re.getWarps()[0].room = 0x117 + re.store(rom) + re = RoomEditor(rom, 0x11B) + re.getWarps()[0].room = 0x117 + re.store(rom) + + #D2 + setMinimap(1, 2, 6, 0x2B) + setMinimap(1, 1, 6, 0x2A) + re = RoomEditor(rom, 0x136) + for n in range(1, 7): + re.removeObject(n, 0) + re.objects += [Object(4, 0, 0xf0)] + re.store(rom) + + #D3 + setMinimap(2, 1, 6, 0x5A) + setMinimap(2, 1, 5, 0x59) + re = RoomEditor(rom, 0x152) + for n in range(2, 7): + re.removeObject(9, n) + re.store(rom) + + #D4 + setMinimap(3, 3, 6, 0x66) + setMinimap(3, 3, 5, 0x62) + re = RoomEditor(rom, 0x17A) + for n in range(3, 7): + re.removeObject(n, 0) + re.objects += [Object(4, 0, 0xf0)] + re.store(rom) + + #D5 + setMinimap(4, 7, 6, 0x85) + setMinimap(4, 7, 5, 0x82) + re = RoomEditor(rom, 0x1A1) + for n in range(3, 8): + re.removeObject(n, 0) + re.removeObject(0, n) + for n in range(4, 6): + re.removeObject(n, 1) + re.removeObject(n, 2) + re.objects += [Object(4, 0, 0xf0)] + re.store(rom) + + #D6 + setMinimap(5, 3, 6, 0xBC) + setMinimap(5, 3, 5, 0xB5) + re = RoomEditor(rom, 0x1D4) + for n in range(2, 8): + re.removeObject(0, n) + re.removeObject(9, n) + re.objects += [Object(4, 0, 0xf0)] + re.store(rom) + + #D7 + setMinimap(6, 1, 6, 0x2E) + setMinimap(6, 1, 5, 0x2C) + re = RoomEditor(rom, 0x20E) + for n in range(1, 8): + re.removeObject(0, n) + re.removeObject(9, n) + re.objects += [Object(3, 0, 0x29), ObjectHorizontal(4, 0, 0x0D, 2), Object(6, 0, 0x2A)] + re.store(rom) + re = RoomEditor(rom, 0x22E) + re.objects = [Object(4, 0, 0xf0), Object(3, 7, 0x2B), ObjectHorizontal(4, 7, 0x0D, 2), Object(6, 7, 0x2C), Object(1, 0, 0xA8)] + re.getWarps() + re.floor_object = 13 + re.store(rom) + re = RoomEditor(rom, 0x22C) + re.removeObject(0, 7) + re.removeObject(2, 7) + re.objects.append(ObjectHorizontal(0, 7, 0x03, 3)) + re.store(rom) + + #D8 + setMinimap(7, 3, 6, 0x34) + setMinimap(7, 3, 5, 0x30) + re = RoomEditor(rom, 0x25D) + re.objects += [Object(3, 0, 0x25), Object(4, 0, 0xf0), Object(6, 0, 0x26)] + re.store(rom) + + #D0 + setMinimap(11, 2, 6, 0x00) + setMinimap(11, 3, 6, 0x01) diff --git a/worlds/ladx/LADXR/patches/endscreen.py b/worlds/ladx/LADXR/patches/endscreen.py new file mode 100644 index 000000000000..843120f1c060 --- /dev/null +++ b/worlds/ladx/LADXR/patches/endscreen.py @@ -0,0 +1,139 @@ +from ..assembler import ASM +import os + + +def updateEndScreen(rom): + # Call our custom data loader in bank 3F + rom.patch(0x00, 0x391D, ASM(""" + ld a, $20 + ld [$2100], a + jp $7de6 + """), ASM(""" + ld a, $3F + ld [$2100], a + jp $4200 + """)) + rom.patch(0x17, 0x2FCE, "B170", "D070") # Ignore the final tile data load + + rom.patch(0x3F, 0x0200, None, ASM(""" + ; Disable LCD + xor a + ldh [$40], a + + ld hl, $8000 + ld de, $5000 +copyLoop: + ld a, [de] + inc de + ldi [hl], a + bit 4, h + jr z, copyLoop + + ld a, $01 + ldh [$4F], a + + ld hl, $8000 + ld de, $6000 +copyLoop2: + ld a, [de] + inc de + ldi [hl], a + bit 4, h + jr z, copyLoop2 + + ld hl, $9800 + ld de, $0190 +clearLoop1: + xor a + ldi [hl], a + dec de + ld a, d + or e + jr nz, clearLoop1 + + ld de, $0190 +clearLoop2: + ld a, $08 + ldi [hl], a + dec de + ld a, d + or e + jr nz, clearLoop2 + + xor a + ldh [$4F], a + + + ld hl, $9800 + ld de, $000C + xor a +loadLoop1: + ldi [hl], a + ld b, a + ld a, l + and $1F + cp $14 + jr c, .noLineSkip + add hl, de +.noLineSkip: + ld a, b + inc a + jr nz, loadLoop1 + +loadLoop2: + ldi [hl], a + ld b, a + ld a, l + and $1F + cp $14 + jr c, .noLineSkip + add hl, de +.noLineSkip: + ld a, b + inc a + jr nz, loadLoop2 + + ; Load palette + ld hl, $DC10 + ld a, $00 + ldi [hl], a + ld a, $00 + ldi [hl], a + + ld a, $ad + ldi [hl], a + ld a, $35 + ldi [hl], a + + ld a, $94 + ldi [hl], a + ld a, $52 + ldi [hl], a + + ld a, $FF + ldi [hl], a + ld a, $7F + ldi [hl], a + + ld a, $00 + ld [$DDD3], a + ld a, $04 + ld [$DDD4], a + ld a, $81 + ld [$DDD1], a + + ; Enable LCD + ld a, $91 + ldh [$40], a + ld [$d6fd], a + + xor a + ldh [$96], a + ldh [$97], a + ret + """)) + + addr = 0x1000 + for c in open(os.path.join(os.path.dirname(__file__), "nyan.bin"), "rb").read(): + rom.banks[0x3F][addr] = c + addr += 1 diff --git a/worlds/ladx/LADXR/patches/enemies.py b/worlds/ladx/LADXR/patches/enemies.py new file mode 100644 index 000000000000..f5e1df131356 --- /dev/null +++ b/worlds/ladx/LADXR/patches/enemies.py @@ -0,0 +1,462 @@ +from ..roomEditor import RoomEditor, Object, ObjectWarp, ObjectHorizontal +from ..assembler import ASM +from ..locations import constants +from typing import List + + +# Room containing the boss +BOSS_ROOMS = [ + 0x106, + 0x12b, + 0x15a, + 0x166, + 0x185, + 0x1bc, + 0x223, # Note: unused room normally + 0x234, + 0x300, +] +BOSS_ENTITIES = [ + (3, 2, 0x59), + (4, 2, 0x5C), + (4, 3, 0x5B), + None, + (4, 3, 0x5D), + (4, 3, 0x5A), + None, + (4, 3, 0x62), + (5, 2, 0xF9), +] +MINIBOSS_ENTITIES = { + "ROLLING_BONES": [(8, 3, 0x81), (6, 3, 0x82)], + "HINOX": [(5, 2, 0x89)], + "DODONGO": [(3, 2, 0x60), (5, 2, 0x60)], + "CUE_BALL": [(1, 1, 0x8e)], + "GHOMA": [(2, 1, 0x5e), (2, 4, 0x5e)], + "SMASHER": [(5, 2, 0x92)], + "GRIM_CREEPER": [(4, 0, 0xbc)], + "BLAINO": [(5, 3, 0xbe)], + "AVALAUNCH": [(5, 1, 0xf4)], + "GIANT_BUZZ_BLOB": [(4, 2, 0xf8)], + "MOBLIN_KING": [(5, 5, 0xe4)], + "ARMOS_KNIGHT": [(4, 3, 0x88)], +} +MINIBOSS_ROOMS = { + 0: 0x111, 1: 0x128, 2: 0x145, 3: 0x164, 4: 0x193, 5: 0x1C5, 6: 0x228, 7: 0x23F, + "c1": 0x30C, "c2": 0x303, + "moblin_cave": 0x2E1, + "armos_temple": 0x27F, +} + + +def fixArmosKnightAsMiniboss(rom): + # Make the armos temple room with armos knight drop a ceiling key on kill. + # This makes the door always open, but that's fine. + rom.patch(0x14, 0x017F, "21", "81") + + # Do not change the drop from Armos knight into a ceiling key. + rom.patch(0x06, 0x12E8, ASM("ld [hl], $30"), "", fill_nop=True) + + +def getBossRoomStatusFlagLocation(dungeon_nr): + if BOSS_ROOMS[dungeon_nr] >= 0x300: + return 0xDDE0 - 0x300 + BOSS_ROOMS[dungeon_nr] + return 0xD800 + BOSS_ROOMS[dungeon_nr] + + +def fixDungeonItem(item_chest_id, dungeon_nr): + if item_chest_id == constants.CHEST_ITEMS[constants.MAP]: + return constants.CHEST_ITEMS["MAP%d" % (dungeon_nr + 1)] + if item_chest_id == constants.CHEST_ITEMS[constants.COMPASS]: + return constants.CHEST_ITEMS["COMPASS%d" % (dungeon_nr + 1)] + if item_chest_id == constants.CHEST_ITEMS[constants.KEY]: + return constants.CHEST_ITEMS["KEY%d" % (dungeon_nr + 1)] + if item_chest_id == constants.CHEST_ITEMS[constants.NIGHTMARE_KEY]: + return constants.CHEST_ITEMS["NIGHTMARE_KEY%d" % (dungeon_nr + 1)] + if item_chest_id == constants.CHEST_ITEMS[constants.STONE_BEAK]: + return constants.CHEST_ITEMS["STONE_BEAK%d" % (dungeon_nr + 1)] + return item_chest_id + + +def getCleanBossRoom(rom, dungeon_nr): + re = RoomEditor(rom, BOSS_ROOMS[dungeon_nr]) + new_objects = [] + for obj in re.objects: + if isinstance(obj, ObjectWarp): + continue + if obj.type_id == 0xBE: # Remove staircases + continue + if obj.type_id == 0x06: # Remove lava + continue + if obj.type_id == 0x1c: # Change D1 pits into normal pits + obj.type_id = 0x01 + if obj.type_id == 0x1e: # Change D1 pits into normal pits + obj.type_id = 0xaf + if obj.type_id == 0x1f: # Change D1 pits into normal pits + obj.type_id = 0xb0 + if obj.type_id == 0xF5: # Change open doors into closing doors. + obj.type_id = 0xF1 + new_objects.append(obj) + + + # Make D4 room a valid fighting room by removing most content. + if dungeon_nr == 3: + new_objects = new_objects[:2] + [Object(1, 1, 0xAC), Object(8, 1, 0xAC), Object(1, 6, 0xAC), Object(8, 6, 0xAC)] + + # D7 has an empty room we use for most bosses, but it needs some adjustments. + if dungeon_nr == 6: + # Move around the unused and instrument room. + rom.banks[0x14][0x03a0 + 6 + 1 * 8] = 0x00 + rom.banks[0x14][0x03a0 + 7 + 2 * 8] = 0x2C + rom.banks[0x14][0x03a0 + 7 + 3 * 8] = 0x23 + rom.banks[0x14][0x03a0 + 6 + 5 * 8] = 0x00 + + rom.banks[0x14][0x0520 + 7 + 2 * 8] = 0x2C + rom.banks[0x14][0x0520 + 7 + 3 * 8] = 0x23 + rom.banks[0x14][0x0520 + 6 + 5 * 8] = 0x00 + + re.floor_object &= 0x0F + new_objects += [ + Object(4, 0, 0xF0), + Object(1, 6, 0xBE), + ObjectWarp(1, dungeon_nr, 0x22E, 24, 16) + ] + + # Set the stairs towards the eagle tower top to our new room. + r = RoomEditor(rom, 0x22E) + r.objects[-1] = ObjectWarp(1, dungeon_nr, re.room, 24, 112) + r.store(rom) + + # Remove the normal door to the instrument room + r = RoomEditor(rom, 0x22e) + r.removeObject(4, 0) + r.store(rom) + rom.banks[0x14][0x22e - 0x100] = 0x00 + + r = RoomEditor(rom, 0x22c) + r.changeObject(0, 7, 0x03) + r.changeObject(2, 7, 0x03) + r.store(rom) + + re.objects = new_objects + re.entities = [] + return re + + +def changeBosses(rom, mapping: List[int]): + # Fix the color dungeon not properly warping to room 0 with the boss. + for addr in range(0x04E0, 0x04E0 + 64): + if rom.banks[0x14][addr] == 0x00 and addr not in {0x04E0 + 1 + 3 * 8, 0x04E0 + 2 + 6 * 8}: + rom.banks[0x14][addr] = 0xFF + # Fix the genie death not really liking pits/water. + rom.patch(0x04, 0x0521, ASM("ld [hl], $81"), ASM("ld [hl], $91")) + + # For the sidescroll bosses, we need to update this check to be the evil eagle dungeon. + # But if evil eagle is not there we still need to remove this check to make angler fish work in D7 + dungeon_nr = mapping.index(6) if 6 in mapping else 0xFE + rom.patch(0x02, 0x1FC8, ASM("cp $06"), ASM("cp $%02x" % (dungeon_nr if dungeon_nr < 8 else 0xff))) + + for dungeon_nr in range(9): + target = mapping[dungeon_nr] + if target == dungeon_nr: + continue + + if target == 3: # D4 fish boss + # If dungeon_nr == 6: use normal eagle door towards fish. + if dungeon_nr == 6: + # Add the staircase to the boss, and fix the warp back. + re = RoomEditor(rom, 0x22E) + for obj in re.objects: + if isinstance(obj, ObjectWarp): + obj.type_id = 2 + obj.map_nr = 3 + obj.room = 0x1EF + obj.target_x = 24 + obj.target_y = 16 + re.store(rom) + re = RoomEditor(rom, 0x1EF) + re.objects[-1] = ObjectWarp(1, dungeon_nr if dungeon_nr < 8 else 0xff, 0x22E, 24, 16) + re.store(rom) + else: + # Set the proper room event flags + rom.banks[0x14][BOSS_ROOMS[dungeon_nr] - 0x100] = 0x2A + + # Add the staircase to the boss, and fix the warp back. + re = getCleanBossRoom(rom, dungeon_nr) + re.objects += [Object(4, 4, 0xBE), ObjectWarp(2, 3, 0x1EF, 24, 16)] + re.store(rom) + re = RoomEditor(rom, 0x1EF) + re.objects[-1] = ObjectWarp(1, dungeon_nr if dungeon_nr < 8 else 0xff, BOSS_ROOMS[dungeon_nr], 72, 80) + re.store(rom) + + # Patch the fish heart container to open up the right room. + if dungeon_nr == 6: + rom.patch(0x03, 0x1A0F, ASM("ld hl, $D966"), ASM("ld hl, $%04x" % (0xD800 + 0x22E))) + else: + rom.patch(0x03, 0x1A0F, ASM("ld hl, $D966"), ASM("ld hl, $%04x" % (getBossRoomStatusFlagLocation(dungeon_nr)))) + + # Patch the proper item towards the D4 boss + rom.banks[0x3E][0x3800 + 0x01ff] = fixDungeonItem(rom.banks[0x3E][0x3800 + BOSS_ROOMS[dungeon_nr]], dungeon_nr) + rom.banks[0x3E][0x3300 + 0x01ff] = fixDungeonItem(rom.banks[0x3E][0x3300 + BOSS_ROOMS[dungeon_nr]], dungeon_nr) + elif target == 6: # Evil eagle + rom.banks[0x14][BOSS_ROOMS[dungeon_nr] - 0x100] = 0x2A + + # Patch the eagle heart container to open up the right room. + rom.patch(0x03, 0x1A04, ASM("ld hl, $DA2E"), ASM("ld hl, $%04x" % (getBossRoomStatusFlagLocation(dungeon_nr)))) + + # Add the staircase to the boss, and fix the warp back. + re = getCleanBossRoom(rom, dungeon_nr) + re.objects += [Object(4, 4, 0xBE), ObjectWarp(2, 6, 0x2F8, 72, 80)] + re.store(rom) + re = RoomEditor(rom, 0x2F8) + re.objects[-1] = ObjectWarp(1, dungeon_nr if dungeon_nr < 8 else 0xff, BOSS_ROOMS[dungeon_nr], 72, 80) + re.store(rom) + + # Patch the proper item towards the D7 boss + rom.banks[0x3E][0x3800 + 0x02E8] = fixDungeonItem(rom.banks[0x3E][0x3800 + BOSS_ROOMS[dungeon_nr]], dungeon_nr) + rom.banks[0x3E][0x3300 + 0x02E8] = fixDungeonItem(rom.banks[0x3E][0x3300 + BOSS_ROOMS[dungeon_nr]], dungeon_nr) + else: + rom.banks[0x14][BOSS_ROOMS[dungeon_nr] - 0x100] = 0x21 + re = getCleanBossRoom(rom, dungeon_nr) + re.entities = [BOSS_ENTITIES[target]] + + if target == 4: + # For slime eel, we need to setup the right wall tiles. + rom.banks[0x20][0x2EB3 + BOSS_ROOMS[dungeon_nr] - 0x100] = 0x06 + if target == 5: + # Patch facade so he doesn't use the spinning tiles, which is a problem for the sprites. + rom.patch(0x04, 0x121D, ASM("cp $14"), ASM("cp $00")) + rom.patch(0x04, 0x1226, ASM("cp $04"), ASM("cp $00")) + rom.patch(0x04, 0x127F, ASM("cp $14"), ASM("cp $00")) + if target == 7: + pass + # For hot head, add some lava (causes graphical glitches) + # re.animation_id = 0x06 + # re.objects += [ + # ObjectHorizontal(3, 2, 0x06, 4), + # ObjectHorizontal(2, 3, 0x06, 6), + # ObjectHorizontal(2, 4, 0x06, 6), + # ObjectHorizontal(3, 5, 0x06, 4), + # ] + + re.store(rom) + + +def readBossMapping(rom): + mapping = [] + for dungeon_nr in range(9): + r = RoomEditor(rom, BOSS_ROOMS[dungeon_nr]) + if r.entities: + mapping.append(BOSS_ENTITIES.index(r.entities[0])) + elif isinstance(r.objects[-1], ObjectWarp) and r.objects[-1].room == 0x1ef: + mapping.append(3) + elif isinstance(r.objects[-1], ObjectWarp) and r.objects[-1].room == 0x2f8: + mapping.append(6) + else: + mapping.append(dungeon_nr) + return mapping + + +def changeMiniBosses(rom, mapping): + # Fix avalaunch not working when entering a room from the left or right + rom.patch(0x03, 0x0BE0, ASM(""" + ld [hl], $50 + ld hl, $C2D0 + add hl, bc + ld [hl], $00 + jp $4B56 + """), ASM(""" + ld a, [hl] + sub $08 + ld [hl], a + ld hl, $C2D0 + add hl, bc + ld [hl], b ; b is always zero here + ret + """), fill_nop=True) + # Fix avalaunch waiting until the room event is done (and not all rooms have a room event on enter) + rom.patch(0x36, 0x1C14, ASM("ret z"), "", fill_nop=True) + # Fix giant buzz blob waiting until the room event is done (and not all rooms have a room event on enter) + rom.patch(0x36, 0x153B, ASM("ret z"), "", fill_nop=True) + + # Remove the powder fairy from giant buzz blob + rom.patch(0x36, 0x14F7, ASM("jr nz, $05"), ASM("jr $05")) + + # Do not allow the force barrier in D3 dodongo room + rom.patch(0x14, 0x14AC, 0x14B5, ASM("jp $7FE0"), fill_nop=True) + rom.patch(0x14, 0x3FE0, "00" * 0x20, ASM(""" + ld a, [$C124] ; room transition + ld hl, $C17B + or [hl] + ret nz + ldh a, [$F6] ; room + cp $45 ; check for D3 dodogo room + ret z + cp $7F ; check for armos temple room + ret z + jp $54B5 + """), fill_nop=True) + + # Patch smasher to spawn the ball closer, so it doesn't spawn on the wall in the armos temple + rom.patch(0x06, 0x0533, ASM("add a, $30"), ASM("add a, $20")) + + for target, name in mapping.items(): + re = RoomEditor(rom, MINIBOSS_ROOMS[target]) + re.entities = [e for e in re.entities if e[2] == 0x61] # Only keep warp, if available + re.entities += MINIBOSS_ENTITIES[name] + + if re.room == 0x228 and name != "GRIM_CREEPER": + for x in range(3, 7): + for y in range(0, 3): + re.removeObject(x, y) + + if name == "CUE_BALL": + re.objects += [ + Object(3, 3, 0x2c), + ObjectHorizontal(4, 3, 0x22, 2), + Object(6, 3, 0x2b), + Object(3, 4, 0x2a), + ObjectHorizontal(4, 4, 0x21, 2), + Object(6, 4, 0x29), + ] + if name == "BLAINO": + # BLAINO needs a warp object to hit you to the entrance of the dungeon. + if len(re.getWarps()) < 1: + # Default to start house. + target = (0x10, 0x2A3, 0x50, 0x7c) + if 0x100 <= re.room < 0x11D: #D1 + target = (0, 0x117, 80, 80) + elif 0x11D <= re.room < 0x140: #D2 + target = (1, 0x136, 80, 80) + elif 0x140 <= re.room < 0x15D: #D3 + target = (2, 0x152, 80, 80) + elif 0x15D <= re.room < 0x180: #D4 + target = (3, 0x174, 80, 80) + elif 0x180 <= re.room < 0x1AC: #D5 + target = (4, 0x1A1, 80, 80) + elif 0x1B0 <= re.room < 0x1DE: #D6 + target = (5, 0x1D4, 80, 80) + elif 0x200 <= re.room < 0x22D: #D7 + target = (6, 0x20E, 80, 80) + elif 0x22D <= re.room < 0x26C: #D8 + target = (7, 0x25D, 80, 80) + elif re.room >= 0x300: #D0 + target = (0xFF, 0x312, 80, 80) + elif re.room == 0x2E1: #Moblin cave + target = (0x15, 0x2F0, 0x50, 0x7C) + elif re.room == 0x27F: #Armos temple + target = (0x16, 0x28F, 0x50, 0x7C) + re.objects.append(ObjectWarp(1, *target)) + if name == "DODONGO": + # Remove breaking floor tiles from the room. + re.objects = [obj for obj in re.objects if obj.type_id != 0xDF] + if name == "ROLLING_BONES" and target == 2: + # Make rolling bones pass trough walls so it does not get stuck here. + rom.patch(0x03, 0x02F1 + 0x81, "84", "95") + re.store(rom) + + +def readMiniBossMapping(rom): + mapping = {} + for key, room in MINIBOSS_ROOMS.items(): + r = RoomEditor(rom, room) + for me_key, me_data in MINIBOSS_ENTITIES.items(): + if me_data[-1][2] == r.entities[-1][2]: + mapping[key] = me_key + return mapping + + +def doubleTrouble(rom): + for n in range(0x316): + if n == 0x2FF: + continue + re = RoomEditor(rom, n) + # Bosses + if re.hasEntity(0x59): # Moldorm (TODO; double heart container drop) + re.removeEntities(0x59) + re.entities += [(3, 2, 0x59), (4, 2, 0x59)] + re.store(rom) + if re.hasEntity(0x5C): # Ghini + re.removeEntities(0x5C) + re.entities += [(3, 2, 0x5C), (4, 2, 0x5C)] + re.store(rom) + if re.hasEntity(0x5B): # slime eye + re.removeEntities(0x5B) + re.entities += [(3, 2, 0x5B), (6, 2, 0x5B)] + re.store(rom) + if re.hasEntity(0x65): # angler fish + re.removeEntities(0x65) + re.entities += [(6, 2, 0x65), (6, 5, 0x65)] + re.store(rom) + # Slime eel bugs out on death if duplicated. + # if re.hasEntity(0x5D): # slime eel + # re.removeEntities(0x5D) + # re.entities += [(6, 2, 0x5D), (6, 5, 0x5D)] + # re.store(rom) + if re.hasEntity(0x5A): # facade (TODO: Drops two hearts, shared health?) + re.removeEntities(0x5A) + re.entities += [(2, 3, 0x5A), (6, 3, 0x5A)] + re.store(rom) + # Evil eagle causes a crash, and messes up the intro sequence and generally is just a mess if I spawn multiple + # if re.hasEntity(0x63): # evil eagle + # re.removeEntities(0x63) + # re.entities += [(3, 4, 0x63), (2, 4, 0x63)] + # re.store(rom) + # # Remove that links movement is blocked + # rom.patch(0x05, 0x2258, ASM("ldh [$A1], a"), "0000") + # rom.patch(0x05, 0x1AE3, ASM("ldh [$A1], a"), "0000") + # rom.patch(0x05, 0x1C5D, ASM("ldh [$A1], a"), "0000") + # rom.patch(0x05, 0x1C8D, ASM("ldh [$A1], a"), "0000") + # rom.patch(0x05, 0x1CAF, ASM("ldh [$A1], a"), "0000") + if re.hasEntity(0x62): # hot head (TODO: Drops thwo hearts) + re.removeEntities(0x62) + re.entities += [(2, 2, 0x62), (4, 4, 0x62)] + re.store(rom) + if re.hasEntity(0xF9): # hardhit beetle + re.removeEntities(0xF9) + re.entities += [(2, 2, 0xF9), (5, 4, 0xF9)] + re.store(rom) + # Minibosses + if re.hasEntity(0x89): + re.removeEntities(0x89) + re.entities += [(2, 3, 0x89), (6, 3, 0x89)] + re.store(rom) + if re.hasEntity(0x81): + re.removeEntities(0x81) + re.entities += [(2, 3, 0x81), (6, 3, 0x81)] + re.store(rom) + if re.hasEntity(0x60): + dodongo = [e for e in re.entities if e[2] == 0x60] + x = (dodongo[0][0] + dodongo[1][0]) // 2 + y = (dodongo[0][1] + dodongo[1][1]) // 2 + re.entities += [(x, y, 0x60)] + re.store(rom) + if re.hasEntity(0x8e): + re.removeEntities(0x8e) + re.entities += [(1, 1, 0x8e), (7, 1, 0x8e)] + re.store(rom) + if re.hasEntity(0x92): + re.removeEntities(0x92) + re.entities += [(2, 3, 0x92), (4, 3, 0x92)] + re.store(rom) + if re.hasEntity(0xf4): + re.removeEntities(0xf4) + re.entities += [(2, 1, 0xf4), (6, 1, 0xf4)] + re.store(rom) + if re.hasEntity(0xf8): + re.removeEntities(0xf8) + re.entities += [(2, 2, 0xf8), (6, 2, 0xf8)] + re.store(rom) + if re.hasEntity(0xe4): + re.removeEntities(0xe4) + re.entities += [(5, 2, 0xe4), (5, 5, 0xe4)] + re.store(rom) + + if re.hasEntity(0x88): # Armos knight (TODO: double item drop) + re.removeEntities(0x88) + re.entities += [(3, 3, 0x88), (6, 3, 0x88)] + re.store(rom) + if re.hasEntity(0x87): # Lanmola (TODO: killing one drops the item, and marks as done) + re.removeEntities(0x87) + re.entities += [(2, 2, 0x87), (1, 1, 0x87)] + re.store(rom) diff --git a/worlds/ladx/LADXR/patches/entrances.py b/worlds/ladx/LADXR/patches/entrances.py new file mode 100644 index 000000000000..82a09edf5813 --- /dev/null +++ b/worlds/ladx/LADXR/patches/entrances.py @@ -0,0 +1,58 @@ +from ..roomEditor import RoomEditor, ObjectWarp +from ..worldSetup import ENTRANCE_INFO + + +def changeEntrances(rom, mapping): + warp_to_indoor = {} + warp_to_outdoor = {} + for key in mapping.keys(): + info = ENTRANCE_INFO[key] + re = RoomEditor(rom, info.alt_room if info.alt_room is not None else info.room) + warp = re.getWarps()[info.index if info.index not in (None, "all") else 0] + warp_to_indoor[key] = warp + assert info.target == warp.room, "%s != %03x" % (key, warp.room) + + re = RoomEditor(rom, warp.room) + for warp in re.getWarps(): + if warp.room == info.room: + warp_to_outdoor[key] = warp + assert key in warp_to_outdoor, "Missing warp to outdoor on %s" % (key) + + # First collect all the changes we need to do per room + changes_per_room = {} + def addChange(source_room, target_room, new_warp): + if source_room not in changes_per_room: + changes_per_room[source_room] = {} + changes_per_room[source_room][target_room] = new_warp + for key, target in mapping.items(): + if key == target: + continue + info = ENTRANCE_INFO[key] + # Change the entrance to point to the new indoor room + addChange(info.room, warp_to_indoor[key].room, warp_to_indoor[target]) + if info.alt_room: + addChange(info.alt_room, warp_to_indoor[key].room, warp_to_indoor[target]) + + # Change the exit to point to the right outside + addChange(warp_to_indoor[target].room, ENTRANCE_INFO[target].room, warp_to_outdoor[key]) + if ENTRANCE_INFO[target].instrument_room is not None: + addChange(ENTRANCE_INFO[target].instrument_room, ENTRANCE_INFO[target].room, warp_to_outdoor[key]) + + # Finally apply the changes, we need to do this once per room to prevent A->B->C issues. + for room, changes in changes_per_room.items(): + re = RoomEditor(rom, room) + for idx, obj in enumerate(re.objects): + if isinstance(obj, ObjectWarp) and obj.room in changes: + re.objects[idx] = changes[obj.room].copy() + re.store(rom) + + +def readEntrances(rom): + result = {} + for key, info in ENTRANCE_INFO.items(): + re = RoomEditor(rom, info.alt_room if info.alt_room is not None else info.room) + warp = re.getWarps()[info.index if info.index not in (None, "all") else 0] + for other_key, other_info in ENTRANCE_INFO.items(): + if warp.room == other_info.target: + result[key] = other_key + return result diff --git a/worlds/ladx/LADXR/patches/fishingMinigame.py b/worlds/ladx/LADXR/patches/fishingMinigame.py new file mode 100644 index 000000000000..a0c079a6e2f0 --- /dev/null +++ b/worlds/ladx/LADXR/patches/fishingMinigame.py @@ -0,0 +1,19 @@ +from ..assembler import ASM +from ..roomEditor import RoomEditor + + +def updateFinishingMinigame(rom): + rom.patch(0x04, 0x26BE, 0x26DF, ASM(""" + ld a, $0E ; GiveItemAndMessageForRoomMultiworld + rst 8 + + ; Mark selection as stopping minigame, as we are not asking a question. + ld a, $01 + ld [$C177], a + + ; Check if we got rupees from the item skip getting rupees from the fish. + ld a, [$DB90] + ld hl, $DB8F + or [hl] + jp nz, $66FE + """), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/goal.py b/worlds/ladx/LADXR/patches/goal.py new file mode 100644 index 000000000000..cb932aa1d93e --- /dev/null +++ b/worlds/ladx/LADXR/patches/goal.py @@ -0,0 +1,317 @@ +from ..assembler import ASM +from ..roomEditor import RoomEditor, Object, ObjectVertical, ObjectHorizontal, ObjectWarp +from ..utils import formatText + + +def setRequiredInstrumentCount(rom, count): + rom.texts[0x1A3] = formatText("You need %d instruments" % (count)) + if count >= 8: + return + if count < 0: + rom.patch(0x00, 0x31f5, ASM("ld a, [$D806]\nand $10\njr z, $25"), ASM(""), fill_nop=True) + rom.patch(0x20, 0x2dea, ASM("ld a, [$D806]\nand $10\njr z, $29"), ASM(""), fill_nop=True) + count = 0 + + # TODO: Music bugs out at the end, unless you have all instruments. + rom.patch(0x19, 0x0B79, None, "0000") # always spawn all instruments, we need the last one as that handles opening the egg. + rom.patch(0x19, 0x0BF4, ASM("jp $3BC0"), ASM("jp $7FE0")) # instead of rendering the instrument, jump to the code below. + rom.patch(0x19, 0x0BFE, ASM(""" + ; Normal check fo all instruments + ld e, $08 + ld hl, $DB65 + loop: + ldi a, [hl] + and $02 + jr z, $12 + dec e + jr nz, loop + """), ASM(""" + jp $7F2B ; jump to the end of the bank, where there is some space for code. + """), fill_nop=True) + # Add some code at the end of the bank, as we do not have enough space to do this "in place" + rom.patch(0x19, 0x3F2B, "0000000000000000000000000000000000000000000000000000", ASM(""" + ld d, $00 + ld e, $08 + ld hl, $DB65 ; start of has instrument memory +loop: + ld a, [hl] + and $02 + jr z, noinc + inc d +noinc: + inc hl + dec e + jr nz, loop + ld a, d + cp $%02x ; check if we have a minimal of this amount of instruments. + jp c, $4C1A ; not enough instruments + jp $4C0B ; enough instruments + """ % (count)), fill_nop=True) + rom.patch(0x19, 0x3FE0, "0000000000000000000000000000000000000000000000000000", ASM(""" + ; Entry point of render code + ld hl, $DB65 ; table of having instruments + push bc + ldh a, [$F1] + ld c, a + add hl, bc + pop bc + ld a, [hl] + and $02 ; check if we have this instrument + ret z + jp $3BC0 ; jump to render code + """), fill_nop=True) + + +def setSeashellGoal(rom, count): + rom.texts[0x1A3] = formatText("You need %d {SEASHELL}s" % (count)) + + # Remove the seashell mansion handler (as it will take your seashells) but put a heartpiece instead + re = RoomEditor(rom, 0x2E9) + re.entities = [(4, 4, 0x35)] + re.store(rom) + + rom.patch(0x19, 0x0ACB, 0x0C21, ASM(""" + ldh a, [$F8] ; room status + and $10 + ret nz + ldh a, [$F0] ; active entity state + rst 0 + dw state0, state1, state2, state3, state4 + +state0: + ld a, [$C124] ; room transition state + and a + ret nz + ldh a, [$99] ; link position Y + cp $70 + ret nc + jp $3B12 ; increase entity state + +state1: + call $0C05 ; get entity transition countdown + jr nz, renderShells + ld [hl], $10 + call renderShells + + ld hl, $C2B0 ; private state 1 table + add hl, bc + ld a, [wSeashellsCount] + cp [hl] + jp z, $3B12 ; increase entity state + ld a, [hl] ; increase the amount of compared shells + inc a + daa + ld [hl], a + ld hl, $C2C0 ; private state 2 table + add hl, bc + inc [hl] ; increase amount of displayed shells + ld a, $2B + ldh [$F4], a ; SFX + ret + +state2: + ld a, [wSeashellsCount] + cp $%02d + jr c, renderShells + ; got enough shells + call $3B12 ; increase entity state + call $0C05 ; get entity transition countdown + ld [hl], $40 + jp renderShells + +state3: + ld a, $23 + ldh [$F2], a ; SFX: Dungeon opened + ld hl, $D806 ; egg room status + set 4, [hl] + ld a, [hl] + ldh [$F8], a ; current room status + call $3B12 ; increase entity state + + ld a, $00 + jp $4C2E + +state4: + ret + +renderShells: + ld hl, $C2C0 ; private state 2 table + add hl, bc + ld a, [hl] + cp $14 + jr c, .noMax + ld a, $14 +.noMax: + and a + ret z + ld c, a + ld hl, spriteRect + call $3CE6 ; RenderActiveEntitySpritesRect + ret + +spriteRect: + db $10, $1E, $1E, $0C + db $10, $2A, $1E, $0C + db $10, $36, $1E, $0C + db $10, $42, $1E, $0C + db $10, $4E, $1E, $0C + + db $10, $5A, $1E, $0C + db $10, $66, $1E, $0C + db $10, $72, $1E, $0C + db $10, $7E, $1E, $0C + db $10, $8A, $1E, $0C + + db $24, $1E, $1E, $0C + db $24, $2A, $1E, $0C + db $24, $36, $1E, $0C + db $24, $42, $1E, $0C + db $24, $4E, $1E, $0C + + db $24, $5A, $1E, $0C + db $24, $66, $1E, $0C + db $24, $72, $1E, $0C + db $24, $7E, $1E, $0C + db $24, $8A, $1E, $0C + """ % (count), 0x4ACB), fill_nop=True) + + +def setRaftGoal(rom): + rom.texts[0x1A3] = formatText("Just sail away.") + + # Remove the egg and egg event handler. + re = RoomEditor(rom, 0x006) + for x in range(4, 7): + for y in range(0, 4): + re.removeObject(x, y) + re.objects.append(ObjectHorizontal(4, 1, 0x4d, 3)) + re.objects.append(ObjectHorizontal(4, 2, 0x03, 3)) + re.objects.append(ObjectHorizontal(4, 3, 0x03, 3)) + re.entities = [] + re.updateOverlay() + re.store(rom) + + re = RoomEditor(rom, 0x08D) + re.objects[6].count = 4 + re.objects[7].x += 2 + re.objects[7].type_id = 0x2B + re.objects[8].x += 2 + re.objects[8].count = 2 + re.objects[9].x += 1 + re.objects[11] = ObjectVertical(7, 5, 0x37, 2) + re.objects[12].x -= 1 + re.objects[13].x -= 1 + re.objects[14].x -= 1 + re.objects[14].type_id = 0x34 + re.objects[17].x += 3 + re.objects[17].count -= 3 + re.updateOverlay() + re.overlay[7 + 60] = 0x33 + re.store(rom) + + re = RoomEditor(rom, 0x0E9) + re.objects[30].count = 1 + re.objects[30].x += 2 + re.overlay[7 + 70] = 0x0E + re.overlay[8 + 70] = 0x0E + re.store(rom) + re = RoomEditor(rom, 0x0F9) + re.objects = [ + ObjectHorizontal(4, 0, 0x0E, 6), + ObjectVertical(9, 0, 0xCA, 8), + ObjectVertical(8, 0, 0x0E, 8), + + Object(3, 0, 0x38), + Object(3, 1, 0x32), + ObjectHorizontal(4, 1, 0x2C, 3), + Object(7, 1, 0x2D), + ObjectVertical(7, 2, 0x38, 5), + Object(7, 7, 0x34), + ObjectHorizontal(0, 7, 0x2F, 7), + + ObjectVertical(2, 3, 0xE8, 4), + ObjectVertical(3, 2, 0xE8, 5), + ObjectVertical(4, 2, 0xE8, 2), + + ObjectVertical(4, 4, 0x5C, 3), + ObjectVertical(5, 2, 0x5C, 5), + ObjectVertical(6, 2, 0x5C, 5), + + Object(6, 4, 0xC6), + ObjectWarp(1, 0x1F, 0xF6, 136, 112) + ] + re.updateOverlay(True) + re.entities.append((0, 0, 0x41)) + re.store(rom) + re = RoomEditor(rom, 0x1F6) + re.objects[-1].target_x -= 16 + re.store(rom) + + # Fix the raft graphics (this overrides some unused graphic tiles) + rom.banks[0x31][0x21C0:0x2200] = rom.banks[0x2E][0x07C0:0x0800] + + # Patch the owl entity to run our custom end handling. + rom.patch(0x06, 0x27F5, 0x2A77, ASM(""" + ld a, [$DB95] + cp $0B + ret nz + ; If map is not fully loaded, return + ld a, [$C124] + and a + ret nz + ; Check if we are moving off the bottom of the map + ldh a, [$99] + cp $7D + ret c + ; Move link back so it does not move off the map + ld a, $7D + ldh [$99], a + + xor a + ld e, a + ld d, a + +raftSearchLoop: + ld hl, $C280 + add hl, de + ld a, [hl] + and a + jr z, .skipEntity + + ld hl, $C3A0 + add hl, de + ld a, [hl] + cp $6A + jr nz, .skipEntity + + ; Raft found, check if near the bottom of the screen. + ld hl, $C210 + add hl, de + ld a, [hl] + cp $70 + jr nc, raftOffWorld + +.skipEntity: + inc e + ld a, e + cp $10 + jr nz, raftSearchLoop + ret + +raftOffWorld: + ; Switch to the end credits + ld a, $01 + ld [$DB95], a + ld a, $00 + ld [$DB96], a + ret + """), fill_nop=True) + + # We need to run quickly trough part of the credits, or else it bugs out + # Skip the whole windfish part. + rom.patch(0x17, 0x0D39, None, ASM("ld a, $18\nld [$D00E], a\nret")) + # And skip the zoomed out laying on the log + rom.patch(0x17, 0x20ED, None, ASM("ld a, $00")) + # Finally skip some waking up on the log. + rom.patch(0x17, 0x23BC, None, ASM("jp $4CD9")) + rom.patch(0x17, 0x2476, None, ASM("jp $4CD9")) diff --git a/worlds/ladx/LADXR/patches/goldenLeaf.py b/worlds/ladx/LADXR/patches/goldenLeaf.py new file mode 100644 index 000000000000..87cefae0f6d8 --- /dev/null +++ b/worlds/ladx/LADXR/patches/goldenLeaf.py @@ -0,0 +1,34 @@ +from ..assembler import ASM + + +def fixGoldenLeaf(rom): + # Patch the golden leaf code so it jumps to the dropped key handling in bank 3E + rom.patch(3, 0x2007, ASM(""" + ld de, $5FFB + call $3C77 ; RenderActiveEntitySprite + """), ASM(""" + ld a, $04 + rst 8 + """), fill_nop=True) + rom.patch(3, 0x2018, None, ASM(""" + ld a, $06 ; giveItemMultiworld + rst 8 + jp $602F + """)) + rom.patch(3, 0x2037, None, ASM(""" + ld a, $0a ; showMessageMultiworld + rst 8 + jp $604B + """)) + + # Patch all over the place to move the golden leafs to a different memory location. + # We use $DB6D (dungeon 9 status), but we could also use $DB7A (which is only used by the ghost) + rom.patch(0x00, 0x2D17, ASM("ld a, [$DB15]"), ASM("ld a, $06"), fill_nop=True) # Always load the slime tiles + rom.patch(0x02, 0x3005, ASM("cp $06"), ASM("cp $01"), fill_nop=True) # Instead of checking for 6 leaves a the keyhole, just check for the key + rom.patch(0x20, 0x1AD1, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # For the status screen, load the number of leafs from the proper memory + rom.patch(0x03, 0x0980, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # If leaves >= 6 move richard + rom.patch(0x06, 0x0059, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # If leaves >= 6 move richard + rom.patch(0x06, 0x007D, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # Richard message if no leaves + rom.patch(0x06, 0x00B8, ASM("ld [$DB15], a"), ASM("ld [wGoldenLeaves], a")) # Stores FF in the leaf counter if we opened the path + # 6:40EE uses leaves == 6 to check if we have collected the key, but only to change the message. + # rom.patch(0x06, 0x2AEF, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # Telephone message handler diff --git a/worlds/ladx/LADXR/patches/hardMode.py b/worlds/ladx/LADXR/patches/hardMode.py new file mode 100644 index 000000000000..3ecceda9198e --- /dev/null +++ b/worlds/ladx/LADXR/patches/hardMode.py @@ -0,0 +1,64 @@ +from ..assembler import ASM + + +def oracleMode(rom): + # Reduce iframes + rom.patch(0x03, 0x2DB2, ASM("ld a, $50"), ASM("ld a, $20")) + + # Make bomb explosions damage you. + rom.patch(0x03, 0x2618, ASM(""" + ld hl, $C440 + add hl, bc + ld a, [hl] + and a + jr nz, $05 + """), ASM(""" + call $6625 + """), fill_nop=True) + # Reduce bomb blast push back on link + rom.patch(0x03, 0x2643, ASM("sla [hl]"), ASM("sra [hl]"), fill_nop=True) + rom.patch(0x03, 0x2648, ASM("sla [hl]"), ASM("sra [hl]"), fill_nop=True) + + # Never spawn a piece of power or acorn + rom.patch(0x03, 0x1608, ASM("jr nz, $05"), ASM("jr $05")) + rom.patch(0x03, 0x1642, ASM("jr nz, $04"), ASM("jr $04")) + + # Let hearts only recover half a container instead of a full one. + rom.patch(0x03, 0x24B7, ASM("ld a, $08"), ASM("ld a, $04")) + # Don't randomly drop fairies from enemies, drop a rupee instead + rom.patch(0x03, 0x15C7, "2E2D382F2E2D3837", "2E2D382E2E2D3837") + + # Make dropping in water without flippers damage you. + rom.patch(0x02, 0x3722, ASM("ldh a, [$AF]"), ASM("ld a, $06")) + + +def heroMode(rom): + # Don't randomly drop fairies and hearts from enemies, drop a rupee instead + rom.patch(0x03, 0x159D, + "2E2E2D2D372DFFFF2F37382E2F2F", + "2E2EFFFF37FFFFFFFF37382EFFFF") + rom.patch(0x03, 0x15C7, + "2E2D382F2E2D3837", + "2E2E382E2E2E3837") + rom.patch(0x00, 0x168F, ASM("ld a, $2D"), "", fill_nop=True) + rom.patch(0x02, 0x0CDB, ASM("ld a, $2D"), "", fill_nop=True) + # Double damage + rom.patch(0x03, 0x2DAB, + ASM("ld a, [$DB94]\nadd a, e\nld [$DB94], a"), + ASM("ld hl, $DB94\nld a, [hl]\nadd a, e\nadd a, e\nld [hl], a")) + rom.patch(0x02, 0x11B2, ASM("add a, $04"), ASM("add a, $08")) + rom.patch(0x02, 0x127E, ASM("add a, $04"), ASM("add a, $08")) + rom.patch(0x02, 0x291C, ASM("add a, $04"), ASM("add a, $08")) + rom.patch(0x02, 0x362B, ASM("add a, $04"), ASM("add a, $08")) + rom.patch(0x06, 0x041C, ASM("ld a, $02"), ASM("ld a, $04")) + rom.patch(0x15, 0x09B8, ASM("add a, $08"), ASM("add a, $10")) + rom.patch(0x15, 0x32FD, ASM("ld a, $08"), ASM("ld a, $10")) + rom.patch(0x18, 0x370E, ASM("ld a, $08"), ASM("ld a, $10")) + rom.patch(0x07, 0x3103, ASM("ld a, $08"), ASM("ld a, $10")) + rom.patch(0x06, 0x1166, ASM("ld a, $08"), ASM("ld a, $10")) + + + + +def oneHitKO(rom): + rom.patch(0x02, 0x238C, ASM("ld [$DB94], a"), "", fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/health.py b/worlds/ladx/LADXR/patches/health.py new file mode 100644 index 000000000000..7488e6280ad0 --- /dev/null +++ b/worlds/ladx/LADXR/patches/health.py @@ -0,0 +1,33 @@ +from ..assembler import ASM +from ..utils import formatText + + +def setStartHealth(rom, amount): + rom.patch(0x01, 0x0B1C, ASM("ld [hl], $03"), ASM("ld [hl], $%02X" % (amount))) # max health of new save + rom.patch(0x01, 0x0B14, ASM("ld [hl], $18"), ASM("ld [hl], $%02X" % (amount * 8))) # current health of new save + + +def upgradeHealthContainers(rom): + # Reuse 2 unused shop messages for the heart containers. + rom.texts[0x2A] = formatText("You found a {HEART_CONTAINER}!") + rom.texts[0x2B] = formatText("You lost a heart!") + + rom.patch(0x03, 0x19DC, ASM(""" + ld de, $59D8 + call $3BC0 + """), ASM(""" + ld a, $05 ; renderHeartPiece + rst 8 + """), fill_nop=True) + rom.patch(0x03, 0x19F0, ASM(""" + ld hl, $DB5B + inc [hl] + ld hl, $DB93 + ld [hl], $FF + """), ASM(""" + ld a, $06 ; giveItemMultiworld + rst 8 + ld a, $0A ; messageForItemMultiworld + rst 8 +skip: + """), fill_nop=True) # add heart->remove heart on heart container diff --git a/worlds/ladx/LADXR/patches/heartPiece.py b/worlds/ladx/LADXR/patches/heartPiece.py new file mode 100644 index 000000000000..4147c8fe9547 --- /dev/null +++ b/worlds/ladx/LADXR/patches/heartPiece.py @@ -0,0 +1,42 @@ +from ..assembler import ASM + + +def fixHeartPiece(rom): + # Patch all locations where the piece of heart is rendered. + rom.patch(0x03, 0x1b52, ASM("ld de, $5A4D\ncall $3BC0"), ASM("ld a, $04\nrst 8"), fill_nop=True) # state 0 + + # Write custom code in the first state handler, this overwrites all state handlers + # Till state 5. + rom.patch(0x03, 0x1A74, 0x1A98, ASM(""" + ; Render sprite + ld a, $05 + rst 8 + + ; Handle item effect + ld a, $06 ; giveItemMultiworld + rst 8 + + ;Show message + ld a, $0A ; showMessageMultiworld + rst 8 + + ; Switch to state 5 + ld hl, $C290; stateTable + add hl, bc + ld [hl], $05 + ret + """), fill_nop=True) + # Insert a state 5 handler + rom.patch(0x03, 0x1A98, 0x1B17, ASM(""" + ; Render sprite + ld a, $05 + rst 8 + + ld a, [$C19F] ; dialog state + and a + ret nz + + call $512A ; mark room as done + call $3F8D ; unload entity + ret + """), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/instrument.py b/worlds/ladx/LADXR/patches/instrument.py new file mode 100644 index 000000000000..9e1cfecc4058 --- /dev/null +++ b/worlds/ladx/LADXR/patches/instrument.py @@ -0,0 +1,24 @@ +from ..assembler import ASM + + +def fixInstruments(rom): + rom.patch(0x03, 0x1EA9, 0x1EAE, "", fill_nop=True) + rom.patch(0x03, 0x1EB9, 0x1EC8, ASM(""" + ; Render sprite + ld a, $05 + rst 8 + """), fill_nop=True) + + # Patch the message and instrument giving code + rom.patch(0x03, 0x1EE3, 0x1EF6, ASM(""" + ; Handle item effect + ld a, $06 ; giveItemMultiworld + rst 8 + + ;Show message + ld a, $0A ; showMessageMultiworld + rst 8 + """), fill_nop=True) + + # Color cycle palette 7 instead of 1 + rom.patch(0x36, 0x30F0, ASM("ld de, $DC5C"), ASM("ld de, $DC84")) diff --git a/worlds/ladx/LADXR/patches/inventory.py b/worlds/ladx/LADXR/patches/inventory.py new file mode 100644 index 000000000000..c3ca96e01b06 --- /dev/null +++ b/worlds/ladx/LADXR/patches/inventory.py @@ -0,0 +1,421 @@ +from ..assembler import ASM +from ..backgroundEditor import BackgroundEditor + + +def selectToSwitchSongs(rom): + # Do not ignore left/right keys when ocarina is selected + rom.patch(0x20, 0x1F18, ASM("and a"), ASM("xor a")) + # Change the keys which switch the ocarina song to select and no key. + rom.patch(0x20, 0x21A9, ASM("and $01"), ASM("and $40")) + rom.patch(0x20, 0x21C7, ASM("and $02"), ASM("and $00")) + +def songSelectAfterOcarinaSelect(rom): + rom.patch(0x20, 0x2002, ASM("ld [$DB00], a"), ASM("call $5F96")) + rom.patch(0x20, 0x1FE0, ASM("ld [$DB01], a"), ASM("call $5F9B")) + # Remove the code that opens the ocerina on cursor movement, but use it to insert code + # for opening the menu on item select + rom.patch(0x20, 0x1F93, 0x1FB2, ASM(""" + jp $5FB2 + itemToB: + ld [$DB00], a + jr checkForOcarina + itemToA: + ld [$DB01], a + checkForOcarina: + cp $09 + jp nz, $6010 + ld a, [$DB49] + and a + ret z + ld a, $08 + ldh [$90], a ; load ocarina song select graphics + ;ld a, $10 + ;ld [$C1B8], a ; shows the opening animation + ld a, $01 + ld [$C1B5], a + ret + """), fill_nop=True) + # More code that opens the menu, use this to close the menu + rom.patch(0x20, 0x200D, 0x2027, ASM(""" + jp $6027 + closeOcarinaMenu: + ld a, [$C1B5] + and a + ret z + xor a + ld [$C1B5], a + ld a, $10 + ld [$C1B9], a ; shows the closing animation + ret + """), fill_nop=True) + rom.patch(0x20, 0x2027, 0x2036, "", fill_nop=True) # Code that closes the ocarina menu on item select + + rom.patch(0x20, 0x22A2, ASM(""" + ld a, [$C159] + inc a + ld [$C159], a + and $10 + jr nz, $30 + """), ASM(""" + ld a, [$C1B5] + and a + ret nz + ldh a, [$E7] ; frame counter + and $10 + ret nz + """), fill_nop=True) + +def moreSlots(rom): + #Move flippers, medicine, trade item and seashells to DB3E+ + rom.patch(0x02, 0x292B, ASM("ld a, [$DB0C]"), ASM("ld a, [$DB3E]")) + #rom.patch(0x02, 0x2E8F, ASM("ld a, [$DB0C]"), ASM("ld a, [$DB3E]")) + rom.patch(0x02, 0x3713, ASM("ld a, [$DB0C]"), ASM("ld a, [$DB3E]")) + rom.patch(0x20, 0x1A23, ASM("ld de, $DB0C"), ASM("ld de, $DB3E")) + rom.patch(0x02, 0x23a3, ASM("ld a, [$DB0D]"), ASM("ld a, [$DB3F]")) + rom.patch(0x02, 0x23d7, ASM("ld a, [$DB0D]"), ASM("ld a, [$DB3F]")) + rom.patch(0x02, 0x23aa, ASM("ld [$DB0D], a"), ASM("ld [$DB3F], a")) + rom.patch(0x04, 0x3b1f, ASM("ld [$DB0D], a"), ASM("ld [$DB3F], a")) + rom.patch(0x06, 0x1f58, ASM("ld a, [$DB0D]"), ASM("ld a, [$DB3F]")) + rom.patch(0x06, 0x1ff5, ASM("ld hl, $DB0D"), ASM("ld hl, $DB3F")) + rom.patch(0x07, 0x3c33, ASM("ld [$DB0D], a"), ASM("ld [$DB3F], a")) + rom.patch(0x00, 0x1e01, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x00, 0x2d21, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x00, 0x3199, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x03, 0x0ae6, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x03, 0x0b6d, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x03, 0x0f68, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x04, 0x2faa, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x04, 0x3502, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x04, 0x3624, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x05, 0x0bff, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x05, 0x0d20, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x05, 0x0db1, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x05, 0x0dd5, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x05, 0x0e8e, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x05, 0x11ce, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x06, 0x1a2c, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x06, 0x1a7c, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x06, 0x1ab1, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x06, 0x2214, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x06, 0x223e, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x02f8, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x04bf, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x057f, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x07, 0x0797, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0856, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x07, 0x0a21, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0a33, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0a58, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0a81, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0acf, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0af9, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0b31, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x07, 0x0bcc, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0c23, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0c3c, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0c60, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x07, 0x0d73, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x07, 0x1549, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x155d, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x159f, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x18e6, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x19ce, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + #rom.patch(0x15, 0x3F23, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0966, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0972, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x18, 0x09f3, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0bf1, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0c2c, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0c6d, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x18, 0x0c8b, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0ce4, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x18, 0x0d3c, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0d4a, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0d95, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0da3, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0de4, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x18, 0x0e7a, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0e91, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0eb6, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x18, 0x219e, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x19, 0x05ec, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x19, 0x2d54, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x19, 0x2df2, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x19, 0x2ef1, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x19, 0x2f95, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x20, 0x1b04, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x20, 0x1e42, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x36, 0x0948, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x19, 0x31Ca, ASM("ld a, [$DB0F]"), ASM("ld a, [$DB41]")) + rom.patch(0x19, 0x3215, ASM("ld a, [$DB0F]"), ASM("ld a, [$DB41]")) + rom.patch(0x19, 0x32a2, ASM("ld a, [$DB0F]"), ASM("ld a, [$DB41]")) + rom.patch(0x19, 0x3700, ASM("ld [$DB0F], a"), ASM("ld [$DB41], a")) + rom.patch(0x19, 0x38b3, ASM("ld a, [$DB0F]"), ASM("ld a, [$DB41]")) + rom.patch(0x19, 0x38c3, ASM("ld [$DB0F], a"), ASM("ld [$DB41], a")) + rom.patch(0x20, 0x1a83, ASM("ld a, [$DB0F]"), ASM("ld a, [$DB41]")) + + # Fix the whole inventory rendering, this needs to extend a few tables with more entries so it moves tables + # to the end of the bank as well. + rom.patch(0x20, 0x3E53, "00" * 32, + "9C019C06" + "9C619C65" + "9CA19CA5" + "9CE19CE5" + "9D219D25" + "9D619D65" + "9DA19DA5" + "9DE19DE5") # New table with tile addresses for all slots + rom.patch(0x20, 0x1CC7, ASM("ld hl, $5C84"), ASM("ld hl, $7E53")) # use the new table + rom.patch(0x20, 0x1BCC, ASM("ld hl, $5C84"), ASM("ld hl, $7E53")) # use the new table + rom.patch(0x20, 0x1CF0, ASM("ld hl, $5C84"), ASM("ld hl, $7E53")) # use the new table + + # sprite positions for inventory cursor, new table, placed at the end of the bank + rom.patch(0x20, 0x3E90, "00" * 16, "28283838484858586868787888889898") + rom.patch(0x20, 0x22b3, ASM("ld hl, $6298"), ASM("ld hl, $7E90")) + rom.patch(0x20, 0x2298, "28284040", "08280828") # Extend the sprite X positions for the inventory table + + # Piece of power overlay positions + rom.patch(0x20, 0x233A, + "1038103010301030103010300E0E2626", + "10381030103010301030103010301030") + rom.patch(0x20, 0x3E73, "00" * 16, + "0E0E2626363646465656666676768686") + rom.patch(0x20, 0x2377, ASM("ld hl, $6346"), ASM("ld hl, $7E73")) + + # Allow selecting the 4 extra slots. + rom.patch(0x20, 0x1F33, ASM("ld a, $09"), ASM("ld a, $0D")) + rom.patch(0x20, 0x1F54, ASM("ld a, $09"), ASM("ld a, $0D")) + rom.patch(0x20, 0x1F2A, ASM("cp $0A"), ASM("cp $0E")) + rom.patch(0x20, 0x1F4B, ASM("cp $0A"), ASM("cp $0E")) + rom.patch(0x02, 0x217E, ASM("ld a, $0B"), ASM("ld a, $0F")) + + # Patch all the locations that iterate over inventory to check the extra slots + rom.patch(0x02, 0x33FC, ASM("cp $0C"), ASM("cp $10")) + rom.patch(0x03, 0x2475, ASM("ld e, $0C"), ASM("ld e, $10")) + rom.patch(0x03, 0x248a, ASM("cp $0C"), ASM("cp $10")) + rom.patch(0x04, 0x3849, ASM("ld c, $0B"), ASM("ld c, $0F")) + rom.patch(0x04, 0x3862, ASM("ld c, $0B"), ASM("ld c, $0F")) + rom.patch(0x04, 0x39C2, ASM("ld d, $0C"), ASM("ld d, $10")) + rom.patch(0x04, 0x39E0, ASM("ld d, $0C"), ASM("ld d, $10")) + rom.patch(0x04, 0x39FE, ASM("ld d, $0C"), ASM("ld d, $10")) + rom.patch(0x05, 0x0F95, ASM("ld e, $0B"), ASM("ld e, $0F")) + rom.patch(0x05, 0x0FD1, ASM("ld c, $0B"), ASM("ld c, $0F")) + rom.patch(0x05, 0x1324, ASM("ld e, $0C"), ASM("ld e, $10")) + rom.patch(0x05, 0x1339, ASM("cp $0C"), ASM("cp $10")) + rom.patch(0x18, 0x005A, ASM("ld e, $0B"), ASM("ld e, $0F")) + rom.patch(0x18, 0x0571, ASM("ld e, $0B"), ASM("ld e, $0F")) + rom.patch(0x19, 0x0703, ASM("cp $0C"), ASM("cp $10")) + rom.patch(0x20, 0x235C, ASM("ld d, $0C"), ASM("ld d, $10")) + rom.patch(0x36, 0x31B8, ASM("ld e, $0C"), ASM("ld e, $10")) + + ## Patch the toadstool as a different item + rom.patch(0x20, 0x1C84, "9C019C" "069C61", "4C7F7F" "4D7F7F") # Which tiles are used for the toadstool + rom.patch(0x20, 0x1C8A, "9C659C" "C19CC5", "90927F" "91937F") # Which tiles are used for the rooster + rom.patch(0x20, 0x1C6C, "927F7F" "937F7F", "127F7F" "137F7F") # Which tiles are used for the feather (to make space for rooster) + rom.patch(0x20, 0x1C66, "907F7F" "917F7F", "107F7F" "117F7F") # Which tiles are used for the ocarina (to make space for rooster) + + # Move the inventory tile numbers to a higher address, so there is space for the table above it. + rom.banks[0x20][0x1C34:0x1C94] = rom.banks[0x20][0x1C30:0x1C90] + rom.patch(0x20, 0x1CDB, ASM("ld hl, $5C30"), ASM("ld hl, $5C34")) + rom.patch(0x20, 0x1D0D, ASM("ld hl, $5C33"), ASM("ld hl, $5C37")) + rom.patch(0x20, 0x1C30, "7F7F", "0A0B") # Toadstool tile attributes + rom.patch(0x20, 0x1C32, "7F7F", "0101") # Rooster tile attributes + rom.patch(0x20, 0x1C28, "0303", "0B0B") # Feather tile attributes (due to rooster) + rom.patch(0x20, 0x1C26, "0202", "0A0A") # Ocarina tile attributes (due to rooster) + + # Allow usage of the toadstool (replace the whole manual jump table with an rst 0 jumptable + rom.patch(0x00, 0x129D, 0x12D8, ASM(""" + rst 0 ; jump table + dw $12ED ; no item + dw $1528 ; Sword + dw $135A ; Bomb + dw $1382 ; Bracelet + dw $12EE ; Shield + dw $13BD ; Bow + dw $1319 ; Hookshot + dw $12D8 ; Magic rod + dw $12ED ; Boots (no action) + dw $41FC ; Ocarina + dw $14CB ; Feather + dw $12F8 ; Shovel + dw $148D ; Magic powder + dw $1383 ; Boomerang + dw $1498 ; Toadstool + dw RoosterUse ; Rooster +RoosterUse: + ld a, $01 + ld [$DB7B], a ; has rooster + call $3958 ; spawn followers + xor a + ld [$DB7B], a ; has rooster + ret + """, 0x129D), fill_nop=True) + # Fix the graphics of the toadstool hold over your head + rom.patch(0x02, 0x121E, ASM("ld e, $8E"), ASM("ld e, $4C")) + rom.patch(0x02, 0x1241, ASM("ld a, $14"), ASM("ld a, $1C")) + + # Do not remove powder when it is used up. + rom.patch(0x20, 0x0C59, ASM("jr nz, $12"), ASM("jr $12")) + + # Patch the toadstool entity code to give the proper item, and not set the has-toadstool flag. + rom.patch(0x03, 0x1D6F, ASM(""" + ld a, $0A + ldh [$A5], a + ld d, $0C + call $6472 + ld a, $01 + ld [$DB4B], a + """), ASM(""" + ld d, $0E + call $6472 + """), fill_nop=True) + + # Patch the debug save game so it does not give a bunch of swords + rom.patch(0x01, 0x0673, "01010100", "0D0E0F00") + + # Patch the witch to use the new toadstool instead of the old flag + rom.patch(0x05, 0x081A, ASM("ld a, [$DB4B]"), ASM("ld a, $01"), fill_nop=True) + rom.patch(0x05, 0x082A, ASM("cp $0C"), ASM("cp $0E")) + rom.patch(0x05, 0x083E, ASM("cp $0C"), ASM("cp $0E")) + + +def advancedInventorySubscreen(rom): + # Instrument positions + rom.patch(0x01, 0x2BCF, + "0F51B1EFECAA4A0C", + "090C0F12494C4F52") + + be = BackgroundEditor(rom, 2) + be.tiles[0x9DA9] = 0x4A + be.tiles[0x9DC9] = 0x4B + for x in range(1, 10): + be.tiles[0x9DE9 + x] = 0xB0 + (x % 9) + be.tiles[0x9DE9] = 0xBA + be.store(rom) + be = BackgroundEditor(rom, 2, attributes=True) + + # Remove all attributes out of range. + for y in range(0x9C00, 0x9E40, 0x20): + for x in range(0x14, 0x20): + del be.tiles[x + y] + for n in range(0x9E40, 0xA020): + del be.tiles[n] + + # Remove palette of instruments + for y in range(0x9D00, 0x9E20, 0x20): + for x in range(0x00, 0x14): + be.tiles[x + y] = 0x01 + # And place it at the proper location + for y in range(0x9D00, 0x9D80, 0x20): + for x in range(0x09, 0x14): + be.tiles[x + y] = 0x07 + + # Key from 2nd vram bank + be.tiles[0x9DA9] = 0x09 + be.tiles[0x9DC9] = 0x09 + # Nightmare heads from 2nd vram bank with proper palette + for n in range(1, 10): + be.tiles[0x9DA9 + n] = 0x0E + + be.store(rom) + + rom.patch(0x20, 0x19D3, ASM("ld bc, $5994\nld e, $33"), ASM("ld bc, $7E08\nld e, $%02x" % (0x33 + 24))) + rom.banks[0x20][0x3E08:0x3E08+0x33] = rom.banks[0x20][0x1994:0x1994+0x33] + rom.patch(0x20, 0x3E08+0x32, "00" * 25, "9DAA08464646464646464646" "9DCA08B0B0B0B0B0B0B0B0B0" "00") + + # instead of doing an GBC specific check, jump to our custom handling + rom.patch(0x20, 0x19DE, ASM("ldh a, [$FE]\nand a\njr z, $40"), ASM("call $7F00"), fill_nop=True) + + rom.patch(0x20, 0x3F00, "00" * 0x100, ASM(""" + ld a, [$DBA5] ; isIndoor + and a + jr z, RenderKeysCounts + ldh a, [$F7] ; mapNr + cp $FF + jr z, RenderDungeonFix + cp $06 + jr z, D7RenderDungeonFix + cp $08 + jr c, RenderDungeonFix + +RenderKeysCounts: + ; Check if we have each nightmare key, and else null out the rendered tile + ld hl, $D636 + ld de, $DB19 + ld c, $08 +NKeyLoop: + ld a, [de] + and a + jr nz, .hasNKey + ld a, $7F + ld [hl], a +.hasNKey: + inc hl + inc de + inc de + inc de + inc de + inc de + dec c + jr nz, NKeyLoop + + ld a, [$DDDD] + and a + jr nz, .hasCNKey + ld a, $7F + ld [hl], a +.hasCNKey: + + ; Check the small key count for each dungeon and increase the tile to match the number + ld hl, $D642 + ld de, $DB1A + ld c, $08 +KeyLoop: + ld a, [de] + add a, $B0 + ld [hl], a + inc hl + inc de + inc de + inc de + inc de + inc de + dec c + jr nz, KeyLoop + + ld a, [$DDDE] + add a, $B0 + ld [hl], a + ret + +D7RenderDungeonFix: + ld de, D7DungeonFix + ld c, $11 + jr RenderDungeonFixGo + +RenderDungeonFix: + ld de, DungeonFix + ld c, $0D +RenderDungeonFixGo: + ld hl, $D633 +.copyLoop: + ld a, [de] + inc de + ldi [hl], a + dec c + jr nz, .copyLoop + ret + +DungeonFix: + db $9D, $09, $C7, $7F + db $9D, $0A, $C7, $7F + db $9D, $13, $C3, $7F + db $00 +D7DungeonFix: + db $9D, $09, $C7, $7F + db $9D, $0A, $C7, $7F + db $9D, $6B, $48, $7F + db $9D, $0F, $C7, $7F + db $00 + + """, 0x7F00), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/madBatter.py b/worlds/ladx/LADXR/patches/madBatter.py new file mode 100644 index 000000000000..601c5ac51e58 --- /dev/null +++ b/worlds/ladx/LADXR/patches/madBatter.py @@ -0,0 +1,42 @@ +from ..assembler import ASM +from ..utils import formatText + + +def upgradeMadBatter(rom): + # Normally the madbatter won't do anything if you have full capacity. Remove that check. + rom.patch(0x18, 0x0F05, 0x0F1D, "", fill_nop=True) + # Remove the code that finds which upgrade to apply, + rom.patch(0x18, 0x0F9E, 0x0FC4, "", fill_nop=True) + rom.patch(0x18, 0x0FD2, 0x0FD8, "", fill_nop=True) + + # Finally, at the last step, give the item and the item message. + rom.patch(0x18, 0x1016, 0x101B, "", fill_nop=True) + rom.patch(0x18, 0x101E, 0x1051, ASM(""" + ; Mad batter rooms are E0,E1 and E2, load the item type from a table in the rom + ; which only has 3 entries, and store it where bank 3E wants it. + ldh a, [$F6] ; current room + and $0F + ld d, $00 + ld e, a + ld hl, $4F90 + add hl, de + ld a, [hl] + ldh [$F1], a + + ; Give item + ld a, $06 ; giveItemMultiworld + rst 8 + ; Message + ld a, $0A ; showMessageMultiworld + rst 8 + ; Force the dialog at the bottom + ld a, [$C19F] + or $80 + ld [$C19F], a + """), fill_nop=True) + # Setup the default items + rom.patch(0x18, 0x0F90, "406060", "848586") + + rom.texts[0xE2] = formatText("You can now carry more Magic Powder!") + rom.texts[0xE3] = formatText("You can now carry more Bombs!") + rom.texts[0xE4] = formatText("You can now carry more Arrows!") diff --git a/worlds/ladx/LADXR/patches/maptweaks.py b/worlds/ladx/LADXR/patches/maptweaks.py new file mode 100644 index 000000000000..c25dd83dcada --- /dev/null +++ b/worlds/ladx/LADXR/patches/maptweaks.py @@ -0,0 +1,27 @@ +from ..roomEditor import RoomEditor, ObjectWarp, ObjectVertical + + +def tweakMap(rom): + # 5 holes at the castle, reduces to 3 + re = RoomEditor(rom, 0x078) + re.objects[-1].count = 3 + re.overlay[7 + 6 * 10] = re.overlay[9 + 6 * 10] + re.overlay[8 + 6 * 10] = re.overlay[9 + 6 * 10] + re.store(rom) + + +def addBetaRoom(rom): + re = RoomEditor(rom, 0x1FC) + re.objects[-1].target_y -= 0x10 + re.store(rom) + re = RoomEditor(rom, 0x038) + re.changeObject(5, 1, 0xE1) + re.removeObject(0, 0) + re.removeObject(0, 1) + re.removeObject(0, 2) + re.removeObject(6, 1) + re.objects.append(ObjectVertical(0, 0, 0x38, 3)) + re.objects.append(ObjectWarp(1, 0x1F, 0x1FC, 0x50, 0x7C)) + re.store(rom) + + rom.room_sprite_data_indoor[0x0FC] = rom.room_sprite_data_indoor[0x1A1] diff --git a/worlds/ladx/LADXR/patches/multiworld.py b/worlds/ladx/LADXR/patches/multiworld.py new file mode 100644 index 000000000000..e41dacf35b68 --- /dev/null +++ b/worlds/ladx/LADXR/patches/multiworld.py @@ -0,0 +1,308 @@ +from ..assembler import ASM +from ..roomEditor import RoomEditor, ObjectHorizontal, ObjectVertical, Object +from .. import entityData + + +def addMultiworldShop(rom, this_player, player_count): + # Make a copy of the shop into GrandpaUlrira house + re = RoomEditor(rom, 0x2A9) + re.objects = [ + ObjectHorizontal(1,1, 0x00, 8), + ObjectHorizontal(1,2, 0x00, 8), + ObjectHorizontal(1,3, 0xCD, 8), + Object(2, 0, 0xC7), + Object(7, 0, 0xC7), + Object(7, 7, 0xFD), + ] + re.getWarps() + re.entities = [(0, 6, 0xD4)] + for n in range(player_count): + if n != this_player: + re.entities.append((n + 1, 6, 0xD4)) + re.animation_id = 0x04 + re.floor_object = 0x0D + re.store(rom) + # Fix the tileset + rom.banks[0x20][0x2EB3 + 0x2A9 - 0x100] = rom.banks[0x20][0x2EB3 + 0x2A1 - 0x100] + + re = RoomEditor(rom, 0x0B1) + re.getWarps()[0].target_x = 128 + re.store(rom) + + # Load the shopkeeper sprites + entityData.SPRITE_DATA[0xD4] = entityData.SPRITE_DATA[0x4D] + rom.patch(0x03, 0x01CF, "00", "98") # Fix the hitbox of the ghost to be 16x16 + + # Patch Ghost to work as a multiworld shop + rom.patch(0x19, 0x1E18, 0x20B0, ASM(""" + ld a, $01 + ld [$C50A], a ; this stops link from using items + + ldh a, [$EE] ; X + cp $08 + ; Jump to other code which is placed on the old owl code. As we do not have enough space here. + jp z, shopItemsHandler + +;Draw shopkeeper + ld de, OwnerSpriteData + call $3BC0 ; render sprite pair + ldh a, [$E7] ; frame counter + swap a + and $01 + call $3B0C ; set sprite variant + + ldh a, [$F0] + and a + jr nz, checkTalkingResult + + call $7CA2 ; prevent link from moving into the sprite + call $7CF0 ; check if talking to NPC + call c, talkHandler ; talk handling + ret + +checkTalkingResult: + ld a, [$C19F] + and a + ret nz ; still taking + call $3B12 ; increase entity state + ld [hl], $00 + ld a, [$C177] ; dialog selection + and a + ret nz + jp TalkResultHandler + +OwnerSpriteData: + ;db $60, $03, $62, $03, $62, $23, $60, $23 ; down + db $64, $03, $66, $03, $66, $23, $64, $23 ; up + ;db $68, $03, $6A, $03, $6C, $03, $6E, $03 ; left + ;db $6A, $23, $68, $23, $6E, $23, $6C, $23 ; right + +shopItemsHandler: +; Render the shop items + ld h, $00 +loop: + ; First load links position to render the item at + ldh a, [$98] ; LinkX + ldh [$EE], a ; X + ldh a, [$99] ; LinkY + sub $0E + ldh [$EC], a ; Y + ; Check if this is the item we have picked up + ld a, [$C509] ; picked up item in shop + dec a + cp h + jr z, .renderCarry + + ld a, h + swap a + add a, $20 + ldh [$EE], a ; X + ld a, $30 + ldh [$EC], a ; Y +.renderCarry: + ld a, h + push hl + ldh [$F1], a ; variant + cp $03 + jr nc, .singleSprite + ld de, ItemsDualSpriteData + call $3BC0 ; render sprite pair + jr .renderDone +.singleSprite: + ld de, ItemsSingleSpriteData + call $3C77 ; render sprite +.renderDone: + + pop hl +.skipItem: + inc h + ld a, $07 + cp h + jr nz, loop + +; check if we want to pickup or drop an item + ldh a, [$CC] + and $30 ; A or B button + call nz, checkForPickup + +; check if we have an item + ld a, [$C509] ; carry item + and a + ret z + + ; Set that link has picked something up + ld a, $01 + ld [$C15C], a + call $0CAF ; reset spin attack... + + ; Check if we are trying to exit the shop and so drop our item. + ldh a, [$99] + cp $78 + ret c + xor a + ld [$C509], a + + ret + +checkForPickup: + ldh a, [$9E] ; direction + cp $02 + ret nz + ldh a, [$99] ; LinkY + cp $48 + ret nc + + ld a, $13 + ldh [$F2], a ; play SFX + + ld a, [$C509] ; picked up shop item + and a + jr nz, .drop + + ldh a, [$98] ; LinkX + sub $08 + swap a + and $07 + ld [$C509], a ; picked up shop item + ret +.drop: + xor a + ld [$C509], a + ret + +ItemsDualSpriteData: + db $60, $08, $60, $28 ; zol + db $68, $09 ; chicken (left) +ItemsSingleSpriteData: ; (first 3 entries are still dual sprites) + db $6A, $09 ; chicken (right) + db $14, $02, $14, $22 ; piece of power +;Real single sprite data starts here + db $00, $0F ; bomb + db $38, $0A ; rupees + db $20, $0C ; medicine + db $28, $0C ; heart + +;------------------------------------trying to buy something starts here +talkHandler: + ld a, [$C509] ; carry item + add a, a + ret z ; check if we have something to buy + sub $02 + + ld hl, itemNames + ld e, a + ld d, b ; b=0 + add hl, de + ld e, [hl] + inc hl + ld d, [hl] + + ld hl, wCustomMessage + call appendString + dec hl + call padString + ld de, postMessage + call appendString + dec hl + ld a, $fe + ld [hl], a + ld de, $FFEF + add hl, de + ldh a, [$EE] + swap a + and $0F + add a, $30 + ld [hl], a + ld a, $C9 + call $2385 ; open dialog + call $3B12 ; increase entity state + ret + +appendString: + ld a, [de] + inc de + and a + ret z + ldi [hl], a + jr appendString + +padString: + ld a, l + and $0F + ret z + ld a, $20 + ldi [hl], a + jr padString + +itemNames: + dw itemZol + dw itemChicken + dw itemPieceOfPower + dw itemBombs + dw itemRupees + dw itemMedicine + dw itemHealth + +postMessage: + db "For player X? Yes No ", $00 + +itemZol: + db m"Slime storm|100 {RUPEES}", $00 +itemChicken: + db m"Coccu party|50 {RUPEES}", $00 +itemPieceOfPower: + db m"Piece of Power|50 {RUPEES}", $00 +itemBombs: + db m"10 Bombs|50 {RUPEES}", $00 +itemRupees: + db m"100 {RUPEES}|200 {RUPEES}", $00 +itemMedicine: + db m"Medicine|100 {RUPEES}", $00 +itemHealth: + db m"Health refill|10 {RUPEES}", $00 + +TalkResultHandler: + ld hl, ItemPriceTableBCD + ld a, [$C509] + dec a + add a, a + ld c, a ; b=0 + add hl, bc + ldi a, [hl] + ld d, [hl] + ld e, a + ld a, [$DB5D] + cp d + ret c + jr nz, .highEnough + ld a, [$DB5E] + cp e + ret c +.highEnough: + ; Got enough money, take it. + ld hl, ItemPriceTableDEC + ld a, [$C509] + dec a + ld c, a ; b=0 + add hl, bc + ld a, [hl] + ld [$DB92], a ; set substract buffer + + ; Set the item to send + ld hl, $DDFE + ld a, [$C509] ; currently picked up item + ldi [hl], a + ldh a, [$EE] ; X position of NPC + ldi [hl], a + ld hl, $DDF7 + set 2, [hl] + + ; No longer picked up item + xor a + ld [$C509], a + ret + +ItemPriceTableBCD: + dw $0100, $0050, $0050, $0050, $0200, $0100, $0010 +ItemPriceTableDEC: + db $64, $32, $32, $32, $C8, $64, $0A + """, 0x5E18), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/music.py b/worlds/ladx/LADXR/patches/music.py new file mode 100644 index 000000000000..f7478a80c533 --- /dev/null +++ b/worlds/ladx/LADXR/patches/music.py @@ -0,0 +1,27 @@ +from ..assembler import ASM + + +_LOOPING_MUSIC = (1, 2, 3, 4, 5, 6, 7, 8, 9, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1C, 0x1D, 0x1F, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x2F, 0x31, 0x32, 0x33, 0x37, + 0x39, 0x3A, 0x3C, 0x3E, 0x40, 0x48, 0x49, 0x4A, 0x4B, 0x4E, 0x50, 0x53, 0x54, 0x55, 0x57, 0x58, 0x59, + 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61) + + +def randomizeMusic(rom, rnd): + # Randomize overworld + for x in range(0, 16, 2): + for y in range(0, 16, 2): + idx = x + y * 16 + result = rnd.choice(_LOOPING_MUSIC) + rom.banks[0x02][idx] = result + rom.banks[0x02][idx+1] = result + rom.banks[0x02][idx+16] = result + rom.banks[0x02][idx+17] = result + # Random music in dungeons/caves + for n in range(0x20): + rom.banks[0x02][0x100 + n] = rnd.choice(_LOOPING_MUSIC) + + +def noMusic(rom): + rom.patch(0x1B, 0x001E, ASM("ld hl, $D368\nldi a, [hl]"), ASM("xor a"), fill_nop=True) + rom.patch(0x1E, 0x001E, ASM("ld hl, $D368\nldi a, [hl]"), ASM("xor a"), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/nyan.bin b/worlds/ladx/LADXR/patches/nyan.bin new file mode 100644 index 0000000000000000000000000000000000000000..65f1772e7da1089ba3514b084e0e973df8e28e76 GIT binary patch literal 5760 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 F{{TG-AJG5+ literal 0 HcmV?d00001 diff --git a/worlds/ladx/LADXR/patches/overworld.py b/worlds/ladx/LADXR/patches/overworld.py new file mode 100644 index 000000000000..04668efca088 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld.py @@ -0,0 +1,224 @@ +from ..assembler import ASM +from ..roomEditor import RoomEditor, ObjectWarp, Object, WARP_TYPE_IDS +from .. import entityData +import os +import json + + +def patchOverworldTilesets(rom): + rom.patch(0x00, 0x0D5B, 0x0D79, ASM(""" + ; Instead of loading tileset info from a small 8x8 table, load it from a 16x16 table to give + ; full control. + ; A=MapRoom + ld hl, $2100 + ld [hl], $3F + ld d, $00 + ld e, a + ld hl, $7F00 + add hl, de + ldh a, [$94] ; We need to load the currently loaded tileset in E to compare it + ld e, a + ld a, [hl] + ld hl, $2100 + ld [hl], $20 + """), fill_nop=True) + # Remove the camera shop exception + rom.patch(0x00, 0x0D80, 0x0D8B, "", fill_nop=True) + + for x in range(16): + for y in range(16): + rom.banks[0x3F][0x3F00+x+y*16] = rom.banks[0x20][0x2E73 + (x // 2) + (y // 2) * 8] + rom.banks[0x3F][0x3F07] = rom.banks[0x3F][0x3F08] # Fix the room next to the egg + rom.banks[0x3F][0x3F17] = rom.banks[0x3F][0x3F08] # Fix the room next to the egg + rom.banks[0x3F][0x3F3A] = 0x0F # room below mambo cave + rom.banks[0x3F][0x3F3B] = 0x0F # room below D4 + rom.banks[0x3F][0x3F4B] = 0x0F # room next to castle + rom.banks[0x3F][0x3F5B] = 0x0F # room next to castle + # Fix the rooms around the camera shop + rom.banks[0x3F][0x3F26] = 0x0F + rom.banks[0x3F][0x3F27] = 0x0F + rom.banks[0x3F][0x3F36] = 0x0F + + +def createDungeonOnlyOverworld(rom): + # Skip the whole egg maze. + rom.patch(0x14, 0x0453, "75", "73") + + instrument_rooms = [0x102, 0x12A, 0x159, 0x162, 0x182, 0x1B5, 0x22C, 0x230, 0x301] + path = os.path.dirname(__file__) + + # Start with clearing all the maps, because this just generates a bunch of room in the rom. + for n in range(0x100): + re = RoomEditor(rom, n) + re.entities = [] + re.objects = [] + if os.path.exists("%s/overworld/dive/%02X.json" % (path, n)): + re.loadFromJson("%s/overworld/dive/%02X.json" % (path, n)) + entrances = list(filter(lambda obj: obj.type_id in WARP_TYPE_IDS, re.objects)) + for obj in re.objects: + if isinstance(obj, ObjectWarp) and entrances: + e = entrances.pop(0) + + other = RoomEditor(rom, obj.room) + for o in other.objects: + if isinstance(o, ObjectWarp) and o.warp_type == 0: + o.room = n + o.target_x = e.x * 16 + 8 + o.target_y = e.y * 16 + 16 + other.store(rom) + + if obj.room == 0x1F5: + # Patch the boomang guy exit + other = RoomEditor(rom, "Alt1F5") + other.getWarps()[0].room = n + other.getWarps()[0].target_x = e.x * 16 + 8 + other.getWarps()[0].target_y = e.y * 16 + 16 + other.store(rom) + + if obj.warp_type == 1 and (obj.map_nr < 8 or obj.map_nr == 0xFF) and obj.room not in (0x1B0, 0x23A, 0x23D): + other = RoomEditor(rom, instrument_rooms[min(8, obj.map_nr)]) + for o in other.objects: + if isinstance(o, ObjectWarp) and o.warp_type == 0: + o.room = n + o.target_x = e.x * 16 + 8 + o.target_y = e.y * 16 + 16 + other.store(rom) + re.store(rom) + + +def exportOverworld(rom): + import PIL.Image + + path = os.path.dirname(__file__) + for room_index in list(range(0x100)) + ["Alt06", "Alt0E", "Alt1B", "Alt2B", "Alt79", "Alt8C"]: + room = RoomEditor(rom, room_index) + if isinstance(room_index, int): + room_nr = room_index + else: + room_nr = int(room_index[3:], 16) + tileset_index = rom.banks[0x3F][0x3F00 + room_nr] + attributedata_bank = rom.banks[0x1A][0x2476 + room_nr] + attributedata_addr = rom.banks[0x1A][0x1E76 + room_nr * 2] + attributedata_addr |= rom.banks[0x1A][0x1E76 + room_nr * 2 + 1] << 8 + attributedata_addr -= 0x4000 + + metatile_info = rom.banks[0x1A][0x2B1D:0x2B1D + 0x400] + attrtile_info = rom.banks[attributedata_bank][attributedata_addr:attributedata_addr+0x400] + + palette_index = rom.banks[0x21][0x02EF + room_nr] + palette_addr = rom.banks[0x21][0x02B1 + palette_index * 2] + palette_addr |= rom.banks[0x21][0x02B1 + palette_index * 2 + 1] << 8 + palette_addr -= 0x4000 + + hidden_warp_tiles = [] + for obj in room.objects: + if obj.type_id in WARP_TYPE_IDS and room.overlay[obj.x + obj.y * 10] != obj.type_id: + if obj.type_id != 0xE1 or room.overlay[obj.x + obj.y * 10] != 0x53: # Ignore the waterfall 'caves' + hidden_warp_tiles.append(obj) + if obj.type_id == 0xC5 and room_nr < 0x100 and room.overlay[obj.x + obj.y * 10] == 0xC4: + # Pushable gravestones have the wrong overlay by default + room.overlay[obj.x + obj.y * 10] = 0xC5 + if obj.type_id == 0xDC and room_nr < 0x100: + # Flowers above the rooster windmill need a different tile + hidden_warp_tiles.append(obj) + + image_filename = "tiles_%02x_%02x_%02x_%02x_%04x.png" % (tileset_index, room.animation_id, palette_index, attributedata_bank, attributedata_addr) + data = { + "width": 10, "height": 8, + "type": "map", "renderorder": "right-down", "tiledversion": "1.4.3", "version": 1.4, + "tilewidth": 16, "tileheight": 16, "orientation": "orthogonal", + "tilesets": [ + { + "columns": 16, "firstgid": 1, + "image": image_filename, "imageheight": 256, "imagewidth": 256, + "margin": 0, "name": "main", "spacing": 0, + "tilecount": 256, "tileheight": 16, "tilewidth": 16 + } + ], + "layers": [{ + "data": [n+1 for n in room.overlay], + "width": 10, "height": 8, + "id": 1, "name": "Tiles", "type": "tilelayer", "visible": True, "opacity": 1, "x": 0, "y": 0, + }, { + "id": 2, "name": "EntityLayer", "type": "objectgroup", "visible": True, "opacity": 1, "x": 0, "y": 0, + "objects": [ + {"width": 16, "height": 16, "x": entity[0] * 16, "y": entity[1] * 16, "name": entityData.NAME[entity[2]], "type": "entity"} for entity in room.entities + ] + [ + {"width": 8, "height": 8, "x": 0, "y": idx * 8, "name": "%x:%02x:%03x:%02x:%02x" % (obj.warp_type, obj.map_nr, obj.room, obj.target_x, obj.target_y), "type": "warp"} for idx, obj in enumerate(room.getWarps()) if isinstance(obj, ObjectWarp) + ] + [ + {"width": 16, "height": 16, "x": obj.x * 16, "y": obj.y * 16, "name": "%02X" % (obj.type_id), "type": "hidden_tile"} for obj in hidden_warp_tiles + ], + }], + "properties": [ + {"name": "tileset", "type": "string", "value": "%02X" % (tileset_index)}, + {"name": "animationset", "type": "string", "value": "%02X" % (room.animation_id)}, + {"name": "attribset", "type": "string", "value": "%02X:%04X" % (attributedata_bank, attributedata_addr)}, + {"name": "palette", "type": "string", "value": "%02X" % (palette_index)}, + ] + } + if isinstance(room_index, str): + json.dump(data, open("%s/overworld/export/%s.json" % (path, room_index), "wt")) + else: + json.dump(data, open("%s/overworld/export/%02X.json" % (path, room_index), "wt")) + + if not os.path.exists("%s/overworld/export/%s" % (path, image_filename)): + tilemap = rom.banks[0x2F][tileset_index*0x100:tileset_index*0x100+0x200] + tilemap += rom.banks[0x2C][0x1200:0x1800] + tilemap += rom.banks[0x2C][0x0800:0x1000] + anim_addr = {2: 0x2B00, 3: 0x2C00, 4: 0x2D00, 5: 0x2E00, 6: 0x2F00, 7: 0x2D00, 8: 0x3000, 9: 0x3100, 10: 0x3200, 11: 0x2A00, 12: 0x3300, 13: 0x3500, 14: 0x3600, 15: 0x3400, 16: 0x3700}.get(room.animation_id, 0x0000) + tilemap[0x6C0:0x700] = rom.banks[0x2C][anim_addr:anim_addr + 0x40] + + palette = [] + for n in range(8*4): + p0 = rom.banks[0x21][palette_addr] + p1 = rom.banks[0x21][palette_addr + 1] + pal = p0 | p1 << 8 + palette_addr += 2 + r = (pal & 0x1F) << 3 + g = ((pal >> 5) & 0x1F) << 3 + b = ((pal >> 10) & 0x1F) << 3 + palette += [r, g, b] + + img = PIL.Image.new("P", (16*16, 16*16)) + img.putpalette(palette) + def drawTile(x, y, index, attr): + for py in range(8): + a = tilemap[index * 16 + py * 2] + b = tilemap[index * 16 + py * 2 + 1] + if attr & 0x40: + a = tilemap[index * 16 + 14 - py * 2] + b = tilemap[index * 16 + 15 - py * 2] + for px in range(8): + bit = 0x80 >> px + if attr & 0x20: + bit = 0x01 << px + c = (attr & 7) << 2 + if a & bit: + c |= 1 + if b & bit: + c |= 2 + img.putpixel((x+px, y+py), c) + for x in range(16): + for y in range(16): + idx = x+y*16 + metatiles = metatile_info[idx*4:idx*4+4] + attrtiles = attrtile_info[idx*4:idx*4+4] + drawTile(x * 16 + 0, y * 16 + 0, metatiles[0], attrtiles[0]) + drawTile(x * 16 + 8, y * 16 + 0, metatiles[1], attrtiles[1]) + drawTile(x * 16 + 0, y * 16 + 8, metatiles[2], attrtiles[2]) + drawTile(x * 16 + 8, y * 16 + 8, metatiles[3], attrtiles[3]) + img.save("%s/overworld/export/%s" % (path, image_filename)) + + world = { + "maps": [ + {"fileName": "%02X.json" % (n), "height": 128, "width": 160, "x": (n & 0x0F) * 160, "y": (n >> 4) * 128} + for n in range(0x100) + ], + "onlyShowAdjacentMaps": False, + "type": "world" + } + json.dump(world, open("%s/overworld/export/world.world" % (path), "wt")) + + +def isNormalOverworld(rom): + return len(RoomEditor(rom, 0x010).getWarps()) > 0 diff --git a/worlds/ladx/LADXR/patches/overworld/dive/00.json b/worlds/ladx/LADXR/patches/overworld/dive/00.json new file mode 100644 index 000000000000..fd16fa6675c4 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/00.json @@ -0,0 +1,124 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 125, 126, 1, 129, 78, 78, 78, 130, 1, 125, 240, 240, 240, 56, 11, 11, 11, 57, 240, 240, 2, 2, 30, 47, 73, 225, 74, 79, 94, 2, 2, 2, 56, 58, 226, 225, 59, 60, 57, 2, 2, 2, 56, 10, 10, 10, 10, 10, 123, 123, 2, 2, 56, 10, 10, 10, 10, 10, 57, 2, 2, 2, 47, 48, 48, 48, 48, 48, 79, 2], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":16, + "id":1, + "name":"HEART_PIECE", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":64, + "y":32 + }, + { + "height":16, + "id":2, + "name":"CROW", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":96, + "y":32 + }, + { + "height":16, + "id":3, + "name":"MINI_MOLDORM", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":112, + "y":96 + }, + { + "height":8, + "id":4, + "name":"1:07:23a:58:10", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":5, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"0B" + }, + { + "name":"attribset", + "type":"string", + "value":"25:3400" + }, + { + "name":"palette", + "type":"string", + "value":"0F" + }, + { + "name":"tileset", + "type":"string", + "value":"1C" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_1c_0b_0f_25_3400.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/01.json b/worlds/ladx/LADXR/patches/overworld/dive/01.json new file mode 100644 index 000000000000..0441d875a1c6 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/01.json @@ -0,0 +1,102 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[125, 126, 1, 1, 1, 1, 1, 1, 1, 125, 29, 29, 126, 1, 1, 129, 78, 130, 125, 29, 240, 240, 240, 240, 240, 56, 4, 57, 240, 240, 2, 2, 30, 81, 81, 47, 48, 79, 94, 2, 2, 2, 56, 4, 4, 206, 226, 216, 57, 2, 123, 123, 123, 11, 4, 4, 4, 4, 57, 2, 2, 2, 56, 11, 11, 11, 11, 11, 57, 2, 2, 2, 47, 48, 48, 48, 48, 48, 79, 2], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":16, + "id":1, + "name":"CROW", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":32, + "y":80 + }, + { + "height":8, + "id":2, + "name":"1:07:23d:58:10", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":3, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"0B" + }, + { + "name":"attribset", + "type":"string", + "value":"25:3400" + }, + { + "name":"palette", + "type":"string", + "value":"0F" + }, + { + "name":"tileset", + "type":"string", + "value":"1C" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_1c_0b_0f_25_3400.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/06.json b/worlds/ladx/LADXR/patches/overworld/dive/06.json new file mode 100644 index 000000000000..405a7aa73c1a --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/06.json @@ -0,0 +1,113 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 6, 7, 8, 1, 1, 1, 125, 126, 1, 129, 100, 101, 102, 130, 125, 126, 240, 240, 240, 56, 114, 29, 128, 57, 240, 240, 230, 230, 30, 56, 170, 171, 192, 57, 94, 230, 230, 230, 56, 47, 73, 225, 74, 79, 57, 230, 230, 230, 56, 63, 59, 225, 59, 64, 57, 230, 230, 30, 47, 48, 73, 225, 74, 48, 79, 94, 230, 56, 63, 59, 59, 225, 59, 59, 64, 57], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":16, + "id":1, + "name":"EGG_SONG_EVENT", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":0, + "y":0 + }, + { + "height":8, + "id":2, + "name":"1:08:270:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }, + { + "height":16, + "id":3, + "name":"E1", + "rotation":0, + "type":"hidden_tile", + "visible":true, + "width":16, + "x":80, + "y":48 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":4, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"0B" + }, + { + "name":"attribset", + "type":"string", + "value":"27:1620" + }, + { + "name":"palette", + "type":"string", + "value":"13" + }, + { + "name":"tileset", + "type":"string", + "value":"3C" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_3c_0b_13_27_1620.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/16.json b/worlds/ladx/LADXR/patches/overworld/dive/16.json new file mode 100644 index 000000000000..25538084ca78 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/16.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[30, 47, 48, 48, 73, 225, 48, 48, 48, 79, 56, 63, 59, 59, 59, 225, 59, 59, 59, 64, 56, 58, 59, 59, 59, 225, 59, 59, 59, 60, 47, 48, 48, 48, 73, 225, 74, 48, 48, 48, 58, 59, 226, 59, 59, 225, 59, 59, 59, 59, 201, 213, 4, 4, 4, 4, 4, 4, 4, 201, 201, 4, 4, 4, 4, 4, 4, 4, 4, 201, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"0:00:082:48:30", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"0B" + }, + { + "name":"attribset", + "type":"string", + "value":"27:1620" + }, + { + "name":"palette", + "type":"string", + "value":"13" + }, + { + "name":"tileset", + "type":"string", + "value":"3C" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_3c_0b_13_27_1620.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/62.json b/worlds/ladx/LADXR/patches/overworld/dive/62.json new file mode 100644 index 000000000000..26d6c9de6c74 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/62.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[2, 2, 2, 115, 117, 117, 117, 116, 2, 2, 2, 2, 2, 115, 118, 215, 119, 116, 2, 2, 2, 2, 30, 115, 117, 226, 117, 116, 94, 2, 2, 2, 56, 183, 117, 120, 117, 184, 57, 2, 2, 2, 56, 4, 4, 4, 4, 4, 57, 2, 2, 2, 56, 4, 4, 4, 4, 4, 57, 2, 2, 2, 47, 48, 73, 225, 74, 48, 79, 2, 2, 2, 63, 59, 59, 225, 59, 59, 64, 2], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:06:20e:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"27:1E40" + }, + { + "name":"palette", + "type":"string", + "value":"16" + }, + { + "name":"tileset", + "type":"string", + "value":"30" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_30_00_16_27_1e40.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/6C.json b/worlds/ladx/LADXR/patches/overworld/dive/6C.json new file mode 100644 index 000000000000..79b3f9617421 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/6C.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 44, 45, 45, 46, 15, 15, 15, 15, 15, 15, 56, 161, 199, 57, 15, 15, 15, 15, 15, 15, 56, 5, 5, 57, 15, 15, 15, 15, 15, 15, 52, 48, 48, 53, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:05:1b0:78:10", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"03" + }, + { + "name":"attribset", + "type":"string", + "value":"22:0000" + }, + { + "name":"palette", + "type":"string", + "value":"16" + }, + { + "name":"tileset", + "type":"string", + "value":"0F" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_0f_03_16_22_0000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/71.json b/worlds/ladx/LADXR/patches/overworld/dive/71.json new file mode 100644 index 000000000000..52910db6c4e1 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/71.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[3, 56, 63, 59, 59, 59, 59, 59, 64, 48, 3, 56, 58, 183, 59, 226, 59, 183, 60, 10, 3, 56, 33, 184, 10, 10, 10, 184, 10, 10, 3, 56, 33, 10, 10, 10, 10, 10, 10, 4, 3, 56, 201, 10, 10, 10, 10, 10, 4, 4, 3, 56, 201, 201, 10, 10, 10, 10, 10, 4, 3, 47, 48, 48, 48, 48, 48, 48, 48, 48, 3, 63, 59, 59, 59, 59, 59, 59, 59, 59], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:07:25d:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"25:3400" + }, + { + "name":"palette", + "type":"string", + "value":"19" + }, + { + "name":"tileset", + "type":"string", + "value":"1C" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_1c_00_19_25_3400.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/72.json b/worlds/ladx/LADXR/patches/overworld/dive/72.json new file mode 100644 index 000000000000..aa79f1a6559b --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/72.json @@ -0,0 +1,80 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[48, 54, 58, 59, 59, 225, 59, 59, 60, 46, 4, 4, 4, 4, 4, 4, 4, 4, 4, 57, 4, 4, 4, 4, 4, 93, 93, 93, 4, 77, 4, 4, 12, 12, 4, 93, 93, 93, 93, 4, 4, 4, 12, 4, 4, 4, 93, 93, 4, 4, 4, 4, 12, 4, 4, 4, 4, 4, 4, 4, 48, 73, 225, 74, 48, 73, 75, 74, 48, 48, 59, 59, 225, 59, 59, 59, 59, 59, 59, 59], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":1, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"22:0000" + }, + { + "name":"palette", + "type":"string", + "value":"16" + }, + { + "name":"tileset", + "type":"string", + "value":"0F" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_0f_00_16_22_0000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/73.json b/worlds/ladx/LADXR/patches/overworld/dive/73.json new file mode 100644 index 000000000000..b762b0ce87b1 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/73.json @@ -0,0 +1,113 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[2, 2, 2, 180, 180, 180, 180, 180, 2, 2, 2, 2, 44, 180, 180, 180, 180, 180, 46, 2, 81, 81, 76, 174, 178, 232, 174, 178, 57, 2, 4, 4, 4, 175, 179, 228, 175, 179, 57, 2, 4, 4, 4, 4, 4, 4, 4, 4, 57, 2, 4, 4, 4, 4, 4, 4, 4, 4, 57, 2, 48, 48, 48, 48, 48, 48, 48, 48, 53, 2, 59, 59, 59, 59, 59, 59, 59, 59, 64, 2], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":16, + "id":1, + "name":"ARMOS_STATUE", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":64, + "y":64 + }, + { + "height":16, + "id":2, + "name":"ARMOS_STATUE", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":96, + "y":64 + }, + { + "height":8, + "id":3, + "name":"1:05:1d4:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":4, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"25:3C00" + }, + { + "name":"palette", + "type":"string", + "value":"1C" + }, + { + "name":"tileset", + "type":"string", + "value":"2A" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_2a_00_1c_25_3c00.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/81.json b/worlds/ladx/LADXR/patches/overworld/dive/81.json new file mode 100644 index 000000000000..e85673835c03 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/81.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[11, 57, 63, 59, 214, 215, 216, 59, 64, 63, 11, 57, 58, 59, 206, 226, 207, 59, 60, 63, 11, 57, 15, 15, 15, 12, 15, 15, 15, 58, 11, 57, 15, 15, 15, 12, 15, 15, 15, 5, 11, 57, 15, 15, 15, 12, 15, 15, 5, 5, 55, 53, 15, 15, 15, 12, 12, 12, 5, 5, 38, 39, 10, 15, 15, 15, 15, 15, 15, 38, 40, 42, 39, 38, 39, 38, 39, 38, 39, 40], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:03:17a:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"03" + }, + { + "name":"attribset", + "type":"string", + "value":"27:2640" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"34" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_34_03_01_27_2640.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/82.json b/worlds/ladx/LADXR/patches/overworld/dive/82.json new file mode 100644 index 000000000000..ad6294870660 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/82.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[59, 59, 225, 59, 59, 59, 59, 59, 59, 59, 59, 59, 225, 59, 59, 59, 59, 59, 59, 59, 59, 59, 225, 59, 187, 59, 59, 59, 59, 59, 5, 5, 12, 10, 93, 10, 5, 5, 5, 5, 5, 5, 12, 5, 10, 5, 11, 11, 11, 5, 5, 5, 12, 5, 5, 11, 93, 93, 93, 11, 39, 5, 5, 5, 5, 11, 93, 93, 93, 11, 41, 5, 5, 5, 5, 5, 11, 11, 11, 62], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"0:00:016:28:50", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"25:0C00" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"0F" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_0f_00_01_25_0c00.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/83.json b/worlds/ladx/LADXR/patches/overworld/dive/83.json new file mode 100644 index 000000000000..a97454979ef8 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/83.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[59, 64, 38, 39, 72, 59, 59, 59, 59, 60, 59, 64, 40, 41, 57, 183, 184, 103, 82, 82, 59, 60, 212, 212, 57, 104, 228, 105, 82, 203, 5, 5, 5, 5, 57, 15, 15, 15, 15, 203, 5, 5, 62, 225, 53, 15, 15, 15, 15, 203, 5, 5, 57, 15, 15, 15, 15, 15, 82, 203, 5, 33, 57, 15, 15, 15, 15, 82, 82, 203, 48, 61, 51, 45, 45, 45, 45, 45, 45, 45], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:04:1a1:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"03" + }, + { + "name":"attribset", + "type":"string", + "value":"22:2400" + }, + { + "name":"palette", + "type":"string", + "value":"09" + }, + { + "name":"tileset", + "type":"string", + "value":"3A" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_3a_03_09_22_2400.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/91.json b/worlds/ladx/LADXR/patches/overworld/dive/91.json new file mode 100644 index 000000000000..1c9fe427d31e --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/91.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[43, 42, 63, 59, 59, 59, 59, 59, 64, 42, 42, 43, 63, 59, 183, 216, 183, 59, 64, 40, 43, 41, 58, 183, 184, 226, 184, 183, 60, 11, 41, 82, 82, 28, 28, 28, 28, 82, 19, 5, 82, 28, 28, 28, 28, 28, 28, 27, 23, 5, 82, 82, 28, 28, 82, 82, 28, 19, 33, 5, 82, 82, 82, 28, 28, 28, 28, 19, 33, 5, 38, 39, 38, 39, 38, 39, 38, 39, 38, 39], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:01:136:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"03" + }, + { + "name":"attribset", + "type":"string", + "value":"22:3400" + }, + { + "name":"palette", + "type":"string", + "value":"0E" + }, + { + "name":"tileset", + "type":"string", + "value":"36" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_36_03_0e_22_3400.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/92.json b/worlds/ladx/LADXR/patches/overworld/dive/92.json new file mode 100644 index 000000000000..1f78b1bebe61 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/92.json @@ -0,0 +1,102 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[39, 5, 5, 5, 5, 5, 5, 5, 5, 51, 41, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 11, 11, 11, 5, 38, 70, 39, 5, 5, 5, 83, 83, 83, 11, 40, 226, 41, 5, 5, 5, 92, 227, 92, 11, 11, 93, 11, 5, 5, 5, 5, 5, 5, 11, 11, 11, 5, 10, 5, 5, 5, 5, 5, 5, 5, 5, 10, 10, 38, 39, 5, 5, 5, 5, 5, 5, 10, 111], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:10:2cb:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }, + { + "height":8, + "id":2, + "name":"1:0e:2a1:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":8 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":3, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"03" + }, + { + "name":"attribset", + "type":"string", + "value":"22:0000" + }, + { + "name":"palette", + "type":"string", + "value":"0E" + }, + { + "name":"tileset", + "type":"string", + "value":"0F" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_0f_03_0e_22_0000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/93.json b/worlds/ladx/LADXR/patches/overworld/dive/93.json new file mode 100644 index 000000000000..c9ac830b6df1 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/93.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[45, 50, 63, 59, 59, 59, 59, 59, 64, 63, 5, 5, 58, 107, 109, 109, 109, 107, 60, 63, 5, 5, 183, 108, 99, 228, 99, 108, 183, 63, 5, 5, 184, 18, 28, 28, 28, 19, 184, 63, 5, 5, 5, 22, 17, 17, 17, 23, 5, 63, 10, 5, 10, 183, 5, 5, 5, 183, 5, 58, 10, 10, 10, 184, 5, 5, 5, 184, 5, 38, 111, 38, 39, 38, 39, 38, 39, 38, 39, 40], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:02:152:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"03" + }, + { + "name":"attribset", + "type":"string", + "value":"22:1800" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"2E" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_2e_03_01_22_1800.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/A1.json b/worlds/ladx/LADXR/patches/overworld/dive/A1.json new file mode 100644 index 000000000000..8ed9e355bb11 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/A1.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[43, 42, 44, 45, 45, 45, 46, 42, 43, 42, 42, 43, 47, 48, 48, 48, 79, 40, 41, 40, 43, 41, 58, 99, 228, 99, 60, 11, 11, 5, 41, 11, 11, 10, 10, 10, 10, 10, 10, 10, 111, 11, 183, 10, 10, 10, 183, 10, 10, 10, 111, 5, 184, 10, 10, 10, 184, 10, 5, 5, 111, 5, 5, 10, 10, 10, 10, 5, 5, 5, 48, 48, 73, 75, 74, 48, 48, 48, 48, 48], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:00:117:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"22:0C00" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"24" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_24_00_01_22_0c00.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/A2.json b/worlds/ladx/LADXR/patches/overworld/dive/A2.json new file mode 100644 index 000000000000..540f5c97b2e5 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/A2.json @@ -0,0 +1,113 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[43, 41, 5, 5, 5, 5, 5, 10, 10, 111, 41, 5, 5, 5, 5, 5, 5, 5, 5, 10, 5, 5, 38, 39, 93, 93, 93, 38, 39, 11, 10, 5, 40, 41, 83, 83, 83, 40, 41, 11, 10, 5, 11, 11, 92, 227, 92, 11, 11, 11, 5, 5, 11, 11, 11, 12, 11, 11, 11, 11, 5, 5, 5, 11, 12, 12, 5, 5, 5, 11, 61, 4, 4, 4, 12, 4, 4, 4, 4, 62], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":16, + "id":1, + "name":"BUTTERFLY", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":32, + "y":48 + }, + { + "height":16, + "id":2, + "name":"BUTTERFLY", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":112, + "y":80 + }, + { + "height":8, + "id":3, + "name":"1:10:2a3:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":4, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"22:0000" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"0F" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_0f_00_01_22_0000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/A3.json b/worlds/ladx/LADXR/patches/overworld/dive/A3.json new file mode 100644 index 000000000000..10eebc4c6f5c --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/A3.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[111, 82, 82, 82, 82, 82, 82, 82, 82, 82, 11, 11, 11, 5, 5, 5, 5, 11, 11, 82, 11, 183, 184, 10, 183, 201, 184, 5, 11, 82, 11, 206, 207, 10, 206, 226, 207, 5, 11, 82, 11, 5, 5, 11, 11, 11, 11, 11, 11, 82, 11, 11, 11, 5, 197, 10, 197, 11, 11, 82, 11, 5, 11, 11, 11, 11, 5, 5, 11, 82, 48, 48, 48, 48, 48, 48, 48, 48, 48, 61], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:ff:312:50:5c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"25:2800" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"38" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_38_00_01_25_2800.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/B0.json b/worlds/ladx/LADXR/patches/overworld/dive/B0.json new file mode 100644 index 000000000000..9203a9ec0371 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/B0.json @@ -0,0 +1,80 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[56, 4, 4, 4, 4, 4, 4, 4, 4, 57, 56, 183, 184, 4, 4, 4, 4, 62, 48, 53, 56, 206, 207, 4, 4, 4, 4, 57, 183, 184, 56, 161, 93, 4, 4, 4, 4, 57, 206, 207, 56, 93, 93, 62, 73, 75, 74, 79, 183, 184, 56, 93, 4, 57, 59, 59, 59, 60, 206, 207, 47, 48, 48, 79, 31, 31, 31, 31, 31, 31, 58, 59, 59, 60, 32, 32, 32, 32, 32, 32], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":1, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"02" + }, + { + "name":"attribset", + "type":"string", + "value":"22:1000" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"22" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_22_02_01_22_1000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/B1.json b/worlds/ladx/LADXR/patches/overworld/dive/B1.json new file mode 100644 index 000000000000..9ec04a19cd5c --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/B1.json @@ -0,0 +1,102 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[44, 46, 44, 45, 45, 45, 45, 46, 44, 46, 52, 53, 52, 48, 227, 48, 48, 53, 52, 53, 183, 184, 9, 9, 9, 9, 201, 183, 184, 9, 206, 207, 9, 9, 9, 9, 9, 206, 207, 9, 183, 184, 201, 9, 9, 9, 9, 183, 184, 9, 206, 207, 9, 9, 9, 9, 9, 206, 207, 9, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":16, + "id":1, + "name":"MARIN", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":48, + "y":48 + }, + { + "height":8, + "id":2, + "name":"1:1f:1e1:88:50", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":3, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"02" + }, + { + "name":"attribset", + "type":"string", + "value":"22:1000" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"22" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_22_02_01_22_1000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/B2.json b/worlds/ladx/LADXR/patches/overworld/dive/B2.json new file mode 100644 index 000000000000..b16f6fe221a3 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/B2.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[56, 4, 4, 4, 12, 4, 4, 4, 4, 57, 52, 54, 4, 4, 4, 9, 9, 9, 55, 53, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 183, 184, 9, 9, 9, 9, 9, 37, 9, 9, 206, 207, 36, 9, 9, 9, 9, 9, 9, 9, 9, 36, 9, 9, 9, 9, 9, 9, 9, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":16, + "id":1, + "name":"HEART_PIECE", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":80, + "y":80 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"02" + }, + { + "name":"attribset", + "type":"string", + "value":"22:1000" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"22" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_22_02_01_22_1000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/B3.json b/worlds/ladx/LADXR/patches/overworld/dive/B3.json new file mode 100644 index 000000000000..608aed75d3cf --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/B3.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[63, 59, 59, 63, 59, 59, 59, 64, 59, 56, 58, 59, 59, 63, 59, 59, 59, 64, 59, 56, 9, 9, 9, 58, 59, 187, 59, 60, 9, 56, 9, 9, 36, 9, 9, 36, 9, 9, 9, 56, 9, 9, 9, 9, 9, 9, 9, 9, 9, 56, 9, 9, 9, 9, 36, 9, 9, 9, 37, 56, 31, 31, 31, 31, 31, 31, 31, 31, 31, 52, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:1f:1f5:48:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"02" + }, + { + "name":"attribset", + "type":"string", + "value":"22:1000" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"22" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_22_02_01_22_1000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/owl.py b/worlds/ladx/LADXR/patches/owl.py new file mode 100644 index 000000000000..b22386a6cb8f --- /dev/null +++ b/worlds/ladx/LADXR/patches/owl.py @@ -0,0 +1,144 @@ +from ..roomEditor import RoomEditor +from ..assembler import ASM +from ..utils import formatText + + +def removeOwlEvents(rom): + # Remove all the owl events from the entity tables. + for room in range(0x100): + re = RoomEditor(rom, room) + if re.hasEntity(0x41): + re.removeEntities(0x41) + re.store(rom) + # Clear texts used by the owl. Potentially reused somewhere o else. + rom.texts[0x0D9] = b'\xff' # used by boomerang + # 1 Used by empty chest (master stalfos message) + # 8 unused (0x0C0-0x0C7) + # 1 used by bowwow in chest + # 1 used by item for other player message + # 2 used by arrow chest messages + # 2 used by tunics + for idx in range(0x0BE, 0x0CE): + rom.texts[idx] = b'\xff' + + + # Patch the owl entity into a ghost to allow refill of powder/bombs/arrows + rom.texts[0xC0] = formatText("Everybody hates me, so I give away free things in the hope people will love me. Want something?", ask="Okay No") + rom.texts[0xC1] = formatText("Good for you.") + rom.patch(0x06, 0x27F5, 0x2A77, ASM(""" + ; Check if we have powder or bombs. + ld e, INV_SIZE + ld hl, $DB00 +loop: + ldi a, [hl] + cp $02 ; bombs + jr z, hasProperItem + cp $0C ; powder + jr z, hasProperItem + cp $05 ; bow + jr z, hasProperItem + dec e + jr nz, loop + ret +hasProperItem: + + ; Render ghost + ld de, sprite + call $3BC0 + + call $64C6 ; check if game is busy (pops this stack frame if busy) + + ldh a, [$E7] ; frame counter + swap a + and $01 + call $3B0C ; set entity sprite variant + call $641A ; check collision + ldh a, [$F0] ;entity state + rst 0 + dw waitForTalk + dw talking + +waitForTalk: + call $645D ; check if talked to + ret nc + ld a, $C0 + call $2385 ; open dialog + call $3B12 ; increase entity state + ret + +talking: + ; Check if we are still talking + ld a, [$C19F] + and a + ret nz + call $3B12 ; increase entity state + ld [hl], $00 ; set to state 0 + ld a, [$C177] ; get which option we selected + and a + ret nz + + ; Give powder + ld a, [$DB4C] + cp $10 + jr nc, doNotGivePowder + ld a, $10 + ld [$DB4C], a +doNotGivePowder: + + ld a, [$DB4D] + cp $10 + jr nc, doNotGiveBombs + ld a, $10 + ld [$DB4D], a +doNotGiveBombs: + + ld a, [$DB45] + cp $10 + jr nc, doNotGiveArrows + ld a, $10 + ld [$DB45], a +doNotGiveArrows: + + ld a, $C1 + call $2385 ; open dialog + ret + +sprite: + db $76, $09, $78, $09, $7A, $09, $7C, $09 +""", 0x67F5), fill_nop=True) + rom.patch(0x20, 0x0322 + 0x41 * 2, "734A", "564B") # Remove the owl init handler + + re = RoomEditor(rom, 0x2A3) + re.entities.append((7, 6, 0x41)) + re.store(rom) + + +def upgradeDungeonOwlStatues(rom): + # Call our custom handler after the check for the stone beak + rom.patch(0x18, 0x1EA2, ASM("ldh a, [$F7]\ncp $FF\njr nz, $05"), ASM("ld a, $09\nrst 8\nret"), fill_nop=True) + +def upgradeOverworldOwlStatues(rom): + # Replace the code that handles signs/owl statues on the overworld + # This removes a "have marin with you" special case to make some room for our custom owl handling. + rom.patch(0x00, 0x201A, ASM(""" + cp $6F + jr z, $2B + cp $D4 + jr z, $27 + ld a, [$DB73] + and a + jr z, $08 + ld a, $78 + call $237C + jp $20CF + """), ASM(""" + cp $D4 + jr z, $2B + cp $6F + jr nz, skip + + ld a, $09 + rst 8 + jp $20CF +skip: + """), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/phone.py b/worlds/ladx/LADXR/patches/phone.py new file mode 100644 index 000000000000..f38745606c38 --- /dev/null +++ b/worlds/ladx/LADXR/patches/phone.py @@ -0,0 +1,60 @@ +from ..assembler import ASM + + +def patchPhone(rom): + rom.texts[0x141] = b"" + rom.texts[0x142] = b"" + rom.texts[0x143] = b"" + rom.texts[0x144] = b"" + rom.texts[0x145] = b"" + rom.texts[0x146] = b"" + rom.texts[0x147] = b"" + rom.texts[0x148] = b"" + rom.texts[0x149] = b"" + rom.texts[0x14A] = b"" + rom.texts[0x14B] = b"" + rom.texts[0x14C] = b"" + rom.texts[0x14D] = b"" + rom.texts[0x14E] = b"" + rom.texts[0x14F] = b"" + rom.texts[0x16E] = b"" + rom.texts[0x1FD] = b"" + rom.texts[0x228] = b"" + rom.texts[0x229] = b"" + rom.texts[0x22A] = b"" + rom.texts[0x240] = b"" + rom.texts[0x241] = b"" + rom.texts[0x242] = b"" + rom.texts[0x243] = b"" + rom.texts[0x244] = b"" + rom.texts[0x245] = b"" + rom.texts[0x247] = b"" + rom.texts[0x248] = b"" + rom.patch(0x06, 0x2A8F, 0x2BBC, ASM(""" + ; We use $DB6D to store which tunics we have. This is normally the Dungeon9 instrument, which does not exist. + ld a, [$DC0F] + ld hl, wCollectedTunics + inc a + + cp $01 + jr nz, notTunic1 + bit 0, [HL] + jr nz, notTunic1 + inc a +notTunic1: + + cp $02 + jr nz, notTunic2 + bit 1, [HL] + jr nz, notTunic2 + inc a +notTunic2: + + cp $03 + jr nz, noWrap + xor a +noWrap: + + ld [$DC0F], a + ret + """), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/photographer.py b/worlds/ladx/LADXR/patches/photographer.py new file mode 100644 index 000000000000..2b377c4d89f0 --- /dev/null +++ b/worlds/ladx/LADXR/patches/photographer.py @@ -0,0 +1,19 @@ +from ..assembler import ASM + + +def fixPhotographer(rom): + # Allow richard photo without slime key + rom.patch(0x36, 0x3234, ASM("jr nz, $52"), "", fill_nop=True) + rom.patch(0x36, 0x3240, ASM("jr z, $46"), "", fill_nop=True) + # Allow richard photo when castle is opened + rom.patch(0x36, 0x31FF, ASM("jp nz, $7288"), "", fill_nop=True) + # Allow photographer with bowwow saved + rom.patch(0x36, 0x0398, ASM("or [hl]"), "", fill_nop=True) + rom.patch(0x36, 0x3183, ASM("ret nz"), "", fill_nop=True) + rom.patch(0x36, 0x31CB, ASM("jp nz, $7288"), "", fill_nop=True) + rom.patch(0x36, 0x03DC, ASM("and $7F"), ASM("and $00")) + # Allow bowwow photo with follower + rom.patch(0x36, 0x31DA, ASM("jp nz, $7288"), "", fill_nop=True) + # Allow bridge photo with follower + rom.patch(0x36, 0x004D, ASM("call nz, $3F8D"), "", fill_nop=True) + rom.patch(0x36, 0x006D, ASM("ret nz"), "", fill_nop=True) # Checks if any entity is alive diff --git a/worlds/ladx/LADXR/patches/reduceRNG.py b/worlds/ladx/LADXR/patches/reduceRNG.py new file mode 100644 index 000000000000..cfeb4da6e475 --- /dev/null +++ b/worlds/ladx/LADXR/patches/reduceRNG.py @@ -0,0 +1,9 @@ +from ..assembler import ASM + + +def slowdownThreeOfAKind(rom): + rom.patch(0x06, 0x096B, ASM("ldh a, [$E7]\nand $0F"), ASM("ldh a, [$E7]\nand $3F")) + + +def fixHorseHeads(rom): + rom.patch(0x07, 0x3653, "00010400", "00010000") diff --git a/worlds/ladx/LADXR/patches/rooster.py b/worlds/ladx/LADXR/patches/rooster.py new file mode 100644 index 000000000000..c8bd831c8820 --- /dev/null +++ b/worlds/ladx/LADXR/patches/rooster.py @@ -0,0 +1,40 @@ +from ..assembler import ASM +from ..utils import formatText + + +def patchRooster(rom): + # Do not give the rooster + rom.patch(0x19, 0x0E9D, ASM("ld [$DB7B], a"), "", fill_nop=True) + + # Do not load the rooster sprites + rom.patch(0x00, 0x2EC7, ASM("jr nz, $08"), "", fill_nop=True) + + # Draw the found item + rom.patch(0x19, 0x0E4A, ASM("ld hl, $4E37\nld c, $03\ncall $3CE6"), ASM("ld a, $0C\nrst $08"), fill_nop=True) + rom.patch(0x19, 0x0E7B, ASM("ld hl, $4E37\nld c, $03\ncall $3CE6"), ASM("ld a, $0C\nrst $08"), fill_nop=True) + # Give the item and message + rom.patch(0x19, 0x0E69, ASM("ld a, $6D\ncall $2373"), ASM("ld a, $0E\nrst $08"), fill_nop=True) + + # Reuse unused evil eagle text slot for rooster message + rom.texts[0x0B8] = formatText("Got the {ROOSTER}!") + + # Allow rooster pickup with special rooster item + rom.patch(0x19, 0x1ABC, ASM("cp $03"), ASM("cp $0F")) + rom.patch(0x19, 0x1AAE, ASM("cp $03"), ASM("cp $0F")) + + # Ignore the has-rooster flag in the rooster entity (do not despawn) + rom.patch(0x19, 0x19E0, ASM("jp z, $7E61"), "", fill_nop=True) + + # If we are spawning the rooster, and the rooster is already existing, do not do anything, instead of despawning the rooster. + rom.patch(0x01, 0x1FEF, ASM("ld [hl], d"), ASM("ret")) + # Allow rooster to unload when changing rooms + rom.patch(0x19, 0x19E9, ASM("ld [hl], a"), "", fill_nop=True) + + # Do not take away the rooster after D7 + rom.patch(0x03, 0x1E25, ASM("ld [$DB7B], a"), "", fill_nop=True) + + # Patch the color dungeon entrance not to check for rooster + rom.patch(0x02, 0x3409, ASM("ld hl, $DB7B\nor [hl]"), "", fill_nop=True) + + # Spawn marin at taltal even with rooster + rom.patch(0x18, 0x1EE3, ASM("jp nz, $7F08"), "", fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/save.py b/worlds/ladx/LADXR/patches/save.py new file mode 100644 index 000000000000..c55d6314a4df --- /dev/null +++ b/worlds/ladx/LADXR/patches/save.py @@ -0,0 +1,54 @@ +from ..assembler import ASM +from ..backgroundEditor import BackgroundEditor + + +def singleSaveSlot(rom): + # Do not validate/erase slots 2 and 3 at rom start + rom.patch(0x01, 0x06B3, ASM("call $4794"), "", fill_nop=True) + rom.patch(0x01, 0x06B9, ASM("call $4794"), "", fill_nop=True) + + # Patch the code that checks if files have proper filenames to skip file 2/3 + rom.patch(0x01, 0x1DD9, ASM("ld b, $02"), ASM("ret"), fill_nop=True) + + # Remove the part that writes death counters for save2/3 on the file select screen + rom.patch(0x01, 0x0821, 0x084B, "", fill_nop=True) + # Remove the call that updates the hearts for save2 + rom.patch(0x01, 0x0800, ASM("call $4DBE"), "", fill_nop=True) + # Remove the call that updates the hearts for save3 + rom.patch(0x01, 0x0806, ASM("call $4DD6"), "", fill_nop=True) + + # Remove the call that updates the names for save2 and save3 + rom.patch(0x01, 0x0D70, ASM("call $4D94\ncall $4D9D"), "", fill_nop=True) + + # Remove the 2/3 slots from the screen and remove the copy text + be = BackgroundEditor(rom, 0x03) + del be.tiles[0x9924] + del be.tiles[0x9984] + be.store(rom) + be = BackgroundEditor(rom, 0x04) + del be.tiles[0x9924] + del be.tiles[0x9984] + for n in range(0x99ED, 0x99F1): + del be.tiles[n] + be.store(rom) + + # Do not do left/right for erase/copy selection. + rom.patch(0x01, 0x092B, ASM("jr z, $0B"), ASM("jr $0B")) + # Only switch between players + rom.patch(0x01, 0x08FA, 0x091D, ASM(""" + ld a, [$DBA7] + and a + ld a, [$DBA6] + jr z, skip + xor $03 +skip: + """), fill_nop=True) + + # On the erase screen, only switch between save 1 and return + rom.patch(0x01, 0x0E12, ASM("inc a\nand $03"), ASM("xor $03"), fill_nop=True) + rom.patch(0x01, 0x0E21, ASM("dec a\ncp $ff\njr nz, $02\nld a,$03"), ASM("xor $03"), fill_nop=True) + + be = BackgroundEditor(rom, 0x06) + del be.tiles[0x9924] + del be.tiles[0x9984] + be.store(rom) diff --git a/worlds/ladx/LADXR/patches/seashell.py b/worlds/ladx/LADXR/patches/seashell.py new file mode 100644 index 000000000000..7e53516d5e63 --- /dev/null +++ b/worlds/ladx/LADXR/patches/seashell.py @@ -0,0 +1,64 @@ +from ..assembler import ASM + + +def fixSeashell(rom): + # Do not unload if we have the lvl2 sword. + rom.patch(0x03, 0x1FD3, ASM("ld a, [$DB4E]\ncp $02\njp nc, $3F8D"), "", fill_nop=True) + # Do not unload in the ghost house + rom.patch(0x03, 0x1FE8, ASM("ldh a, [$F8]\nand $40\njp z, $3F8D"), "", fill_nop=True) + + # Call our special rendering code + rom.patch(0x03, 0x1FF2, ASM("ld de, $5FD1\ncall $3C77"), ASM("ld a, $05\nrst 8"), fill_nop=True) + + # Call our special handlers for messages and pickup + rom.patch(0x03, 0x2368, 0x237C, ASM(""" + ld a, $0A ; showMessageMultiworld + rst 8 + ld a, $06 ; giveItemMultiworld + rst 8 + call $512A + ret + """), fill_nop=True) + + +def upgradeMansion(rom): + rom.patch(0x19, 0x38EC, ASM(""" + ld hl, $78DC + jr $03 + """), "", fill_nop=True) + rom.patch(0x19, 0x38F1, ASM(""" + ld hl, $78CC + ld c, $04 + call $3CE6 + """), ASM(""" + ld a, $0C + rst 8 + """), fill_nop=True) + rom.patch(0x19, 0x3718, ASM("sub $13"), ASM("sub $0D")) + rom.patch(0x19, 0x3697, ASM(""" + cp $70 + jr c, $15 + ld [hl], $70 + """), ASM(""" + cp $73 + jr c, $15 + ld [hl], $73 + """)) + rom.patch(0x19, 0x36F5, ASM(""" + ld a, $02 + ld [$DB4E], a + """), ASM(""" + ld a, $0E ; give item and message for current room multiworld + rst 8 + """), fill_nop=True) + rom.patch(0x19, 0x36E6, ASM(""" + ld a, $9F + call $2385 + """), "", fill_nop=True) + rom.patch(0x19, 0x31E8, ASM(""" + ld a, [$DB4E] + and $02 + """), ASM(""" + ld a, [$DAE9] + and $10 + """)) diff --git a/worlds/ladx/LADXR/patches/shop.py b/worlds/ladx/LADXR/patches/shop.py new file mode 100644 index 000000000000..197fe09b18ca --- /dev/null +++ b/worlds/ladx/LADXR/patches/shop.py @@ -0,0 +1,152 @@ +from ..assembler import ASM + + +def fixShop(rom): + # Move shield visuals to the 2nd slot, and arrow to 3th slot + rom.patch(0x04, 0x3732 + 22, "986A027FB2B098AC01BAB1", "9867027FB2B098A801BAB1") + rom.patch(0x04, 0x3732 + 55, "986302B1B07F98A4010A09", "986B02B1B07F98AC010A09") + + # Just use a fixed location in memory to store which inventory we give. + rom.patch(0x04, 0x37C5, "0708", "0802") + + # Patch the code that decides which shop to show. + rom.patch(0x04, 0x3839, 0x388E, ASM(""" + push bc + jr skipSubRoutine + +checkInventory: + ld hl, $DB00 ; inventory + ld c, INV_SIZE +loop: + cp [hl] + ret z + inc hl + dec c + jr nz, loop + and a + ret + +skipSubRoutine: + ; Set the shop table to all nothing. + ld hl, $C505 + xor a + ldi [hl], a + ldi [hl], a + ldi [hl], a + ldi [hl], a + ld de, $C505 + + ; Check if we want to load a key item into the shop. + ldh a, [$F8] + bit 4, a + jr nz, checkForSecondKeyItem + ld a, $01 + ld [de], a + jr checkForShield +checkForSecondKeyItem: + bit 5, a + jr nz, checkForShield + ld a, $05 + ld [de], a + +checkForShield: + inc de + ; Check if we have the shield or the bow to see if we need to remove certain entries from the shop + ld a, [$DB44] + and a + jr z, hasNoShieldLevel + ld a, $03 + ld [de], a ; Add shield buy option +hasNoShieldLevel: + + inc de + ld a, $05 + call checkInventory + jr nz, hasNoBow + ld a, $06 + ld [de], a ; Add arrow buy option +hasNoBow: + + inc de + ld a, $02 + call checkInventory + jr nz, hasNoBombs + ld a, $04 + ld [de], a ; Add bomb buy option +hasNoBombs: + + pop bc + call $3B12 ; increase entity state + """, 0x7839), fill_nop=True) + + # We do not have enough room at the shovel/bow buy entry to handle this + # So jump to a bit where we have some more space to work, as there is some dead code in the shop. + rom.patch(0x04, 0x3AA9, 0x3AAE, ASM("jp $7AC3"), fill_nop=True) + + # Patch over the "you stole it" dialog + rom.patch(0x00, 0x1A1C, 0x1A21, ASM("""ld a, $C9 + call $2385"""), fill_nop=True) + rom.patch(0x04, 0x3AC3, 0x3AD8, ASM(""" + ; No room override needed, we're in the proper room + ; Call our chest item giving code. + ld a, $0E + rst 8 + ; Update the room status to mark first item as bought + ld hl, $DAA1 + ld a, [hl] + or $10 + ld [hl], a + ret + """), fill_nop=True) + rom.patch(0x04, 0x3A73, 0x3A7E, ASM("jp $7A91"), fill_nop=True) + rom.patch(0x04, 0x3A91, 0x3AA9, ASM(""" + ; Override the room - luckily nothing will go wrong here if we leave it as is + ld a, $A7 + ldh [$F6], a + ; Call our chest item giving code. + ld a, $0E + rst 8 + ; Update the room status to mark second item as bought + ld hl, $DAA1 + ld a, [hl] + or $20 + ld [hl], a + ret + """), fill_nop=True) + + # Patch shop item graphics rendering to use some new code at the end of the bank. + rom.patch(0x04, 0x3B91, 0x3BAC, ASM(""" + call $7FD0 + """), fill_nop=True) + rom.patch(0x04, 0x3BD3, 0x3BE3, ASM(""" + jp $7FD0 + """), fill_nop=True) + rom.patch(0x04, 0x3FD0, "00" * 42, ASM(""" + ; Check if first key item + and a + jr nz, notShovel + ld a, [$77C5] + ldh [$F1], a + ld a, $01 + rst 8 + ret +notShovel: + cp $04 + jr nz, notBow + ld a, [$77C6] + ldh [$F1], a + ld a, $01 + rst 8 + ret +notBow: + cp $05 + jr nz, notArrows + ; Load arrow graphics and render then as a dual sprite + ld de, $7B58 + call $3BC0 + ret +notArrows: + ; Load the normal graphics + ld de, $7B5A + jp $3C77 + """), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/softlock.py b/worlds/ladx/LADXR/patches/softlock.py new file mode 100644 index 000000000000..15a9119538c6 --- /dev/null +++ b/worlds/ladx/LADXR/patches/softlock.py @@ -0,0 +1,93 @@ +from ..roomEditor import RoomEditor, Object +from ..assembler import ASM + + +def fixAll(rom): + # Prevent soft locking in the first mountain cave if we do not have a feather + re = RoomEditor(rom, 0x2B7) + re.removeObject(3, 3) + re.store(rom) + + # Prevent getting stuck in the sidescroll room in the beginning of dungeon 5 + re = RoomEditor(rom, 0x1A9) + re.objects[6].count = 7 + re.store(rom) + + # Cave that allows you to escape from D4 without flippers, make it no longer require a feather + re = RoomEditor(rom, 0x1EA) + re.objects[9].count = 8 + re.removeObject(5, 4) + re.moveObject(4, 4, 7, 5) + re.store(rom) + + # D3 west side room requires feather to get the key. But feather is not required to unlock the door, potentially softlocking you. + re = RoomEditor(rom, 0x155) + re.changeObject(4, 1, 0xcf) + re.changeObject(4, 6, 0xd0) + re.store(rom) + + # D3 boots room requires boots to escape + re = RoomEditor(rom, 0x146) + re.removeObject(5, 6) + re.store(rom) + + allowRaftGameWithoutFlippers(rom) + # We cannot access thes holes in logic: + # removeBirdKeyHoleDrop(rom) + fixDoghouse(rom) + flameThrowerShieldRequirement(rom) + fixLessThen3MaxHealth(rom) + +def fixDoghouse(rom): + # Fix entering the dog house from the back, and ending up out of bounds. + re = RoomEditor(rom, 0x0A1) + re.objects.append(Object(6, 2, 0x0E2)) + re.objects.append(re.objects[20]) # Move the flower patch after the warp entry definition so it overrules the tile + re.objects.append(re.objects[3]) + + re.objects.pop(22) + re.objects.pop(21) + re.objects.pop(20) # Remove the flower patch at the normal entry index + re.objects.pop(11) # Duplicate object, we can just remove it, gives room for our custom entry door + re.store(rom) + +def allowRaftGameWithoutFlippers(rom): + # Allow jumping down the waterfall in the raft game without the flippers. + rom.patch(0x02, 0x2E8F, ASM("ld a, [$DB0C]"), ASM("ld a, $01"), fill_nop=True) + # Change the room that goes back up to the raft game from the bottom, so we no longer need flippers + re = RoomEditor(rom, 0x1F7) + re.changeObject(3, 2, 0x1B) + re.changeObject(2, 3, 0x1B) + re.changeObject(3, 4, 0x1B) + re.changeObject(4, 5, 0x1B) + re.changeObject(6, 6, 0x1B) + re.store(rom) + +def removeBirdKeyHoleDrop(rom): + # Prevent the cave with the bird key from dropping you in the water + # (if you do not have flippers this would softlock you) + rom.patch(0x02, 0x1176, ASM(""" + ldh a, [$F7] + cp $0A + jr nz, $30 + """), ASM(""" + nop + nop + nop + nop + jr $30 + """)) + # Remove the hole that drops you all the way from dungeon7 entrance to the water in the cave + re = RoomEditor(rom, 0x01E) + re.removeObject(5, 4) + re.store(rom) + +def flameThrowerShieldRequirement(rom): + # if you somehow get a lvl3 shield or higher, it no longer works against the flamethrower, easy fix. + rom.patch(0x03, 0x2EBA, + ASM("ld a, [$DB44]\ncp $02\nret nz"), # if not shield level 2 + ASM("ld a, [$DB44]\ncp $02\nret c")) # if not shield level 2 or higher + +def fixLessThen3MaxHealth(rom): + # The table that starts your start HP when you die is not working for less then 3 HP, and locks the game. + rom.patch(0x01, 0x1295, "18181818", "08081018") diff --git a/worlds/ladx/LADXR/patches/songs.py b/worlds/ladx/LADXR/patches/songs.py new file mode 100644 index 000000000000..59ca01c4c8c4 --- /dev/null +++ b/worlds/ladx/LADXR/patches/songs.py @@ -0,0 +1,159 @@ +from ..assembler import ASM + + +def upgradeMarin(rom): + # Show marin outside, even without a sword. + rom.patch(0x05, 0x0E78, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True) + # Make marin ignore the fact that you did not save the tarin yet, and allowing getting her song + rom.patch(0x05, 0x0E87, ASM("ld a, [$D808]"), ASM("ld a, $10"), fill_nop=True) + rom.patch(0x05, 0x0F73, ASM("ld a, [$D808]"), ASM("ld a, $10"), fill_nop=True) + rom.patch(0x05, 0x0FB0, ASM("ld a, [$DB48]"), ASM("ld a, $01"), fill_nop=True) + # Show marin in the animal village + rom.patch(0x03, 0x0A86, ASM("ld a, [$DB74]"), ASM("ld a, $01"), fill_nop=True) + rom.patch(0x05, 0x3F2E, ASM("ld a, [$DB74]"), ASM("ld a, $01"), fill_nop=True) # animal d0 + rom.patch(0x15, 0x3F96, ASM("ld a, [$DB74]"), ASM("ld a, $01"), fill_nop=True) # animal d1 + rom.patch(0x18, 0x11B0, ASM("ld a, [$DB74]"), ASM("ld a, $01"), fill_nop=True) # animal d2 + + # Instead of checking if we have the ballad, check if we have a specific room flag set + rom.patch(0x05, 0x0F89, ASM(""" + ld a, [$DB49] + and $04 + """), ASM(""" + ld a, [$D892] + and $10 + """), fill_nop=True) + rom.patch(0x05, 0x0FDF, ASM(""" + ld a, [$DB49] + and $04 + """), ASM(""" + ld a, [$D892] + and $10 + """), fill_nop=True) + rom.patch(0x05, 0x1042, ASM(""" + ld a, [$DB49] + and $04 + """), ASM(""" + ld a, [$D892] + and $10 + """), fill_nop=True) + + # Patch that we call our specific handler instead of giving the song + rom.patch(0x05, 0x1170, ASM(""" + ld hl, $DB49 + set 2, [hl] + xor a + ld [$DB4A], a + """), ASM(""" + ; Mark Marin as done. + ld a, [$D892] + or $10 + ld [$D892], a + """), fill_nop=True) + + + # Show the right item instead of the ocerina + rom.patch(0x05, 0x11B3, ASM(""" + ld de, $515F + xor a + ldh [$F1], a + jp $3C77 + """), ASM(""" + ld a, $0C + rst 8 + ret + """), fill_nop=True) + + # Patch the message that tells we got the song, to give the item and show the right message + rom.patch(0x05, 0x119C, ASM(""" + ld a, $13 + call $2385 + """), ASM(""" + ld a, $0E + rst 8 + """), fill_nop=True) + + +def upgradeManbo(rom): + # Instead of checking if we have the song, check if we have a specific room flag set + rom.patch(0x18, 0x0536, ASM(""" + ld a, [$DB49] + and $02 + """), ASM(""" + ld a, [$DAFD] + and $20 + """), fill_nop=True) + + # Show the right item instead of the ocerina + rom.patch(0x18, 0x0786, ASM(""" + ld de, $474D + xor a + ldh [$F1], a + jp $3C77 + """), ASM(""" + ld a, $0C + rst 8 + ret + """), fill_nop=True) + + # Patch to replace song giving to give the right item + rom.patch(0x18, 0x0757, ASM(""" + ld a, $01 + ld [$DB4A], a + ld hl, $DB49 + set 1, [hl] + """), ASM(""" + ; Mark Manbo as done. + ld hl, $DAFD + set 5, [hl] + ; Show item message and give item + ld a, $0E + rst 8 + """), fill_nop=True) + # Remove the normal "got song message") + rom.patch(0x18, 0x076F, 0x0774, "", fill_nop=True) + +def upgradeMamu(rom): + # Always allow the sign maze instead of only allowing the sign maze if you do not have song3 + rom.patch(0x00, 0x2057, ASM("ld a, [$DB49]"), ASM("ld a, $00"), fill_nop=True) + + # Patch the condition at which Mamu gives you the option to listen to him + rom.patch(0x18, 0x0031, ASM(""" + ld a, [$DB49] + and $01 + """), ASM(""" + ld a, [$DAFB] ; load room flag of the Mamu room + and $10 + """), fill_nop=True) + + # Show the right item instead of the ocerina + rom.patch(0x18, 0x0299, ASM(""" + ld de, $474D + xor a + ldh [$F1], a + call $3C77 + """), ASM(""" + ld a, $0C + rst 8 + """), fill_nop=True) + + # Patch given an item + rom.patch(0x18, 0x0270, ASM(""" + ld a, $02 + ld [$DB4A], a + ld hl, $DB49 + set 0, [hl] + """), ASM(""" + ; Set the room complete flag. + ld hl, $DAFB + set 4, [hl] + """), fill_nop=True) + + # Patch to show the right message for the item + rom.patch(0x18, 0x0282, ASM(""" + ld a, $DF + call $4087 + """), ASM(""" + ; Give item and message for room. + ld a, $0E + rst 8 + """), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/tarin.py b/worlds/ladx/LADXR/patches/tarin.py new file mode 100644 index 000000000000..d84935634e32 --- /dev/null +++ b/worlds/ladx/LADXR/patches/tarin.py @@ -0,0 +1,51 @@ +from ..assembler import ASM +from ..utils import formatText + + +def updateTarin(rom): + # Do not give the shield. + rom.patch(0x05, 0x0CD0, ASM(""" + ld d, $04 + call $5321 + ld a, $01 + ld [$DB44], a + """), "", fill_nop=True) + + # Instead of showing the usual "your shield back" message, give the proper message and give the item. + rom.patch(0x05, 0x0CDE, ASM(""" + ld a, $91 + call $2385 + """), ASM(""" + ld a, $0B ; GiveItemAndMessageForRoom + rst 8 + """), fill_nop=True) + + rom.patch(0x05, 0x0CF0, ASM(""" + xor a + ldh [$F1], a + ld de, $4CC6 + call $3C77 + """), ASM(""" + ld a, $0C ; RenderItemForRoom + rst 8 + xor a + ldh [$F1], a + """), fill_nop=True) + + # Set the room status to finished. (replaces a GBC check) + rom.patch(0x05, 0x0CAB, 0x0CB0, ASM(""" + ld a, $20 + call $36C4 + """), fill_nop=True) + + # Instead of checking for the shield level to put you in the bed, check the room flag. + rom.patch(0x05, 0x1202, ASM("ld a, [$DB44]\nand a"), ASM("ldh a, [$F8]\nand $20")) + rom.patch(0x05, 0x0C6D, ASM("ld a, [$DB44]\nand a"), ASM("ldh a, [$F8]\nand $20")) + + # If the starting item is picked up, load the right palette when entering the room + rom.patch(0x21, 0x0176, ASM("ld a, [$DB48]\ncp $01"), ASM("ld a, [$DAA3]\ncp $A1"), fill_nop=True) + rom.patch(0x05, 0x0C94, "FF473152C5280000", "FD2ED911CE100000") + rom.patch(0x05, 0x0CB0, ASM("ld hl, $DC88"), ASM("ld hl, $DC80")) + + # Patch the text that Tarin uses to give your shield back. + rom.texts[0x54] = formatText("#####, it is dangerous to go alone!\nTake this!") diff --git a/worlds/ladx/LADXR/patches/titleScreen.py b/worlds/ladx/LADXR/patches/titleScreen.py new file mode 100644 index 000000000000..52adba68136e --- /dev/null +++ b/worlds/ladx/LADXR/patches/titleScreen.py @@ -0,0 +1,89 @@ +from ..backgroundEditor import BackgroundEditor +import subprocess +import binascii + + +CHAR_MAP = {'z': 0x3E, '-': 0x3F, '.': 0x39, ':': 0x42, '?': 0x3C, '!': 0x3D} + + +def _encode(s): + result = bytearray() + for char in s: + if ord("A") <= ord(char) <= ord("Z"): + result.append(ord(char) - ord("A")) + elif ord("a") <= ord(char) <= ord("y"): + result.append(ord(char) - ord("a") + 26) + elif ord("0") <= ord(char) <= ord("9"): + result.append(ord(char) - ord("0") + 0x70) + else: + result.append(CHAR_MAP.get(char, 0x7E)) + return result + + +def setRomInfo(rom, seed, settings, player_name, player_id): + #try: + # version = subprocess.run(['git', 'describe', '--tags', '--dirty=-D'], stdout=subprocess.PIPE).stdout.strip().decode("ascii", "replace") + #except: + # version = "" + + try: + seednr = int(seed, 16) + except: + import hashlib + seednr = int(hashlib.md5(seed.encode('ascii', 'replace')).hexdigest(), 16) + + if settings.race: + seed = "Race" + if isinstance(settings.race, str): + seed += " " + settings.race + rom.patch(0x00, 0x07, "00", "01") + else: + rom.patch(0x00, 0x07, "00", "52") + + line_1_hex = _encode(seed) + #line_2_hex = _encode(seed[16:]) + BASE_DRAWING_AREA = 0x98a0 + LINE_WIDTH = 0x20 + player_id_text = f"Player {player_id}:" + for n in (3, 4): + be = BackgroundEditor(rom, n) + ba = BackgroundEditor(rom, n, attributes=True) + + for n, v in enumerate(_encode(player_id_text)): + be.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 5 + 2 + n] = v + ba.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 5 + 2 + n] = 0x00 + for n, v in enumerate(_encode(player_name)): + be.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 6 + 0x13 - len(player_name) + n] = v + ba.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 6 + 0x13 - len(player_name) + n] = 0x00 + for n, v in enumerate(line_1_hex): + be.tiles[0x9a20 + n] = v + ba.tiles[0x9a20 + n] = 0x00 + + for n in range(0x09, 0x14): + be.tiles[0x9820 + n] = 0x7F + be.tiles[0x9840 + n] = 0xA0 + (n % 2) + be.tiles[0x9860 + n] = 0xA2 + sn = seednr + for n in range(0x0A, 0x14): + tilenr = sn % 30 + sn //= 30 + if tilenr > 12: + tilenr += 2 + if tilenr > 16: + tilenr += 1 + if tilenr > 19: + tilenr += 3 + if tilenr > 27: + tilenr += 1 + if tilenr > 29: + tilenr += 2 + if tilenr > 35: + tilenr += 1 + be.tiles[0x9800 + n] = tilenr * 2 + be.tiles[0x9820 + n] = tilenr * 2 + 1 + pal = sn % 8 + sn //= 8 + ba.tiles[0x9800 + n] = 0x08 | pal + ba.tiles[0x9820 + n] = 0x08 | pal + be.store(rom) + ba.store(rom) diff --git a/worlds/ladx/LADXR/patches/tradeSequence.py b/worlds/ladx/LADXR/patches/tradeSequence.py new file mode 100644 index 000000000000..5b608977f20d --- /dev/null +++ b/worlds/ladx/LADXR/patches/tradeSequence.py @@ -0,0 +1,355 @@ +from ..assembler import ASM + + +def patchTradeSequence(rom, boomerang_option): + patchTrendy(rom) + patchPapahlsWife(rom) + patchYipYip(rom) + patchBananasSchule(rom) + patchKiki(rom) + patchTarin(rom) + patchBear(rom) + patchPapahl(rom) + patchGoatMrWrite(rom) + patchGrandmaUlrira(rom) + patchFisherman(rom) + patchMermaid(rom) + patchMermaidStatue(rom) + patchSharedCode(rom) + patchVarious(rom, boomerang_option) + patchInventoryMenu(rom) + + +def patchTrendy(rom): + # Trendy game yoshi + rom.patch(0x04, 0x3502, 0x350F, ASM(""" + ldh a, [$F8] ; room status + and a, $20 + jp nz, $6D7A ; clear entity + ; Render sprite + ld a, $0F + rst 8 + ; Reset the sprite variant, else the code gets confused + xor a + ldh [$F1], a ; sprite variant + """), fill_nop=True) + rom.patch(0x04, 0x2E80, ASM("ldh a, [$F8]"), ASM("ld a, $10")) # Prevent marin cutscene from triggering, as that locks the game now. + rom.patch(0x04, 0x3622, 0x3627, "", fill_nop=True) # Dont set the trade item + + +def patchPapahlsWife(rom): + # Rewrite how the first dialog is generated. + rom.patch(0x18, 0x0E7A, 0x0EA8, ASM(""" + ldh a, [$F8] ; room status + and a, $20 + jr nz, tradeDone + + ld a, [wTradeSequenceItem] + and $01 + jr nz, requestTrade + + ld a, $2A ; Dialog about wanting a yoshi doll + jp $2373 ; OpenDialogInTable1 +tradeDone: + ld a, $2C ; Dialog about kids, after trade is done + jp $2373 ; OpenDialogInTable1 +requestTrade: + ld a, $2B ; Dialog about kids, after trade is done + call $3B12; IncrementEntityState + jp $2373 ; OpenDialogInTable1 + """), fill_nop=True) + rom.patch(0x18, 0x0EB4, 0x0EBD, ASM("ld hl, wTradeSequenceItem\nres 0, [hl]"), fill_nop=True) # Take the trade item + + +def patchYipYip(rom): + # Change how the decision is made to draw yipyip with a ribbon + rom.patch(0x06, 0x1A2C, 0x1A36, ASM(""" + ldh a, [$F8] ; room status + and $20 + jr z, tradeNotDone + ld de, $59C8 ; yipyip with ribbon +tradeNotDone: + """), fill_nop=True) + # Check if we have the ribbon + rom.patch(0x06, 0x1A7C, 0x1A83, ASM(""" + ld a, [wTradeSequenceItem] + and $02 + jr z, $07 + """), fill_nop=True) + rom.patch(0x06, 0x1AAF, 0x1AB8, ASM("ld hl, wTradeSequenceItem\nres 1, [hl]"), fill_nop=True) # Take the trade item + + +def patchBananasSchule(rom): + # Change how to check if we have the right trade item + rom.patch(0x19, 0x2D54, 0x2D5B, ASM(""" + ld a, [wTradeSequenceItem] + and $04 + jr z, $08 + """), fill_nop=True) + rom.patch(0x19, 0x2DF0, 0x2DF9, ASM("ld hl, wTradeSequenceItem\nres 2, [hl]"), fill_nop=True) # Take the trade item + # Change how the decision is made to render less bananas + rom.patch(0x19, 0x2EF1, 0x2EFA, ASM(""" + ldh a, [$F8] + and $20 + jr z, skip + dec c + dec c +skip: """), fill_nop=True) + + # Part of the same entity code, but this is the painter, which changes the dialog depending on mermaid scale or magnifier + rom.patch(0x19, 0x2F95, 0x2F9C, ASM(""" + ld a, [wTradeSequenceItem2] + and $10 ; Check for mermaid scale + jr z, $04 + """)) + rom.patch(0x19, 0x2FA0, 0x2FA4, ASM(""" + and $20 ; Check for magnifier + jr z, $07 + """)) + rom.patch(0x19, 0x2CE3, "9A159C15", "B41DB61D") # Properly draw the dog food + + +def patchKiki(rom): + rom.patch(0x07, 0x18E6, 0x18ED, ASM(""" + ld a, [wTradeSequenceItem] + and $08 ; check for banana + jr z, $08 + """)) + rom.patch(0x07, 0x19AF, 0x19B4, "", fill_nop=True) # Do not change trading item memory + rom.patch(0x07, 0x19CC, 0x19D5, ASM("ld hl, wTradeSequenceItem\nres 3, [hl]"), fill_nop=True) # Take the trade item + rom.patch(0x07, 0x194D, "9A179C17", "B81FBA1F") # Properly draw the banana above kiki + + +def patchTarin(rom): + rom.patch(0x07, 0x0EC5, 0x0ECA, ASM(""" + ld a, [wTradeSequenceItem] + and $10 ; check for stick + """)) + rom.patch(0x07, 0x0F30, 0x0F33, "", fill_nop=True) # Take the trade item + # Honeycomb, change how we detect that it should fall on entering the room + rom.patch(0x07, 0x0CCC, 0x0CD3, ASM(""" + ld a, [$D887] + and $40 + jr z, $14 + """)) + # Something about tarin changing messages or not showing up depending on the trade sequence + rom.patch(0x05, 0x0BFF, 0x0C07, "", fill_nop=True) # Just ignore the trade sequence + rom.patch(0x05, 0x0D20, 0x0D27, "", fill_nop=True) # Just ignore the trade sequence + rom.patch(0x05, 0x0DAF, 0x0DB8, "", fill_nop=True) # Tarin giving bananas? + + rom.patch(0x07, 0x0D6D, 0x0D7A, ASM("ld hl, wTradeSequenceItem\nres 4, [hl]"), fill_nop=True) # Take the trade item + + +def patchBear(rom): + # Change the trade item check + rom.patch(0x07, 0x0BCC, 0x0BD3, ASM(""" + ld a, [wTradeSequenceItem] + and $20 ; check for honeycomb + jr z, $0E + """)) + rom.patch(0x07, 0x0C21, ASM("jr nz, $22"), "", fill_nop=True) + rom.patch(0x07, 0x0C23, 0x0C2A, ASM(""" + ld a, [wTradeSequenceItem] + and $20 ; check for honeycomb + jr z, $08 + """)) + + rom.patch(0x07, 0x0C3C, 0x0C43, ASM(""" + nop + nop + nop + nop + nop + jr $02 + """)) + rom.patch(0x07, 0x0C5E, 0x0C67, ASM("ld hl, wTradeSequenceItem\nres 5, [hl]"), fill_nop=True) # Take the trade item + + +def patchPapahl(rom): + rom.patch(0x07, 0x0A21, 0x0A30, ASM("call $7EA4"), fill_nop=True) # Never show indoor papahl + # Render the bag condition + rom.patch(0x07, 0x0A81, 0x0A88, ASM(""" + ldh a, [$F8] ; current room status + and $20 + nop + jr nz, $18 + """)) + # Check for the right item + rom.patch(0x07, 0x0ACF, 0x0AD4, ASM(""" + ld a, [wTradeSequenceItem] + and $40 ; pineapple + """)) + rom.patch(0x07, 0x0AD6, ASM("jr z, $02"), ASM("jr nz, $02")) + + rom.patch(0x07, 0x0AF9, 0x0B00, ASM(""" + ld a, [wTradeSequenceItem] + and $40 ; pineapple + jr z, $0E + """)) + rom.patch(0x07, 0x0B2F, 0x0B38, ASM("ld hl, wTradeSequenceItem\nres 6, [hl]"), fill_nop=True) # Take the trade item + + +def patchGoatMrWrite(rom): # The goat and mrwrite are the same entity + rom.patch(0x18, 0x0BF1, 0x0BF8, ASM(""" + ldh a, [$F8] + and $20 + nop + jr nz, $03 + """)) # Check if we made the trade with the goat + rom.patch(0x18, 0x0C2C, 0x0C33, ASM(""" + ld a, [wTradeSequenceItem] + and $80 ; hibiscus + jr z, $08 + """)) # Check if we have the hibiscus + rom.patch(0x18, 0x0C3D, 0x0C41, "", fill_nop=True) + rom.patch(0x18, 0x0C6B, 0x0C74, ASM("ld hl, wTradeSequenceItem\nres 7, [hl]"), fill_nop=True) # Take the trade item for the goat + + rom.patch(0x18, 0x0C8B, 0x0C92, ASM(""" + ld a, [wTradeSequenceItem2] + and $01 ; letter + jr z, $08 + """)) # Check if we have the letter + rom.patch(0x18, 0x0C9C, 0x0CA0, "", fill_nop=True) + rom.patch(0x18, 0x0CE2, 0x0CEB, ASM("ld hl, wTradeSequenceItem2\nres 0, [hl]"), fill_nop=True) # Take the trade item for mrwrite + + +def patchGrandmaUlrira(rom): + rom.patch(0x18, 0x0D2C, ASM("jr z, $02"), "", fill_nop=True) # Always show up in animal village + rom.patch(0x18, 0x0D3C, 0x0D51, ASM(""" + ldh a, [$F8] + and $20 + jp nz, $4D58 + """), fill_nop=True) + rom.patch(0x18, 0x0D95, 0x0D9A, "", fill_nop=True) + rom.patch(0x18, 0x0D9C, 0x0DA0, "", fill_nop=True) + rom.patch(0x18, 0x0DA3, 0x0DAA, ASM(""" + ld a, [wTradeSequenceItem2] + and $02 ; broom + jr z, $0B + """)) + rom.patch(0x18, 0x0DC4, 0x0DC7, "", fill_nop=True) + rom.patch(0x18, 0x0DE2, 0x0DEB, ASM("ld hl, wTradeSequenceItem2\nres 1, [hl]"), fill_nop=True) # Take the trade item + rom.patch(0x18, 0x0E1D, 0x0E20, "", fill_nop=True) + rom.patch(0x18, 0x0D13, "9A149C14", "D01CD21C") + + +def patchFisherman(rom): + # Not sure what this first check is for + rom.patch(0x07, 0x02F8, 0x0300, ASM(""" + """), fill_nop=True) + # Check for the hook + rom.patch(0x07, 0x04BF, 0x04C6, ASM(""" + ld a, [wTradeSequenceItem2] + and $04 ; hook + jr z, $08 + """)) + rom.patch(0x07, 0x04F3, 0x04F6, "", fill_nop=True) + rom.patch(0x07, 0x057D, 0x0586, ASM("ld hl, wTradeSequenceItem2\nres 2, [hl]"), fill_nop=True) # Take the trade item + rom.patch(0x04, 0x1F88, 0x1F8B, "", fill_nop=True) + + +def patchMermaid(rom): + # Check for the right trade item + rom.patch(0x07, 0x0797, 0x079E, ASM(""" + ld a, [wTradeSequenceItem2] + and $08 ; necklace + jr z, $0B + """)) + rom.patch(0x07, 0x0854, 0x085B, ASM("ld hl, wTradeSequenceItem2\nres 3, [hl]"), fill_nop=True) # Take the trade item + + +def patchMermaidStatue(rom): + rom.patch(0x18, 0x095D, 0x0962, "", fill_nop=True) + rom.patch(0x18, 0x0966, 0x097A, ASM(""" + ld a, [wTradeSequenceItem2] + and $10 ; scale + ret z + ldh a, [$F8] + and $20 + ret nz + """), fill_nop=True) + + +def patchSharedCode(rom): + # Trade item render code override. + rom.patch(0x07, 0x1535, 0x1575, ASM(""" + ldh a, [$F9] + and a + jr z, notSideScroll + + ldh a, [$EC]; hActiveEntityVisualPosY + add a, $02 + ldh [$EC], a +notSideScroll: + ; Render sprite + ld a, $0F + rst 8 + """), fill_nop=True) + # Trade item message code + # rom.patch(0x07, 0x159F, 0x15B9, ASM(""" + # ld a, $09 ; give message and item (from alt item table) + # rst 8 + # """), fill_nop=True) + rom.patch(0x07, 0x159F, 0x15B9, ASM(""" + ldh a, [$F6] ; map room + cp $B2 + jr nz, NotYipYip + add a, 2 ; Add 2 to room to set room pointer to an empty room for trade items + ldh [$F6], a + ld a, $0e ; giveItemMultiworld + rst 8 + ldh a, [$F6] ; map room + sub a, 2 ; ...and undo it + ldh [$F6], a + jr Done + NotYipYip: + ld a, $0e ; giveItemMultiworld + rst 8 + Done: + """), fill_nop=True) + + + # Prevent changing the 2nd trade item memory + rom.patch(0x07, 0x15BD, 0x15C1, ASM(""" + call $7F7F + xor a ; we need to exit with A=00 + """), fill_nop=True) + rom.patch(0x07, 0x3F7F, "00" * 7, ASM("ldh a, [$F8]\nor $20\nldh [$F8], a\nret")) + + +def patchVarious(rom, boomerang_option): + # Make the zora photo work with the magnifier + rom.patch(0x18, 0x09F3, 0x0A02, ASM(""" + ld a, [wTradeSequenceItem2] + and $20 ; MAGNIFYING_GLASS + jp z, $7F08 ; ClearEntityStatusBank18 + """), fill_nop=True) + rom.patch(0x03, 0x0B6D, 0x0B75, ASM(""" + ld a, [wTradeSequenceItem2] + and $20 ; MAGNIFYING_GLASS + jp z, $3F8D ; UnloadEntity + """), fill_nop=True) + # Mimic invisibility + rom.patch(0x18, 0x2AC8, 0x2ACE, "", fill_nop=True) + # Ignore trade quest state for marin at beach + rom.patch(0x18, 0x219E, 0x21A6, "", fill_nop=True) + # Shift the magnifier 8 pixels + rom.patch(0x03, 0x0F68, 0x0F6F, ASM(""" + ldh a, [$F6] ; map room + cp $97 ; check if we are in the maginfier room + jp z, $4F83 + """), fill_nop=True) + # Something with the photographer + rom.patch(0x36, 0x0948, 0x0950, "", fill_nop=True) + + if boomerang_option not in {'trade', 'gift'}: # Boomerang cave is not patched, so adjust it + rom.patch(0x19, 0x05EC, ASM("ld a, [wTradeSequenceItem]\ncp $0E\njp nz, $7E61"), ASM("ld a, [wTradeSequenceItem2]\nand $20\njp z, $7E61")) # show the guy + rom.patch(0x00, 0x3199, ASM("ld a, [wTradeSequenceItem]\ncp $0E\njr nz, $06"), ASM("ld a, [wTradeSequenceItem2]\nand $20\njr z, $06")) # load the proper room layout + rom.patch(0x19, 0x05F4, 0x05FB, "", fill_nop=True) + + +def patchInventoryMenu(rom): + # Never draw the trade item the normal way + rom.patch(0x20, 0x1A2E, ASM("ld a, [wTradeSequenceItem2]\nand a\njr nz, $23"), ASM("jp $5A57"), fill_nop=True) + + rom.patch(0x20, 0x1EB5, ASM("ldh a, [$FE]\nand a\njr z, $34"), ASM("ld a, $10\nrst 8"), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/trendy.py b/worlds/ladx/LADXR/patches/trendy.py new file mode 100644 index 000000000000..21118274fc08 --- /dev/null +++ b/worlds/ladx/LADXR/patches/trendy.py @@ -0,0 +1,3 @@ + +def fixTrendy(rom): + rom.patch(0x04, 0x2F29, "04", "02") # Patch the trendy game shield to be a ruppee diff --git a/worlds/ladx/LADXR/patches/tunicFairy.py b/worlds/ladx/LADXR/patches/tunicFairy.py new file mode 100644 index 000000000000..d61f63408752 --- /dev/null +++ b/worlds/ladx/LADXR/patches/tunicFairy.py @@ -0,0 +1,45 @@ +from ..utils import formatText +from ..assembler import ASM + + +def upgradeTunicFairy(rom): + rom.texts[0x268] = formatText("Welcome, #####. I admire you for coming this far.") + rom.texts[0x0CC] = formatText("Got the {RED_TUNIC}! You can change Tunics at the phone booths.") + rom.texts[0x0CD] = formatText("Got the {BLUE_TUNIC}! You can change Tunics at the phone booths.") + + rom.patch(0x36, 0x111C, 0x1133, ASM(""" + call $3B12 + ld a, [$DDE1] + and $10 + jr z, giveItems + ld [hl], $09 + ret + +giveItems: + ld a, [$DDE1] + or $10 + ld [$DDE1], a + """), fill_nop=True) + rom.patch(0x36, 0x1139, 0x1144, ASM(""" + ld a, $04 + ldh [$F6], a + ld a, $0E + rst 8 + """), fill_nop=True) + + rom.patch(0x36, 0x1162, 0x1192, ASM(""" + ld a, $01 + ldh [$F6], a + ld a, $0E + rst 8 + """), fill_nop=True) + + rom.patch(0x36, 0x119D, 0x11A2, "", fill_nop=True) + rom.patch(0x36, 0x11B5, 0x11BE, ASM(""" + ; Skip to the end ignoring all the tunic giving animation. + call $3B12 + ld [hl], $09 + """), fill_nop=True) + + rom.banks[0x36][0x11BF] = 0x87 + rom.banks[0x36][0x11C0] = 0x88 diff --git a/worlds/ladx/LADXR/patches/weapons.py b/worlds/ladx/LADXR/patches/weapons.py new file mode 100644 index 000000000000..9c949934c93a --- /dev/null +++ b/worlds/ladx/LADXR/patches/weapons.py @@ -0,0 +1,64 @@ +from ..assembler import ASM +from ..roomEditor import RoomEditor + + +def patchSuperWeapons(rom): + # Feather jump height + rom.patch(0x00, 0x1508, ASM("ld a, $20"), ASM("ld a, $2C")) + # Boots charge speed + rom.patch(0x00, 0x1731, ASM("cp $20"), ASM("cp $01")) + # Power bracelet pickup speed + rom.patch(0x00, 0x2121, ASM("ld e, $08"), ASM("ld e, $01")) + # Throwing speed (of pickups and bombs) + rom.patch(0x14, 0x1313, "30D0000018E80000", "60A0000040C00000") + rom.patch(0x14, 0x1323, "0000D0300000E818", "0000A0600000C040") + + # Allow as many bombs to be placed as you want! + rom.patch(0x00, 0x135F, ASM("ret nc"), "", fill_nop=True) + + # Maximum amount of arrows in the air + rom.patch(0x00, 0x13C5, ASM("cp $02"), ASM("cp $05")) + # Delay between arrow shots + rom.patch(0x00, 0x13C9, ASM("ld a, $10"), ASM("ld a, $01")) + + # Maximum amount of firerod fires + rom.patch(0x00, 0x12E4, ASM("cp $02"), ASM("cp $05")) + + # Projectile speed (arrows, firerod) + rom.patch(0x00, 0x13AD, + "30D0000040C00000" "0000D0300000C040", + "60A0000060A00000" "0000A0600000A060") + + # Hookshot shoot speed + rom.patch(0x02, 0x024C, + "30D00000" "0000D030", + "60A00000" "0000A060") + # Hookshot retract speed + rom.patch(0x18, 0x3C41, ASM("ld a, $30"), ASM("ld a, $60")) + # Hookshot pull speed + rom.patch(0x18, 0x3C21, ASM("ld a, $30"), ASM("ld a, $60")) + + # Super shovel, always price! + rom.patch(0x02, 0x0CC6, ASM("jr nz, $57"), "", fill_nop=True) + + # Unlimited boomerangs! + rom.patch(0x00, 0x1387, ASM("ret nz"), "", fill_nop=True) + + # Increase shield push power + rom.patch(0x03, 0x2FC5, ASM("ld a, $08"), ASM("ld a, $10")) + rom.patch(0x03, 0x2FCA, ASM("ld a, $20"), ASM("ld a, $40")) + # Decrease link pushback of shield + rom.patch(0x03, 0x2FB9, ASM("ld a, $12"), ASM("ld a, $04")) + rom.patch(0x03, 0x2F9A, ASM("ld a, $0C"), ASM("ld a, $03")) + + # Super charge the ocarina + rom.patch(0x02, 0x0AD8, ASM("cp $38"), ASM("cp $08")) + rom.patch(0x02, 0x0B05, ASM("cp $14"), ASM("cp $04")) + + re = RoomEditor(rom, 0x23D) + tiles = re.getTileArray() + tiles[11] = 0x0D + tiles[12] = 0xA7 + tiles[22] = 0x98 + re.buildObjectList(tiles) + re.store(rom) \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/witch.py b/worlds/ladx/LADXR/patches/witch.py new file mode 100644 index 000000000000..d87190eedd53 --- /dev/null +++ b/worlds/ladx/LADXR/patches/witch.py @@ -0,0 +1,58 @@ +from ..assembler import ASM +from ..roomEditor import RoomEditor + + +def updateWitch(rom): + # Add a heartpiece at the toadstool, the item patches turn this into a 1 time toadstool item + # Or depending on flags, in something else. + re = RoomEditor(rom, 0x050) + re.addEntity(2, 3, 0x35) + re.store(rom) + + # Change what happens when you trade the toadstool with the witch + # Note that the 2nd byte of this code gets patched with the item to give from the witch. + rom.patch(0x05, 0x08D4, 0x08F0, ASM(""" + ; Get the room flags and mark the witch as done. + ld hl, $DAA2 + ld a, [hl] + and $30 + set 4, [hl] + set 5, [hl] + jr z, item +powder: + ld e, $09 ; give powder every time after the first time. + ld a, e + ldh [$F1], a + ld a, $11 + rst 8 + jp $48F0 +item: + ld a, $0E + rst 8 + """), fill_nop=True) + + # Patch the toadstool to unload when you haven't delivered something to the witch yet. + rom.patch(0x03, 0x1D4B, ASM(""" + ld hl, $DB4B + ld a, [$DB4C] + or [hl] + jp nz, $3F8D + """), ASM(""" + ld a, [$DAA2] + and $20 + jp z, $3F8D + """), fill_nop=True) + + # Patch what happens when we pickup the toadstool, call our chest code to give a toadstool. + rom.patch(0x03, 0x1D6F, 0x1D7D, ASM(""" + ld a, $50 + ldh [$F1], a + ld a, $02 ; give item + rst 8 + + ld hl, $DAA2 + res 5, [hl] + """), fill_nop=True) + +def witchIsPatched(rom): + return sum(rom.banks[0x05][0x08D4:0x08F0]) != 0x0DC2 diff --git a/worlds/ladx/LADXR/plan.py b/worlds/ladx/LADXR/plan.py new file mode 100644 index 000000000000..df1908f433ea --- /dev/null +++ b/worlds/ladx/LADXR/plan.py @@ -0,0 +1,38 @@ + + +# Helper class to read and store planomizer data +class Plan: + def __init__(self, filename): + self.forced_items = {} + self.item_pool = {} + item_group = {} + + for line in open(filename, "rt"): + line = line.strip() + if ";" in line: + line = line[:line.find(";")] + if "#" in line: + line = line[:line.find("#")] + if ":" not in line: + continue + entry_type, params = map(str.strip, line.upper().split(":", 1)) + + if entry_type == "LOCATION" and ":" in params: + location, item = map(str.strip, params.split(":", 1)) + if item == "": + continue + if item.startswith("[") and item.endswith("]"): + item = item_group[item[1:-1]] + if "," in item: + item = list(map(str.strip, item.split(","))) + self.forced_items[location] = item + elif entry_type == "POOL" and ":" in params: + item, count = map(str.strip, params.split(":", 1)) + self.item_pool[item] = self.item_pool.get(item, 0) + int(count) + elif entry_type == "GROUP" and ":" in params: + name, item = map(str.strip, params.split(":", 1)) + if item == "": + continue + if "," in item: + item = list(map(str.strip, item.split(","))) + item_group[name] = item diff --git a/worlds/ladx/LADXR/pointerTable.py b/worlds/ladx/LADXR/pointerTable.py new file mode 100644 index 000000000000..6b56b6ff4449 --- /dev/null +++ b/worlds/ladx/LADXR/pointerTable.py @@ -0,0 +1,207 @@ +import copy +import struct + + +class PointerTable: + END_OF_DATA = (0xff, ) + + """ + Class to manage a list of pointers to data objects + Can rewrite the rom to modify the data objects and still keep the pointers intact. + """ + def __init__(self, rom, info): + assert "count" in info + assert "pointers_bank" in info + assert "pointers_addr" in info + assert ("banks_bank" in info and "banks_addr" in info) or ("data_bank" in info) + self.__info = info + + self.__data = [] + self.__alt_data = {} + self.__banks = [] + self.__storage = [] + + count = info["count"] + addr = info["pointers_addr"] + pointers_bank = rom.banks[info["pointers_bank"]] + if "data_addr" in info: + pointers_raw = [] + for n in range(count): + pointers_raw.append(info["data_addr"] + 0x4000 + pointers_bank[addr + n] * info["data_size"]) + else: + pointers_raw = struct.unpack("<" + "H" * count, pointers_bank[addr:addr+count*2]) + if "data_bank" in info: + banks = [info["data_bank"]] * count + else: + addr = info["banks_addr"] + banks = rom.banks[info["banks_bank"]][addr:addr+count] + + if "alt_pointers" in self.__info: + for key, (bank, addr) in self.__info["alt_pointers"].items(): + pointer = struct.unpack("= len(s) and st["bank"] == bank: + my_storage = st + break + assert my_storage is not None, "Not enough room in storage... %s" % (storage) + + pointer = my_storage["start"] + my_storage["start"] = pointer + len(s) + rom.banks[bank][pointer:pointer + len(s)] = s + + rom.banks[ptr_bank][ptr_addr] = pointer & 0xFF + rom.banks[ptr_bank][ptr_addr + 1] = (pointer >> 8) | 0x40 + + for n, s in enumerate(self.__data): + if isinstance(s, int): + pointer = s + else: + s = bytes(s) + bank = self.__banks[n] + if s in done[bank]: + pointer = done[bank][s] + assert rom.banks[bank][pointer:pointer+len(s)] == s + else: + my_storage = None + for st in storage: + if st["end"] - st["start"] >= len(s) and st["bank"] == bank: + my_storage = st + break + assert my_storage is not None, "Not enough room in storage... %d/%d %s id:%x(%d) bank:%d" % (n, len(self.__data), storage, n, n, bank) + + pointer = my_storage["start"] + my_storage["start"] = pointer + len(s) + rom.banks[bank][pointer:pointer+len(s)] = s + + if "data_size" not in self.__info: + # aggressive de-duplication. + for skip in range(len(s)): + done[bank][s[skip:]] = pointer + skip + done[bank][s] = pointer + + if "data_addr" in self.__info: + offset = pointer - self.__info["data_addr"] + if "data_size" in self.__info: + assert offset % self.__info["data_size"] == 0 + offset //= self.__info["data_size"] + rom.banks[pointers_bank][pointers_addr + n] = offset + else: + rom.banks[pointers_bank][pointers_addr+n*2] = pointer & 0xff + rom.banks[pointers_bank][pointers_addr+n*2+1] = ((pointer >> 8) & 0xff) | 0x40 + + space_left = sum(map(lambda n: n["end"] - n["start"], storage)) + # print(self.__class__.__name__, "Space left:", space_left) + return storage + + def _readData(self, rom, bank_nr, pointer): + bank = rom.banks[bank_nr] + start = pointer + if "data_size" in self.__info: + pointer += self.__info["data_size"] + else: + while bank[pointer] not in self.END_OF_DATA: + pointer += 1 + pointer += 1 + self._addStorage(bank_nr, start, pointer) + return bank[start:pointer] + + def _addStorage(self, bank, start, end): + for n, data in enumerate(self.__storage): + if data["bank"] == bank: + if data["start"] == end: + data["start"] = start + return + if data["end"] == start: + data["end"] = end + return + if data["start"] <= start and data["end"] >= end: + return + self.__storage.append({"bank": bank, "start": start, "end": end}) + + def __mergeStorage(self): + for n in range(len(self.__storage)): + n_end = self.__storage[n]["end"] + n_start = self.__storage[n]["start"] + for m in range(len(self.__storage)): + if m == n or self.__storage[n]["bank"] != self.__storage[m]["bank"]: + continue + m_end = self.__storage[m]["end"] + m_start = self.__storage[m]["start"] + if m_start - 1 <= n_end <= m_end: + self.__storage[n]["start"] = min(self.__storage[n]["start"], self.__storage[m]["start"]) + self.__storage[n]["end"] = self.__storage[m]["end"] + self.__storage.pop(m) + return True + return False + + def addStorage(self, extra_storage): + for data in extra_storage: + self._addStorage(data["bank"], data["start"], data["end"]) + while self.__mergeStorage(): + pass + self.__storage.sort(key=lambda n: n["start"]) + + def adjustDataStart(self, new_start): + self.__info["data_addr"] = new_start \ No newline at end of file diff --git a/worlds/ladx/LADXR/rom.py b/worlds/ladx/LADXR/rom.py new file mode 100644 index 000000000000..e7ff2f244d0d --- /dev/null +++ b/worlds/ladx/LADXR/rom.py @@ -0,0 +1,75 @@ +import binascii + +b2h = binascii.hexlify +h2b = binascii.unhexlify + + +class ROM: + def __init__(self, filename): + data = open(filename, "rb").read() + #assert len(data) == 1024 * 1024 + self.banks = [] + for n in range(0x40): + self.banks.append(bytearray(data[n*0x4000:(n+1)*0x4000])) + + def patch(self, bank_nr, addr, old, new, *, fill_nop=False): + new = h2b(new) + bank = self.banks[bank_nr] + if old is not None: + if isinstance(old, int): + old = bank[addr:old] + else: + old = h2b(old) + if fill_nop: + assert len(old) >= len(new), "Length mismatch: %d != %d (%s != %s)" % (len(old), len(new), b2h(old), b2h(new)) + new += b'\x00' * (len(old) - len(new)) + else: + assert len(old) == len(new), "Length mismatch: %d != %d (%s != %s)" % (len(old), len(new), b2h(old), b2h(new)) + assert addr >= 0 and addr + len(old) <= 16*1024 + if bank[addr:addr+len(old)] != old: + if bank[addr:addr + len(old)] == new: + # Patch is already applied. + return + loc = bank.find(old) + while loc > -1: + print("Possible at:", hex(loc)) + loc = bank.find(old, loc+1) + assert False, "Patch mismatch:\n%s !=\n%s at 0x%04x" % (b2h(bank[addr:addr+len(old)]), b2h(old), addr) + bank[addr:addr+len(new)] = new + assert len(bank) == 0x4000 + + def fixHeader(self, *, name=None): + if name is not None: + name = name.encode("utf-8") + name = (name + (b"\x00" * 15))[:15] + self.banks[0][0x134:0x143] = name + + checksum = 0 + for c in self.banks[0][0x134:0x14D]: + checksum -= c + 1 + self.banks[0][0x14D] = checksum & 0xFF + + # zero out the checksum before calculating it. + self.banks[0][0x14E] = 0 + self.banks[0][0x14F] = 0 + checksum = 0 + for bank in self.banks: + checksum = (checksum + sum(bank)) & 0xFFFF + self.banks[0][0x14E] = checksum >> 8 + self.banks[0][0x14F] = checksum & 0xFF + + def save(self, file, *, name=None): + # don't pass the name to fixHeader + self.fixHeader() + if isinstance(file, str): + f = open(file, "wb") + for bank in self.banks: + f.write(bank) + f.close() + print("Saved:", file) + else: + for bank in self.banks: + file.write(bank) + + def readHexSeed(self): + return self.banks[0x3E][0x2F00:0x2F10].hex().upper() diff --git a/worlds/ladx/LADXR/romTables.py b/worlds/ladx/LADXR/romTables.py new file mode 100644 index 000000000000..fbabe7595f58 --- /dev/null +++ b/worlds/ladx/LADXR/romTables.py @@ -0,0 +1,219 @@ +from .rom import ROM +from .pointerTable import PointerTable +from .assembler import ASM + + +class Texts(PointerTable): + END_OF_DATA = (0xfe, 0xff) + + def __init__(self, rom): + super().__init__(rom, { + "count": 0x2B0, + "pointers_addr": 1, + "pointers_bank": 0x1C, + "banks_addr": 0x741, + "banks_bank": 0x1C, + }) + + +class Entities(PointerTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x320, + "pointers_addr": 0, + "pointers_bank": 0x16, + "data_bank": 0x16, + }) + +class RoomsTable(PointerTable): + HEADER = 2 + + def _readData(self, rom, bank_nr, pointer): + bank = rom.banks[bank_nr] + start = pointer + pointer += self.HEADER + while bank[pointer] != 0xFE: + obj_type = (bank[pointer] & 0xF0) + if obj_type == 0xE0: + pointer += 5 + elif obj_type == 0xC0 or obj_type == 0x80: + pointer += 3 + else: + pointer += 2 + pointer += 1 + self._addStorage(bank_nr, start, pointer) + return bank[start:pointer] + + +class RoomsOverworldTop(RoomsTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x080, + "pointers_addr": 0x000, + "pointers_bank": 0x09, + "data_bank": 0x09, + "alt_pointers": { + "Alt06": (0x00, 0x31FD), + "Alt0E": (0x00, 0x31CD), + "Alt1B": (0x00, 0x320D), + "Alt2B": (0x00, 0x321D), + "Alt79": (0x00, 0x31ED), + } + }) + + +class RoomsOverworldBottom(RoomsTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x080, + "pointers_addr": 0x100, + "pointers_bank": 0x09, + "data_bank": 0x1A, + "alt_pointers": { + "Alt8C": (0x00, 0x31DD), + } + }) + + +class RoomsIndoorA(RoomsTable): + # TODO: The color dungeon tables are in the same bank, but the pointer table is after the room data. + def __init__(self, rom): + super().__init__(rom, { + "count": 0x100, + "pointers_addr": 0x000, + "pointers_bank": 0x0A, + "data_bank": 0x0A, + "alt_pointers": { + "Alt1F5": (0x00, 0x31A1), + } + }) + + +class RoomsIndoorB(RoomsTable): + # Most likely, this table can be expanded all the way to the end of the bank, + # giving a few 100 extra bytes to work with. + def __init__(self, rom): + super().__init__(rom, { + "count": 0x0FF, + "pointers_addr": 0x000, + "pointers_bank": 0x0B, + "data_bank": 0x0B, + }) + + +class RoomsColorDungeon(RoomsTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x016, + "pointers_addr": 0x3B77, + "pointers_bank": 0x0A, + "data_bank": 0x0A, + "expand_to_end_of_bank": True + }) + + +class BackgroundTable(PointerTable): + def _readData(self, rom, bank_nr, pointer): + bank = rom.banks[bank_nr] + start = pointer + while bank[pointer] != 0x00: + addr = bank[pointer] << 8 | bank[pointer + 1] + amount = (bank[pointer + 2] & 0x3F) + 1 + repeat = (bank[pointer + 2] & 0x40) == 0x40 + vertical = (bank[pointer + 2] & 0x80) == 0x80 + pointer += 3 + if not repeat: + pointer += amount + if repeat: + pointer += 1 + pointer += 1 + self._addStorage(bank_nr, start, pointer) + return bank[start:pointer] + + +class BackgroundTilesTable(BackgroundTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x26, + "pointers_addr": 0x052B, + "pointers_bank": 0x20, + "data_bank": 0x08, + "expand_to_end_of_bank": True + }) + + +class BackgroundAttributeTable(BackgroundTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x26, + "pointers_addr": 0x1C4B, + "pointers_bank": 0x24, + "data_bank": 0x24, + "expand_to_end_of_bank": True + }) + + +class OverworldRoomSpriteData(PointerTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x100, + "pointers_addr": 0x30D3, + "pointers_bank": 0x20, + "data_bank": 0x20, + "data_addr": 0x33F3, + "data_size": 4, + "claim_storage_gaps": True, + }) + + +class IndoorRoomSpriteData(PointerTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x220, + "pointers_addr": 0x31D3, + "pointers_bank": 0x20, + "data_bank": 0x20, + "data_addr": 0x363B, + "data_size": 4, + "claim_storage_gaps": True, + }) + + +class ROMWithTables(ROM): + def __init__(self, filename): + super().__init__(filename) + + # Ability to patch any text in the game with different text + self.texts = Texts(self) + # Ability to modify rooms + self.entities = Entities(self) + self.rooms_overworld_top = RoomsOverworldTop(self) + self.rooms_overworld_bottom = RoomsOverworldBottom(self) + self.rooms_indoor_a = RoomsIndoorA(self) + self.rooms_indoor_b = RoomsIndoorB(self) + self.rooms_color_dungeon = RoomsColorDungeon(self) + self.room_sprite_data_overworld = OverworldRoomSpriteData(self) + self.room_sprite_data_indoor = IndoorRoomSpriteData(self) + + # Backgrounds for things like the title screen. + self.background_tiles = BackgroundTilesTable(self) + self.background_attributes = BackgroundAttributeTable(self) + + self.itemNames = {} + + def save(self, filename, *, name=None): + self.texts.store(self) + self.entities.store(self) + self.rooms_overworld_top.store(self) + self.rooms_overworld_bottom.store(self) + self.rooms_indoor_a.store(self) + self.rooms_indoor_b.store(self) + self.rooms_color_dungeon.store(self) + leftover_storage = self.room_sprite_data_overworld.store(self) + self.room_sprite_data_indoor.addStorage(leftover_storage) + self.patch(0x00, 0x0DFA, ASM("ld hl, $763B"), ASM("ld hl, $%04x" % (leftover_storage[0]["start"] | 0x4000))) + self.room_sprite_data_indoor.adjustDataStart(leftover_storage[0]["start"]) + self.room_sprite_data_indoor.store(self) + self.background_tiles.store(self) + self.background_attributes.store(self) + super().save(filename, name=name) diff --git a/worlds/ladx/LADXR/roomEditor.py b/worlds/ladx/LADXR/roomEditor.py new file mode 100644 index 000000000000..c6cc63136359 --- /dev/null +++ b/worlds/ladx/LADXR/roomEditor.py @@ -0,0 +1,584 @@ +import json +from . import entityData + + +WARP_TYPE_IDS = {0xE1, 0xE2, 0xE3, 0xBA, 0xA8, 0xBE, 0xCB, 0xC2, 0xC6} +ALT_ROOM_OVERLAYS = {"Alt06": 0x1040, "Alt0E": 0x1090, "Alt1B": 0x10E0, "Alt2B": 0x1130, "Alt79": 0x1180, "Alt8C": 0x11D0} + + +class RoomEditor: + def __init__(self, rom, room=None): + assert room is not None + self.room = room + self.entities = [] + self.objects = [] + self.tileset_index = None + self.palette_index = None + self.attribset = None + + if isinstance(room, int): + entities_raw = rom.entities[room] + idx = 0 + while entities_raw[idx] != 0xFF: + x = entities_raw[idx] & 0x0F + y = entities_raw[idx] >> 4 + id = entities_raw[idx + 1] + self.entities.append((x, y, id)) + idx += 2 + assert idx == len(entities_raw) - 1 + + if isinstance(room, str): + if room in rom.rooms_overworld_top: + objects_raw = rom.rooms_overworld_top[room] + elif room in rom.rooms_overworld_bottom: + objects_raw = rom.rooms_overworld_bottom[room] + elif room in rom.rooms_indoor_a: + objects_raw = rom.rooms_indoor_a[room] + else: + assert False, "Failed to find alt room: %s" % (room) + else: + if room < 0x080: + objects_raw = rom.rooms_overworld_top[room] + elif room < 0x100: + objects_raw = rom.rooms_overworld_bottom[room - 0x80] + elif room < 0x200: + objects_raw = rom.rooms_indoor_a[room - 0x100] + elif room < 0x300: + objects_raw = rom.rooms_indoor_b[room - 0x200] + else: + objects_raw = rom.rooms_color_dungeon[room - 0x300] + + self.animation_id = objects_raw[0] + self.floor_object = objects_raw[1] + idx = 2 + while objects_raw[idx] != 0xFE: + x = objects_raw[idx] & 0x0F + y = objects_raw[idx] >> 4 + if y == 0x08: # horizontal + count = x + x = objects_raw[idx + 1] & 0x0F + y = objects_raw[idx + 1] >> 4 + self.objects.append(ObjectHorizontal(x, y, objects_raw[idx + 2], count)) + idx += 3 + elif y == 0x0C: # vertical + count = x + x = objects_raw[idx + 1] & 0x0F + y = objects_raw[idx + 1] >> 4 + self.objects.append(ObjectVertical(x, y, objects_raw[idx + 2], count)) + idx += 3 + elif y == 0x0E: # warp + self.objects.append(ObjectWarp(objects_raw[idx] & 0x0F, objects_raw[idx + 1], objects_raw[idx + 2], objects_raw[idx + 3], objects_raw[idx + 4])) + idx += 5 + else: + self.objects.append(Object(x, y, objects_raw[idx + 1])) + idx += 2 + if room is not None: + assert idx == len(objects_raw) - 1 + + if isinstance(room, int) and room < 0x0CC: + self.overlay = rom.banks[0x26][room * 80:room * 80+80] + elif isinstance(room, int) and room < 0x100: + self.overlay = rom.banks[0x27][(room - 0xCC) * 80:(room - 0xCC) * 80 + 80] + elif room in ALT_ROOM_OVERLAYS: + self.overlay = rom.banks[0x27][ALT_ROOM_OVERLAYS[room]:ALT_ROOM_OVERLAYS[room] + 80] + else: + self.overlay = None + + def store(self, rom, new_room_nr=None): + if new_room_nr is None: + new_room_nr = self.room + objects_raw = bytearray([self.animation_id, self.floor_object]) + for obj in self.objects: + objects_raw += obj.export() + objects_raw += bytearray([0xFE]) + + if isinstance(new_room_nr, str): + if new_room_nr in rom.rooms_overworld_top: + rom.rooms_overworld_top[new_room_nr] = objects_raw + elif new_room_nr in rom.rooms_overworld_bottom: + rom.rooms_overworld_bottom[new_room_nr] = objects_raw + elif new_room_nr in rom.rooms_indoor_a: + rom.rooms_indoor_a[new_room_nr] = objects_raw + else: + assert False, "Failed to find alt room: %s" % (new_room_nr) + elif new_room_nr < 0x080: + rom.rooms_overworld_top[new_room_nr] = objects_raw + elif new_room_nr < 0x100: + rom.rooms_overworld_bottom[new_room_nr - 0x80] = objects_raw + elif new_room_nr < 0x200: + rom.rooms_indoor_a[new_room_nr - 0x100] = objects_raw + elif new_room_nr < 0x300: + rom.rooms_indoor_b[new_room_nr - 0x200] = objects_raw + else: + rom.rooms_color_dungeon[new_room_nr - 0x300] = objects_raw + + if isinstance(new_room_nr, int) and new_room_nr < 0x100: + if self.tileset_index is not None: + rom.banks[0x3F][0x3F00 + new_room_nr] = self.tileset_index & 0xFF + if self.attribset is not None: + # With a tileset, comes metatile gbc data that we need to store a proper bank+pointer. + rom.banks[0x1A][0x2476 + new_room_nr] = self.attribset[0] + rom.banks[0x1A][0x1E76 + new_room_nr*2] = self.attribset[1] & 0xFF + rom.banks[0x1A][0x1E76 + new_room_nr*2+1] = self.attribset[1] >> 8 + if self.palette_index is not None: + rom.banks[0x21][0x02ef + new_room_nr] = self.palette_index + + if isinstance(new_room_nr, int): + entities_raw = bytearray() + for entity in self.entities: + entities_raw += bytearray([entity[0] | entity[1] << 4, entity[2]]) + entities_raw += bytearray([0xFF]) + rom.entities[new_room_nr] = entities_raw + + if new_room_nr < 0x0CC: + rom.banks[0x26][new_room_nr * 80:new_room_nr * 80 + 80] = self.overlay + elif new_room_nr < 0x100: + rom.banks[0x27][(new_room_nr - 0xCC) * 80:(new_room_nr - 0xCC) * 80 + 80] = self.overlay + elif new_room_nr in ALT_ROOM_OVERLAYS: + rom.banks[0x27][ALT_ROOM_OVERLAYS[new_room_nr]:ALT_ROOM_OVERLAYS[new_room_nr] + 80] = self.overlay + + def addEntity(self, x, y, type_id): + self.entities.append((x, y, type_id)) + + def removeEntities(self, type_id): + self.entities = list(filter(lambda e: e[2] != type_id, self.entities)) + + def hasEntity(self, type_id): + return any(map(lambda e: e[2] == type_id, self.entities)) + + def changeObject(self, x, y, new_type): + for obj in self.objects: + if obj.x == x and obj.y == y: + obj.type_id = new_type + if self.overlay is not None: + self.overlay[x + y * 10] = new_type + + def removeObject(self, x, y): + self.objects = list(filter(lambda obj: obj.x != x or obj.y != y, self.objects)) + + def moveObject(self, x, y, new_x, new_y): + for obj in self.objects: + if obj.x == x and obj.y == y: + if self.overlay is not None: + self.overlay[x + y * 10] = self.floor_object + self.overlay[new_x + new_y * 10] = obj.type_id + obj.x = new_x + obj.y = new_y + + def getWarps(self): + return list(filter(lambda obj: isinstance(obj, ObjectWarp), self.objects)) + + def updateOverlay(self, preserve_floor=False): + if self.overlay is None: + return + if not preserve_floor: + for n in range(80): + self.overlay[n] = self.floor_object + for obj in self.objects: + if isinstance(obj, ObjectHorizontal): + for n in range(obj.count): + self.overlay[obj.x + n + obj.y * 10] = obj.type_id + elif isinstance(obj, ObjectVertical): + for n in range(obj.count): + self.overlay[obj.x + n * 10 + obj.y * 10] = obj.type_id + elif not isinstance(obj, ObjectWarp): + self.overlay[obj.x + obj.y * 10] = obj.type_id + + def loadFromJson(self, filename): + self.objects = [] + self.entities = [] + self.animation_id = 0 + self.tileset_index = 0x0F + self.palette_index = 0x01 + + data = json.load(open(filename)) + + for prop in data.get("properties", []): + if prop["name"] == "palette": + self.palette_index = int(prop["value"], 16) + elif prop["name"] == "tileset": + self.tileset_index = int(prop["value"], 16) + elif prop["name"] == "animationset": + self.animation_id = int(prop["value"], 16) + elif prop["name"] == "attribset": + bank, _, addr = prop["value"].partition(":") + self.attribset = (int(bank, 16), int(addr, 16) + 0x4000) + + tiles = [0] * 80 + for layer in data["layers"]: + if "data" in layer: + for n in range(80): + if layer["data"][n] > 0: + tiles[n] = (layer["data"][n] - 1) & 0xFF + if "objects" in layer: + for obj in layer["objects"]: + x = int((obj["x"] + obj["width"] / 2) // 16) + y = int((obj["y"] + obj["height"] / 2) // 16) + if obj["type"] == "warp": + warp_type, map_nr, room, x, y = obj["name"].split(":") + self.objects.append(ObjectWarp(int(warp_type), int(map_nr, 16), int(room, 16) & 0xFF, int(x, 16), int(y, 16))) + elif obj["type"] == "entity": + type_id = entityData.NAME.index(obj["name"]) + self.addEntity(x, y, type_id) + elif obj["type"] == "hidden_tile": + self.objects.append(Object(x, y, int(obj["name"], 16))) + self.buildObjectList(tiles, reduce_size=True) + return data + + def getTileArray(self): + if self.room < 0x100: + tiles = [self.floor_object] * 80 + else: + tiles = [self.floor_object & 0x0F] * 80 + def objHSize(type_id): + if type_id == 0xF5: + return 2 + return 1 + def objVSize(type_id): + if type_id == 0xF5: + return 2 + return 1 + def getObject(x, y): + x, y = (x & 15), (y & 15) + if x < 10 and y < 8: + return tiles[x + y * 10] + return 0 + if self.room < 0x100: + def placeObject(x, y, type_id): + if type_id == 0xF5: + if getObject(x, y) in (0x1B, 0x28, 0x29, 0x83, 0x90): + placeObject(x, y, 0x29) + else: + placeObject(x, y, 0x25) + if getObject(x + 1, y) in (0x1B, 0x27, 0x82, 0x86, 0x8A, 0x90, 0x2A): + placeObject(x + 1, y, 0x2A) + else: + placeObject(x + 1, y, 0x26) + if getObject(x, y + 1) in (0x26, 0x2A): + placeObject(x, y + 1, 0x2A) + elif getObject(x, y + 1) == 0x90: + placeObject(x, y + 1, 0x82) + else: + placeObject(x, y + 1, 0x27) + if getObject(x + 1, y + 1) in (0x25, 0x29): + placeObject(x + 1, y + 1, 0x29) + elif getObject(x + 1, y + 1) == 0x90: + placeObject(x + 1, y + 1, 0x83) + else: + placeObject(x + 1, y + 1, 0x28) + elif type_id == 0xF6: # two door house + placeObject(x + 0, y, 0x55) + placeObject(x + 1, y, 0x5A) + placeObject(x + 2, y, 0x5A) + placeObject(x + 3, y, 0x5A) + placeObject(x + 4, y, 0x56) + placeObject(x + 0, y + 1, 0x57) + placeObject(x + 1, y + 1, 0x59) + placeObject(x + 2, y + 1, 0x59) + placeObject(x + 3, y + 1, 0x59) + placeObject(x + 4, y + 1, 0x58) + placeObject(x + 0, y + 2, 0x5B) + placeObject(x + 1, y + 2, 0xE2) + placeObject(x + 2, y + 2, 0x5B) + placeObject(x + 3, y + 2, 0xE2) + placeObject(x + 4, y + 2, 0x5B) + elif type_id == 0xF7: # large house + placeObject(x + 0, y, 0x55) + placeObject(x + 1, y, 0x5A) + placeObject(x + 2, y, 0x56) + placeObject(x + 0, y + 1, 0x57) + placeObject(x + 1, y + 1, 0x59) + placeObject(x + 2, y + 1, 0x58) + placeObject(x + 0, y + 2, 0x5B) + placeObject(x + 1, y + 2, 0xE2) + placeObject(x + 2, y + 2, 0x5B) + elif type_id == 0xF8: # catfish + placeObject(x + 0, y, 0xB6) + placeObject(x + 1, y, 0xB7) + placeObject(x + 2, y, 0x66) + placeObject(x + 0, y + 1, 0x67) + placeObject(x + 1, y + 1, 0xE3) + placeObject(x + 2, y + 1, 0x68) + elif type_id == 0xF9: # palace door + placeObject(x + 0, y, 0xA4) + placeObject(x + 1, y, 0xA5) + placeObject(x + 2, y, 0xA6) + placeObject(x + 0, y + 1, 0xA7) + placeObject(x + 1, y + 1, 0xE3) + placeObject(x + 2, y + 1, 0xA8) + elif type_id == 0xFA: # stone pig head + placeObject(x + 0, y, 0xBB) + placeObject(x + 1, y, 0xBC) + placeObject(x + 0, y + 1, 0xBD) + placeObject(x + 1, y + 1, 0xBE) + elif type_id == 0xFB: # palmtree + if x == 15: + placeObject(x + 1, y + 1, 0xB7) + placeObject(x + 1, y + 2, 0xCE) + else: + placeObject(x + 0, y, 0xB6) + placeObject(x + 0, y + 1, 0xCD) + placeObject(x + 1, y + 0, 0xB7) + placeObject(x + 1, y + 1, 0xCE) + elif type_id == 0xFC: # square "hill with hole" (seen near lvl4 entrance) + placeObject(x + 0, y, 0x2B) + placeObject(x + 1, y, 0x2C) + placeObject(x + 2, y, 0x2D) + placeObject(x + 0, y + 1, 0x37) + placeObject(x + 1, y + 1, 0xE8) + placeObject(x + 2, y + 1, 0x38) + placeObject(x - 1, y + 2, 0x0A) + placeObject(x + 0, y + 2, 0x33) + placeObject(x + 1, y + 2, 0x2F) + placeObject(x + 2, y + 2, 0x34) + placeObject(x + 0, y + 3, 0x0A) + placeObject(x + 1, y + 3, 0x0A) + placeObject(x + 2, y + 3, 0x0A) + placeObject(x + 3, y + 3, 0x0A) + elif type_id == 0xFD: # small house + placeObject(x + 0, y, 0x52) + placeObject(x + 1, y, 0x52) + placeObject(x + 2, y, 0x52) + placeObject(x + 0, y + 1, 0x5B) + placeObject(x + 1, y + 1, 0xE2) + placeObject(x + 2, y + 1, 0x5B) + else: + x, y = (x & 15), (y & 15) + if x < 10 and y < 8: + tiles[x + y * 10] = type_id + else: + def placeObject(x, y, type_id): + x, y = (x & 15), (y & 15) + if type_id == 0xEC: # key door + placeObject(x, y, 0x2D) + placeObject(x + 1, y, 0x2E) + elif type_id == 0xED: + placeObject(x, y, 0x2F) + placeObject(x + 1, y, 0x30) + elif type_id == 0xEE: + placeObject(x, y, 0x31) + placeObject(x, y + 1, 0x32) + elif type_id == 0xEF: + placeObject(x, y, 0x33) + placeObject(x, y + 1, 0x34) + elif type_id == 0xF0: # closed door + placeObject(x, y, 0x35) + placeObject(x + 1, y, 0x36) + elif type_id == 0xF1: + placeObject(x, y, 0x37) + placeObject(x + 1, y, 0x38) + elif type_id == 0xF2: + placeObject(x, y, 0x39) + placeObject(x, y + 1, 0x3A) + elif type_id == 0xF3: + placeObject(x, y, 0x3B) + placeObject(x, y + 1, 0x3C) + elif type_id == 0xF4: # open door + placeObject(x, y, 0x43) + placeObject(x + 1, y, 0x44) + elif type_id == 0xF5: + placeObject(x, y, 0x8C) + placeObject(x + 1, y, 0x08) + elif type_id == 0xF6: + placeObject(x, y, 0x09) + placeObject(x, y + 1, 0x0A) + elif type_id == 0xF7: + placeObject(x, y, 0x0B) + placeObject(x, y + 1, 0x0C) + elif type_id == 0xF8: # boss door + placeObject(x, y, 0xA4) + placeObject(x + 1, y, 0xA5) + elif type_id == 0xF9: # stairs door + placeObject(x, y, 0xAF) + placeObject(x + 1, y, 0xB0) + elif type_id == 0xFA: # flipwall + placeObject(x, y, 0xB1) + placeObject(x + 1, y, 0xB2) + elif type_id == 0xFB: # one way arrow + placeObject(x, y, 0x45) + placeObject(x + 1, y, 0x46) + elif type_id == 0xFC: # entrance + placeObject(x + 0, y, 0xB3) + placeObject(x + 1, y, 0xB4) + placeObject(x + 2, y, 0xB4) + placeObject(x + 3, y, 0xB5) + placeObject(x + 0, y + 1, 0xB6) + placeObject(x + 1, y + 1, 0xB7) + placeObject(x + 2, y + 1, 0xB8) + placeObject(x + 3, y + 1, 0xB9) + placeObject(x + 0, y + 2, 0xBA) + placeObject(x + 1, y + 2, 0xBB) + placeObject(x + 2, y + 2, 0xBC) + placeObject(x + 3, y + 2, 0xBD) + elif type_id == 0xFD: # entrance + placeObject(x, y, 0xC1) + placeObject(x + 1, y, 0xC2) + else: + if x < 10 and y < 8: + tiles[x + y * 10] = type_id + + def addWalls(flags): + for x in range(0, 10): + if flags & 0b0010: + placeObject(x, 0, 0x21) + if flags & 0b0001: + placeObject(x, 7, 0x22) + for y in range(0, 8): + if flags & 0b1000: + placeObject(0, y, 0x23) + if flags & 0b0100: + placeObject(9, y, 0x24) + if flags & 0b1000 and flags & 0b0010: + placeObject(0, 0, 0x25) + if flags & 0b0100 and flags & 0b0010: + placeObject(9, 0, 0x26) + if flags & 0b1000 and flags & 0b0001: + placeObject(0, 7, 0x27) + if flags & 0b0100 and flags & 0b0001: + placeObject(9, 7, 0x28) + + if self.floor_object & 0xF0 == 0x00: + addWalls(0b1111) + if self.floor_object & 0xF0 == 0x10: + addWalls(0b1101) + if self.floor_object & 0xF0 == 0x20: + addWalls(0b1011) + if self.floor_object & 0xF0 == 0x30: + addWalls(0b1110) + if self.floor_object & 0xF0 == 0x40: + addWalls(0b0111) + if self.floor_object & 0xF0 == 0x50: + addWalls(0b1001) + if self.floor_object & 0xF0 == 0x60: + addWalls(0b0101) + if self.floor_object & 0xF0 == 0x70: + addWalls(0b0110) + if self.floor_object & 0xF0 == 0x80: + addWalls(0b1010) + for obj in self.objects: + if isinstance(obj, ObjectWarp): + pass + elif isinstance(obj, ObjectHorizontal): + for n in range(0, obj.count): + placeObject(obj.x + n * objHSize(obj.type_id), obj.y, obj.type_id) + elif isinstance(obj, ObjectVertical): + for n in range(0, obj.count): + placeObject(obj.x, obj.y + n * objVSize(obj.type_id), obj.type_id) + else: + placeObject(obj.x, obj.y, obj.type_id) + return tiles + + def buildObjectList(self, tiles, *, reduce_size=False): + self.objects = [obj for obj in self.objects if isinstance(obj, ObjectWarp)] + tiles = tiles.copy() + if self.overlay: + for n in range(80): + self.overlay[n] = tiles[n] + if reduce_size: + if tiles[n] in {0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, + 0x33, 0x34, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, + 0x48, 0x49, 0x4B, 0x4C, 0x4E, + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F}: + tiles[n] = 0x3A # Solid tiles + if tiles[n] in {0x08, 0x09, 0x0C, 0x44, + 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF}: + tiles[n] = 0x04 # Open tiles + + is_overworld = isinstance(self.room, str) or self.room < 0x100 + counts = {} + for n in tiles: + if n < 0x0F or is_overworld: + counts[n] = counts.get(n, 0) + 1 + self.floor_object = max(counts, key=counts.get) + for y in range(8) if is_overworld else range(1, 7): + for x in range(10) if is_overworld else range(1, 9): + if tiles[x + y * 10] == self.floor_object: + tiles[x + y * 10] = -1 + for y in range(8): + for x in range(10): + obj = tiles[x + y * 10] + if obj == -1: + continue + w = 1 + h = 1 + while x + w < 10 and tiles[x + w + y * 10] == obj: + w += 1 + while y + h < 8 and tiles[x + (y + h) * 10] == obj: + h += 1 + if obj in {0xE1, 0xE2, 0xE3, 0xBA, 0xC6}: # Entrances should never be horizontal/vertical lists + w = 1 + h = 1 + if w > h: + for n in range(w): + tiles[x + n + y * 10] = -1 + self.objects.append(ObjectHorizontal(x, y, obj, w)) + elif h > 1: + for n in range(h): + tiles[x + (y + n) * 10] = -1 + self.objects.append(ObjectVertical(x, y, obj, h)) + else: + self.objects.append(Object(x, y, obj)) + + +class Object: + def __init__(self, x, y, type_id): + self.x = x + self.y = y + self.type_id = type_id + + def export(self): + return bytearray([self.x | (self.y << 4), self.type_id]) + + def __repr__(self): + return "%s:%d,%d:%02X" % (self.__class__.__name__, self.x, self.y, self.type_id) + + +class ObjectHorizontal(Object): + def __init__(self, x, y, type_id, count): + super().__init__(x, y, type_id) + self.count = count + + def export(self): + return bytearray([0x80 | self.count, self.x | (self.y << 4), self.type_id]) + + def __repr__(self): + return "%s:%d,%d:%02Xx%d" % (self.__class__.__name__, self.x, self.y, self.type_id, self.count) + + +class ObjectVertical(Object): + def __init__(self, x, y, type_id, count): + super().__init__(x, y, type_id) + self.count = count + + def export(self): + return bytearray([0xC0 | self.count, self.x | (self.y << 4), self.type_id]) + + def __repr__(self): + return "%s:%d,%d:%02Xx%d" % (self.__class__.__name__, self.x, self.y, self.type_id, self.count) + + +class ObjectWarp(Object): + def __init__(self, warp_type, map_nr, room_nr, target_x, target_y): + super().__init__(None, None, None) + if warp_type > 0: + # indoor map + if map_nr == 0xff: + room_nr += 0x300 # color dungeon + elif 0x06 <= map_nr < 0x1A: + room_nr += 0x200 # indoor B + else: + room_nr += 0x100 # indoor A + self.warp_type = warp_type + self.room = room_nr + self.map_nr = map_nr + self.target_x = target_x + self.target_y = target_y + + def export(self): + return bytearray([0xE0 | self.warp_type, self.map_nr, self.room & 0xFF, self.target_x, self.target_y]) + + def copy(self): + return ObjectWarp(self.warp_type, self.map_nr, self.room & 0xFF, self.target_x, self.target_y) + + def __repr__(self): + return "%s:%d:%03x:%02x:%d,%d" % (self.__class__.__name__, self.warp_type, self.room, self.map_nr, self.target_x, self.target_y) diff --git a/worlds/ladx/LADXR/settings.py b/worlds/ladx/LADXR/settings.py new file mode 100644 index 000000000000..d52e8fe45d49 --- /dev/null +++ b/worlds/ladx/LADXR/settings.py @@ -0,0 +1,321 @@ +from typing import List, Tuple, Optional, Union +import os + + +class Setting: + def __init__(self, key: str, + category: str, short_key: str, label: str, *, + description: str, multiworld: bool = True, aesthetic: bool = False, options: Optional[List[Tuple[str, str, str]]] = None, + default: Optional[Union[bool, float, str]] = None, placeholder: Optional[str] = None): + if options: + assert default in [option_key for option_key, option_short, option_label in options], f"{default} not in {options}" + short_options = set() + for option_key, option_short, option_label in options: + assert option_short != "" or option_key == default, f"No short option for non default {label}:{option_key}" + assert option_short not in short_options, "Duplicate short option value..." + short_options.add(option_short) + + self.key = key + self.category = category + self.short_key = short_key + self.label = label + self.description = description + self.multiworld = multiworld + self.aesthetic = aesthetic + self.options = options + self.default = default + self.placeholder = placeholder + + self.value = default + + def set(self, value): + if isinstance(self.default, bool): + if not isinstance(value, bool): + value = bool(int(value)) + elif not isinstance(value, type(self.default)): + try: + value = type(self.default)(value) + except ValueError: + raise ValueError(f"{value} is not an accepted value for {self.key} setting") + if self.options: + if value not in [k for k, s, v in self.options]: + raise ValueError(f"{value} is not an accepted value for {self.key} setting") + self.value = value + + def getShortValue(self): + if self.options: + for option_key, option_short, option_label in self.options: + if self.value == option_key: + return option_short + return self.value + ">" + + def toJson(self): + result = { + "key": self.key, + "category": self.category, + "short_key": self.short_key, + "label": self.label, + "description": self.description, + "multiworld": self.multiworld, + "aesthetic": self.aesthetic, + "default": self.default, + } + if self.options: + result["options"] = [{"key": option_key, "short": option_short, "label": option_label} for option_key, option_short, option_label in self.options] + if self.placeholder: + result["placeholder"] = self.placeholder + return result + + +class Settings: + def __init__(self, ap_options): + gfx_options = [('', '', 'Default')] + gfx_path = os.path.join("data", "sprites", "ladx") + for filename in sorted(os.listdir(gfx_path)): + if filename.endswith(".bin") or filename.endswith(".png") or filename.endswith(".bmp"): + gfx_options.append((filename, filename + ">", filename[:-4])) + if filename.endswith(".bdiff"): + gfx_options.append((filename, filename + ">", filename[:-6])) + + + self.__all = [ + Setting('seed', 'Main', '<', 'Seed', placeholder='Leave empty for random seed', default="", multiworld=False, + description="""For multiple people to generate the same randomization result, enter the generated seed number here. +Note, not all strings are valid seeds."""), + Setting('logic', 'Main', 'L', 'Logic', options=[('casual', 'c', 'Casual'), ('normal', 'n', 'Normal'), ('hard', 'h', 'Hard'), ('glitched', 'g', 'Glitched'), ('hell', 'H', 'Hell')], default='normal', + description="""Affects where items are allowed to be placed. +[Casual] Same as normal, except that a few more complex options are removed, like removing bushes with powder and killing enemies with powder or bombs. +[Normal] playable without using any tricks or glitches. Requires nothing to be done outside of normal item usage. +[Hard] More advanced techniques may be required, but glitches are not. Examples include tricky jumps, killing enemies with only pots and skipping keys with smart routing. +[Glitched] Advanced glitches and techniques may be required, but extremely difficult or tedious tricks are not required. Examples include Bomb Triggers, Super Jumps and Jesus Jumps. +[Hell] Obscure and hard techniques may be required. Examples include featherless jumping with boots and/or hookshot, sequential pit buffers and unclipped superjumps. Things in here can be extremely hard to do or very time consuming. Only insane people go for this."""), + Setting('forwardfactor', 'Main', 'F', 'Forward Factor', default=0.0, + description="Forward item weight adjustment factor, lower values generate more rear heavy seeds while higher values generate front heavy seeds. Default is 0.5."), + Setting('accessibility', 'Main', 'A', 'Accessibility', options=[('all', 'a', '100% Locations'), ('goal', 'g', 'Beatable')], default='all', + description=""" +[100% Locations] guaranteed that every single item can be reached and gained. +[Beatable] only guarantees that the game is beatable. Certain items/chests might never be reachable."""), + Setting('race', 'Main', 'V', 'Race mode', default=False, multiworld=False, + description=""" +Spoiler logs can not be generated for ROMs generated with race mode enabled, and seed generation is slightly different."""), +# Setting('spoilerformat', 'Main', 'Spoiler Format', options=[('none', 'None'), ('text', 'Text'), ('json', 'JSON')], default='none', multiworld=False, +# description="""Affects how the spoiler log is generated. +# [None] No spoiler log is generated. One can still be manually dumped later. +# [Text] Creates a .txt file meant for a human to read. +# [JSON] Creates a .json file with a little more information and meant for a computer to read.""") + Setting('heartpiece', 'Items', 'h', 'Randomize heart pieces', default=True, + description='Includes heart pieces in the item pool'), + Setting('seashells', 'Items', 's', 'Randomize hidden seashells', default=True, + description='Randomizes the secret sea shells hiding in the ground/trees. (chest are always randomized)'), + Setting('heartcontainers', 'Items', 'H', 'Randomize heart containers', default=True, + description='Includes boss heart container drops in the item pool'), + Setting('instruments', 'Items', 'I', 'Randomize instruments', default=False, + description='Instruments are placed on random locations, dungeon goal will just contain a random item.'), + Setting('tradequest', 'Items', 'T', 'Randomize trade quest', default=True, + description='Trade quest items are randomized, each NPC takes its normal trade quest item, but gives a random item'), + Setting('witch', 'Items', 'W', 'Randomize item given by the witch', default=True, + description='Adds both the toadstool and the reward for giving the toadstool to the witch to the item pool'), + Setting('rooster', 'Items', 'R', 'Add the rooster', default=True, + description='Adds the rooster to the item pool. Without this option, the rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means.'), + Setting('boomerang', 'Items', 'Z', 'Boomerang trade', options=[('default', 'd', 'Normal'), ('trade', 't', 'Trade'), ('gift', 'g', 'Gift')], default='gift', + description=""" +[Normal], requires magnifier to get the boomerang. +[Trade], allows to trade an inventory item for a random other inventory item boomerang is shuffled. +[Gift], You get a random gift of any item, and the boomerang is shuffled."""), + Setting('randomstartlocation', 'Gameplay', 'r', 'Random start location', default=False, + description='Randomize where your starting house is located'), + Setting('dungeonshuffle', 'Gameplay', 'u', 'Dungeon shuffle', default=False, + description='Randomizes the dungeon that each dungeon entrance leads to'), + Setting('entranceshuffle', 'Gameplay', 'E', 'Entrance randomizer', options=[("none", '', "Default"), ("simple", 's', "Simple"), ("advanced", 'a', "Advanced"), ("expert", 'E', "Expert"), ("insanity", 'I', "Insanity")], default='none', + description="""Randomizes where overworld entrances lead to. +[Simple] single entrance caves that contain items are randomized +[Advanced] Connector caves are also randomized +[Expert] Caves/houses without items are also randomized +[Insanity] A few very annoying entrances will be randomized as well. +If random start location and/or dungeon shuffle is enabled, then these will be shuffled with all the entrances. +Note, some entrances can lead into water, use the warp-to-home from the save&quit menu to escape this."""), + Setting('boss', 'Gameplay', 'B', 'Boss shuffle', options=[('default', '', 'Normal'), ('shuffle', 's', 'Shuffle'), ('random', 'r', 'Randomize')], default='default', + description='Randomizes the dungeon bosses that each dungeon has'), + Setting('miniboss', 'Gameplay', 'b', 'Miniboss shuffle', options=[('default', '', 'Normal'), ('shuffle', 's', 'Shuffle'), ('random', 'r', 'Randomize')], default='default', + description='Randomizes the dungeon minibosses that each dungeon has'), + Setting('goal', 'Gameplay', 'G', 'Goal', options=[('8', '8', '8 instruments'), ('7', '7', '7 instruments'), ('6', '6', '6 instruments'), + ('5', '5', '5 instruments'), ('4', '4', '4 instruments'), ('3', '3', '3 instruments'), + ('2', '2', '2 instruments'), ('1', '1', '1 instrument'), ('0', '0', 'No instruments'), + ('open', 'O', 'Egg already open'), ('random', 'R', 'Random instrument count'), + ('open-4', '<', 'Random short game (0-4)'), ('5-8', '>', 'Random long game (5-8)'), + ('seashells', 'S', 'Seashell hunt (20)'), ('bingo', 'b', 'Bingo!'), + ('bingo-full', 'B', 'Bingo-25!')], default='8', + description="""Changes the goal of the game. +[1-8 instruments], number of instruments required to open the egg. +[No instruments] open the egg without instruments, still requires the ocarina with the balled of the windfish +[Egg already open] the egg is already open, just head for it once you have the items needed to defeat the boss. +[Randomized instrument count] random number of instruments required to open the egg, between 0 and 8. +[Random short/long game] random number of instruments required to open the egg, chosen between 0-4 and 5-8 respectively. +[Seashell hunt] egg will open once you collected 20 seashells. Instruments are replaced by seashells and shuffled. +[Bingo] Generate a 5x5 bingo board with various goals. Complete one row/column or diagonal to win! +[Bingo-25] Bingo, but need to fill the whole bingo card to win!"""), + Setting('itempool', 'Gameplay', 'P', 'Item pool', options=[('', '', 'Normal'), ('casual', 'c', 'Casual'), ('pain', 'p', 'Path of Pain'), ('keyup', 'k', 'More keys')], default='', + description="""Effects which items are shuffled. +[Casual] places more inventory and key items so the seed is easier. +[More keys] adds more small keys and extra nightmare keys so dungeons are easier. +[Path of pain]... just find out yourself."""), + Setting('hpmode', 'Gameplay', 'm', 'Health mode', options=[('default', '', 'Normal'), ('inverted', 'i', 'Inverted'), ('1', '1', 'Start with 1 heart'), ('low', 'l', 'Low max')], default='default', + description=""" +[Normal} health works as you would expect. +[Inverted] you start with 9 heart containers, but killing a boss will take a heartcontainer instead of giving one. +[Start with 1] normal game, you just start with 1 heart instead of 3. +[Low max] replace heart containers with heart pieces."""), + Setting('hardmode', 'Gameplay', 'X', 'Hard mode', options=[('none', '', 'Disabled'), ('oracle', 'O', 'Oracle'), ('hero', 'H', 'Hero'), ('ohko', '1', 'One hit KO')], default='none', + description=""" +[Oracle] Less iframes and heath from drops. Bombs damage yourself. Water damages you without flippers. No piece of power or acorn. +[Hero] Switch version hero mode, double damage, no heart/fairy drops. +[One hit KO] You die on a single hit, always."""), + Setting('steal', 'Gameplay', 't', 'Stealing from the shop', + options=[('always', 'a', 'Always'), ('never', 'n', 'Never'), ('default', '', 'Normal')], default='default', + description="""Effects when you can steal from the shop. Stealing is bad and never in logic. +[Normal] requires the sword before you can steal. +[Always] you can always steal from the shop +[Never] you can never steal from the shop."""), + Setting('bowwow', 'Special', 'g', 'Good boy mode', options=[('normal', '', 'Disabled'), ('always', 'a', 'Enabled'), ('swordless', 's', 'Enabled (swordless)')], default='normal', + description='Allows BowWow to be taken into any area, damage bosses and more enemies. If enabled you always start with bowwow. Swordless option removes the swords from the game and requires you to beat the game without a sword and just bowwow.'), + Setting('overworld', 'Special', 'O', 'Overworld', options=[('normal', '', 'Normal'), ('dungeondive', 'D', 'Dungeon dive'), ('nodungeons', 'N', 'No dungeons'), ('random', 'R', 'Randomized')], default='normal', + description=""" +[Dungeon Dive] Create a different overworld where all the dungeons are directly accessible and almost no chests are located in the overworld. +[No dungeons] All dungeons only consist of a boss fight and a instrument reward. Rest of the dungeon is removed. +[Random] Creates a randomized overworld WARNING: This will error out often during generation, work in progress."""), + Setting('owlstatues', 'Special', 'o', 'Owl statues', options=[('', '', 'Never'), ('dungeon', 'D', 'In dungeons'), ('overworld', 'O', 'On the overworld'), ('both', 'B', 'Dungeons and Overworld')], default='', + description='Replaces the hints from owl statues with additional randomized items'), + Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False, + description='All items will be more powerful, faster, harder, bigger stronger. You name it.'), + Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none', + description='Adds that the select button swaps with either A or B. The item is swapped with the top inventory slot. The map is not available when quickswap is enabled.', + aesthetic=True), + Setting('textmode', 'User options', 'f', 'Text mode', options=[('fast', '', 'Fast'), ('default', 'd', 'Normal'), ('none', 'n', 'No-text')], default='fast', + description="""[Fast] makes text appear twice as fast. +[No-Text] removes all text from the game""", aesthetic=True), + Setting('lowhpbeep', 'User options', 'p', 'Low HP beeps', options=[('none', 'D', 'Disabled'), ('slow', 'S', 'Slow'), ('default', 'N', 'Normal')], default='slow', + description='Slows or disables the low health beeping sound', aesthetic=True), + Setting('noflash', 'User options', 'l', 'Remove flashing lights', default=True, + description='Remove the flashing light effects from Mamu, shopkeeper and MadBatter. Useful for capture cards and people that are sensitive for these things.', + aesthetic=True), + Setting('nagmessages', 'User options', 'S', 'Show nag messages', default=False, + description='Enables the nag messages normally shown when touching stones and crystals', + aesthetic=True), + Setting('gfxmod', 'User options', 'c', 'Graphics', options=gfx_options, default='', + description='Generally affects at least Link\'s sprite, but can alter any graphics in the game', + aesthetic=True), + Setting('linkspalette', 'User options', 'C', "Link's color", + options=[('-1', '-', 'Normal'), ('0', '0', 'Green'), ('1', '1', 'Yellow'), ('2', '2', 'Red'), ('3', '3', 'Blue'), + ('4', '4', '?? A'), ('5', '5', '?? B'), ('6', '6', '?? C'), ('7', '7', '?? D')], default='-1', aesthetic=True, + description="""Allows you to force a certain color on link. +[Normal] color of link depends on the tunic. +[Green/Yellow/Red/Blue] forces link into one of these colors. +[?? A/B/C/D] colors of link are usually inverted and color depends on the area you are in."""), + Setting('music', 'User options', 'M', 'Music', options=[('', '', 'Default'), ('random', 'r', 'Random'), ('off', 'o', 'Disable')], default='', + description=""" +[Random] Randomizes overworld and dungeon music' +[Disable] no music in the whole game""", + aesthetic=True), + ] + self.__by_key = {s.key: s for s in self.__all} + + # Make sure all short keys are unique + short_keys = set() + for s in self.__all: + assert s.short_key not in short_keys, s.label + short_keys.add(s.short_key) + self.ap_options = ap_options + + for option in self.ap_options.values(): + if not hasattr(option, 'to_ladxr_option'): + continue + name, value = option.to_ladxr_option(self.ap_options) + if value == "true": + value = 1 + elif value == "false": + value = 0 + + if name: + self.set( f"{name}={value}") + + def __getattr__(self, item): + return self.__by_key[item].value + + def __setattr__(self, key, value): + if not key.startswith("_") and key in self.__by_key: + self.__by_key[key].set(value) + else: + super().__setattr__(key, value) + + def loadShortString(self, value): + for setting in self.__all: + if isinstance(setting.default, bool): + setting.value = False + index = 0 + while index < len(value): + key = value[index] + index += 1 + for setting in self.__all: + if setting.short_key != key: + continue + if isinstance(setting.default, bool): + setting.value = True + elif setting.options: + for option_key, option_short, option_label in setting.options: + if option_key != setting.default and value[index:].startswith(option_short): + setting.value = option_key + index += len(option_short) + break + else: + end_of_param = value.find(">", index) + setting.value = value[index:end_of_param] + index = end_of_param + 1 + + def getShortString(self): + result = "" + for setting in self.__all: + if isinstance(setting.default, bool): + if setting.value: + result += setting.short_key + elif setting.value != setting.default: + result += setting.short_key + setting.getShortValue() + return result + + def validate(self): + def req(setting: str, value: str, message: str) -> None: + if getattr(self, setting) != value: + print("Warning: %s (setting adjusted automatically)" % message) + setattr(self, setting, value) + + def dis(setting: str, value: str, new_value: str, message: str) -> None: + if getattr(self, setting) == value: + print("Warning: %s (setting adjusted automatically)" % message) + setattr(self, setting, new_value) + + if self.goal in ("bingo", "bingo-full"): + req("overworld", "normal", "Bingo goal does not work with dungeondive") + req("accessibility", "all", "Bingo goal needs 'all' accessibility") + dis("steal", "never", "default", "With bingo goal, stealing should be allowed") + dis("boss", "random", "shuffle", "With bingo goal, bosses need to be on normal or shuffle") + dis("miniboss", "random", "shuffle", "With bingo goal, minibosses need to be on normal or shuffle") + if self.overworld == "dungeondive": + dis("goal", "seashells", "8", "Dungeon dive does not work with seashell goal") + if self.overworld == "nodungeons": + dis("goal", "seashells", "8", "No dungeons does not work with seashell goal") + if self.overworld == "random": + self.goal = "4" # Force 4 dungeon goal for random overworld right now. + + def set(self, value: str) -> None: + if "=" in value: + key, value = value.split("=", 1) + else: + key, value = value, "1" + if key not in self.__by_key: + raise ValueError(f"Setting {key} not found") + self.__by_key[key].set(value) + + def toJson(self): + return [s.toJson() for s in self.__all] + + def __iter__(self): + return iter(self.__all) diff --git a/worlds/ladx/LADXR/utils.py b/worlds/ladx/LADXR/utils.py new file mode 100644 index 000000000000..fcf1d2bb56e7 --- /dev/null +++ b/worlds/ladx/LADXR/utils.py @@ -0,0 +1,222 @@ +from typing import Optional + +from .locations.items import * + +_NAMES = { + SWORD: "Sword", + BOMB: "Bombs", + POWER_BRACELET: "Power Bracelet", + SHIELD: "Shield", + BOW: "Bow", + HOOKSHOT: "Hookshot", + MAGIC_ROD: "Magic Rod", + PEGASUS_BOOTS: "Pegasus Boots", + OCARINA: "Ocarina", + FEATHER: "Roc's Feather", + SHOVEL: "Shovel", + MAGIC_POWDER: "Magic Powder", + BOOMERANG: "Boomerang", + ROOSTER: "Flying Rooster", + + FLIPPERS: "Flippers", + SLIME_KEY: "Slime key", + TAIL_KEY: "Tail key", + ANGLER_KEY: "Angler key", + FACE_KEY: "Face key", + BIRD_KEY: "Bird key", + GOLD_LEAF: "Golden leaf", + + "RUPEE": "Rupee", + "RUPEES": "Rupees", + RUPEES_50: "50 Rupees", + RUPEES_20: "20 Rupees", + RUPEES_100: "100 Rupees", + RUPEES_200: "200 Rupees", + RUPEES_500: "500 Rupees", + SEASHELL: "Secret Seashell", + + KEY: "Small Key", + KEY1: "Key for Tail Cave", + KEY2: "Key for Bottle Grotto", + KEY3: "Key for Key Cavern", + KEY4: "Key for Angler's Tunnel", + KEY5: "Key for Catfish's Maw", + KEY6: "Key for Face Shrine", + KEY7: "Key for Eagle's Tower", + KEY8: "Key for Turtle Rock", + KEY9: "Key for Color Dungeon", + + MAP: "Dungeon Map", + MAP1: "Map for Tail Cave", + MAP2: "Map for Bottle Grotto", + MAP3: "Map for Key Cavern", + MAP4: "Map for Angler's Tunnel", + MAP5: "Map for Catfish's Maw", + MAP6: "Map for Face Shrine", + MAP7: "Map for Eagle's Tower", + MAP8: "Map for Turtle Rock", + MAP9: "Map for Color Dungeon", + + COMPASS: "Dungeon Compass", + COMPASS1: "Compass for Tail Cave", + COMPASS2: "Compass for Bottle Grotto", + COMPASS3: "Compass for Key Cavern", + COMPASS4: "Compass for Angler's Tunnel", + COMPASS5: "Compass for Catfish's Maw", + COMPASS6: "Compass for Face Shrine", + COMPASS7: "Compass for Eagle's Tower", + COMPASS8: "Compass for Turtle Rock", + COMPASS9: "Compass for Color Dungeon", + + STONE_BEAK: "Stone Beak", + STONE_BEAK1: "Stone Beak for Tail Cave", + STONE_BEAK2: "Stone Beak for Bottle Grotto", + STONE_BEAK3: "Stone Beak for Key Cavern", + STONE_BEAK4: "Stone Beak for Angler's Tunnel", + STONE_BEAK5: "Stone Beak for Catfish's Maw", + STONE_BEAK6: "Stone Beak for Face Shrine", + STONE_BEAK7: "Stone Beak for Eagle's Tower", + STONE_BEAK8: "Stone Beak for Turtle Rock", + STONE_BEAK9: "Stone Beak for Color Dungeon", + + NIGHTMARE_KEY: "Nightmare Key", + NIGHTMARE_KEY1: "Nightmare Key for Tail Cave", + NIGHTMARE_KEY2: "Nightmare Key for Bottle Grotto", + NIGHTMARE_KEY3: "Nightmare Key for Key Cavern", + NIGHTMARE_KEY4: "Nightmare Key for Angler's Tunnel", + NIGHTMARE_KEY5: "Nightmare Key for Catfish's Maw", + NIGHTMARE_KEY6: "Nightmare Key for Face Shrine", + NIGHTMARE_KEY7: "Nightmare Key for Eagle's Tower", + NIGHTMARE_KEY8: "Nightmare Key for Turtle Rock", + NIGHTMARE_KEY9: "Nightmare Key for Color Dungeon", + + HEART_PIECE: "Piece of Heart", + BOWWOW: "Bowwow", + ARROWS_10: "10 Arrows", + SINGLE_ARROW: "Single Arrow", + MEDICINE: "Medicine", + + MAX_POWDER_UPGRADE: "Magic Powder upgrade", + MAX_BOMBS_UPGRADE: "Bombs upgrade", + MAX_ARROWS_UPGRADE: "Arrows upgrade", + + RED_TUNIC: "Red Tunic", + BLUE_TUNIC: "Blue Tunic", + + HEART_CONTAINER: "Heart Container", + BAD_HEART_CONTAINER: "Anti-Heart Container", + + TOADSTOOL: "Toadstool", + + SONG1: "Ballad of the Wind Fish", + SONG2: "Manbo's Mambo", + SONG3: "Frog's Song of Soul", + + INSTRUMENT1: "Full Moon Cello", + INSTRUMENT2: "Conch Horn", + INSTRUMENT3: "Sea Lily's Bell", + INSTRUMENT4: "Surf Harp", + INSTRUMENT5: "Wind Marimba", + INSTRUMENT6: "Coral Triangle", + INSTRUMENT7: "Organ of Evening Calm", + INSTRUMENT8: "Thunder Drum", + + TRADING_ITEM_YOSHI_DOLL: "Yoshi Doll", + TRADING_ITEM_RIBBON: "Ribbon", + TRADING_ITEM_DOG_FOOD: "Dog Food", + TRADING_ITEM_BANANAS: "Bananas", + TRADING_ITEM_STICK: "Stick", + TRADING_ITEM_HONEYCOMB: "Honeycomb", + TRADING_ITEM_PINEAPPLE: "Pineapple", + TRADING_ITEM_HIBISCUS: "Hibiscus", + TRADING_ITEM_LETTER: "Letter", + TRADING_ITEM_BROOM: "Broom", + TRADING_ITEM_FISHING_HOOK: "Fishing Hook", + TRADING_ITEM_NECKLACE: "Necklace", + TRADING_ITEM_SCALE: "Scale", + TRADING_ITEM_MAGNIFYING_GLASS: "Magnifying Lens", + GEL: "Slimy Surprise", + MESSAGE: "A Special Message From Our Sponsors" +} + + +def setReplacementName(key: str, value: str) -> None: + _NAMES[key] = value + + +def formatText(instr: str, *, center: bool = False, ask: Optional[str] = None) -> bytes: + instr = instr.format(**_NAMES) + s = instr.encode("ascii") + s = s.replace(b"'", b"^") + + def padLine(line: bytes) -> bytes: + return line + b' ' * (16 - len(line)) + if center: + def padLine(line: bytes) -> bytes: + padding = (16 - len(line)) + return b' ' * (padding // 2) + line + b' ' * (padding - padding // 2) + + result = b'' + for line in s.split(b'\n'): + result_line = b'' + for word in line.split(b' '): + if len(result_line) + 1 + len(word) > 16: + result += padLine(result_line) + result_line = b'' + elif result_line: + result_line += b' ' + result_line += word + if result_line: + result += padLine(result_line) + if ask is not None: + askbytes = ask.encode("ascii") + result = result.rstrip() + while len(result) % 32 != 16: + result += b' ' + return result + b' ' + askbytes + b'\xfe' + return result.rstrip() + b'\xff' + + +def tileDataToString(data: bytes, key: str = " 123") -> str: + result = "" + for n in range(0, len(data), 2): + a = data[n] + b = data[n+1] + for m in range(8): + bit = 0x80 >> m + if (a & bit) and (b & bit): + result += key[3] + elif (b & bit): + result += key[2] + elif (a & bit): + result += key[1] + else: + result += key[0] + result += "\n" + return result.rstrip("\n") + + +def createTileData(data: str, key: str = " 123") -> bytes: + result = [] + for line in data.split("\n"): + line = line + " " + a = 0 + b = 0 + for n in range(8): + if line[n] == key[3]: + a |= 0x80 >> n + b |= 0x80 >> n + elif line[n] == key[2]: + b |= 0x80 >> n + elif line[n] == key[1]: + a |= 0x80 >> n + result.append(a) + result.append(b) + assert (len(result) % 16) == 0, len(result) + return bytes(result) + + +if __name__ == "__main__": + data = formatText("It is dangurous to go alone.\nTake\nthis\na\nline.") + for i in range(0, len(data), 16): + print(data[i:i+16]) diff --git a/worlds/ladx/LADXR/worldSetup.py b/worlds/ladx/LADXR/worldSetup.py new file mode 100644 index 000000000000..d7ca37f203d7 --- /dev/null +++ b/worlds/ladx/LADXR/worldSetup.py @@ -0,0 +1,136 @@ +from .patches import enemies, bingo +from .locations.items import * +from .entranceInfo import ENTRANCE_INFO + + + +MULTI_CHEST_OPTIONS = [MAGIC_POWDER, BOMB, MEDICINE, RUPEES_50, RUPEES_20, RUPEES_100, RUPEES_200, RUPEES_500, SEASHELL, GEL, ARROWS_10, SINGLE_ARROW] +MULTI_CHEST_WEIGHTS = [20, 20, 20, 50, 50, 20, 10, 5, 5, 20, 10, 10] + +# List of all the possible locations where we can place our starting house +start_locations = [ + "phone_d8", + "rooster_house", + "writes_phone", + "castle_phone", + "photo_house", + "start_house", + "prairie_right_phone", + "banana_seller", + "prairie_low_phone", + "animal_phone", +] + + +class WorldSetup: + def __init__(self): + self.entrance_mapping = {k: k for k in ENTRANCE_INFO.keys()} + self.boss_mapping = list(range(9)) + self.miniboss_mapping = { + # Main minibosses + 0: "ROLLING_BONES", 1: "HINOX", 2: "DODONGO", 3: "CUE_BALL", 4: "GHOMA", 5: "SMASHER", 6: "GRIM_CREEPER", 7: "BLAINO", + # Color dungeon needs to be special, as always. + "c1": "AVALAUNCH", "c2": "GIANT_BUZZ_BLOB", + # Overworld + "moblin_cave": "MOBLIN_KING", + "armos_temple": "ARMOS_KNIGHT", + } + self.goal = None + self.bingo_goals = None + self.multichest = RUPEES_20 + self.map = None # Randomly generated map data + + def getEntrancePool(self, settings, connectorsOnly=False): + entrances = [] + + if connectorsOnly: + if settings.entranceshuffle in ("advanced", "expert", "insanity"): + entrances = [k for k, v in ENTRANCE_INFO.items() if v.type == "connector"] + + return entrances + + if settings.dungeonshuffle and settings.entranceshuffle == "none": + entrances = [k for k, v in ENTRANCE_INFO.items() if v.type == "dungeon"] + if settings.entranceshuffle in ("simple", "advanced", "expert", "insanity"): + types = {"single"} + if settings.tradequest: + types.add("trade") + if settings.entranceshuffle in ("expert", "insanity"): + types.update(["dummy", "trade"]) + if settings.entranceshuffle in ("insanity",): + types.add("insanity") + if settings.randomstartlocation: + types.add("start") + if settings.dungeonshuffle: + types.add("dungeon") + entrances = [k for k, v in ENTRANCE_INFO.items() if v.type in types] + + return entrances + + def randomize(self, settings, rnd): + if settings.overworld == "dungeondive": + self.entrance_mapping = {"d%d" % (n): "d%d" % (n) for n in range(9)} + if settings.randomstartlocation and settings.entranceshuffle == "none": + start_location = start_locations[rnd.randrange(len(start_locations))] + if start_location != "start_house": + self.entrance_mapping[start_location] = "start_house" + self.entrance_mapping["start_house"] = start_location + + entrances = self.getEntrancePool(settings) + for entrance in entrances.copy(): + self.entrance_mapping[entrance] = entrances.pop(rnd.randrange(len(entrances))) + + # Shuffle connectors among themselves + entrances = self.getEntrancePool(settings, connectorsOnly=True) + for entrance in entrances.copy(): + self.entrance_mapping[entrance] = entrances.pop(rnd.randrange(len(entrances))) + + if settings.boss != "default": + values = list(range(9)) + if settings.heartcontainers: + # Color dungeon boss does not drop a heart container so we cannot shuffle him when we + # have heart container shuffling + values.remove(8) + self.boss_mapping = [] + for n in range(8 if settings.heartcontainers else 9): + value = rnd.choice(values) + self.boss_mapping.append(value) + if value in (3, 6) or settings.boss == "shuffle": + values.remove(value) + if settings.heartcontainers: + self.boss_mapping += [8] + if settings.miniboss != "default": + values = [name for name in self.miniboss_mapping.values()] + for key in self.miniboss_mapping.keys(): + self.miniboss_mapping[key] = rnd.choice(values) + if settings.miniboss == 'shuffle': + values.remove(self.miniboss_mapping[key]) + + if settings.goal == 'random': + self.goal = rnd.randint(-1, 8) + elif settings.goal == 'open': + self.goal = -1 + elif settings.goal in {"seashells", "bingo", "bingo-full"}: + self.goal = settings.goal + elif "-" in settings.goal: + a, b = settings.goal.split("-") + if a == "open": + a = -1 + self.goal = rnd.randint(int(a), int(b)) + else: + self.goal = int(settings.goal) + if self.goal in {"bingo", "bingo-full"}: + self.bingo_goals = bingo.randomizeGoals(rnd, settings) + + self.multichest = rnd.choices(MULTI_CHEST_OPTIONS, MULTI_CHEST_WEIGHTS)[0] + + def loadFromRom(self, rom): + import patches.overworld + if patches.overworld.isNormalOverworld(rom): + import patches.entrances + self.entrance_mapping = patches.entrances.readEntrances(rom) + else: + self.entrance_mapping = {"d%d" % (n): "d%d" % (n) for n in range(9)} + self.boss_mapping = patches.enemies.readBossMapping(rom) + self.miniboss_mapping = patches.enemies.readMiniBossMapping(rom) + self.goal = 8 # Better then nothing diff --git a/worlds/ladx/Locations.py b/worlds/ladx/Locations.py new file mode 100644 index 000000000000..69eb78dd88a9 --- /dev/null +++ b/worlds/ladx/Locations.py @@ -0,0 +1,247 @@ +from BaseClasses import Region, Entrance, Location +from worlds.AutoWorld import LogicMixin + + +from .LADXR.checkMetadata import checkMetadataTable +from .Common import * +from worlds.generic.Rules import add_item_rule +from .Items import ladxr_item_to_la_item_name, ItemName, LinksAwakeningItem +from .LADXR.locations.tradeSequence import TradeRequirements, TradeSequenceItem + +prefilled_events = ["ANGLER_KEYHOLE", "RAFT", "MEDICINE2", "CASTLE_BUTTON"] + +links_awakening_dungeon_names = [ + "Tail Cave", + "Bottle Grotto", + "Key Cavern", + "Angler's Tunnel", + "Catfish's Maw", + "Face Shrine", + "Eagle's Tower", + "Turtle Rock", + "Color Dungeon" +] + + +def meta_to_name(meta): + return f"{meta.name} ({meta.area})" + + +def get_locations_to_id(): + ret = { + + } + + # Magic to generate unique ids + for s, v in checkMetadataTable.items(): + if s == "None": + continue + splits = s.split("-") + + main_id = int(splits[0], 16) + sub_id = 0 + if len(splits) > 1: + sub_id = splits[1] + if sub_id.isnumeric(): + sub_id = (int(sub_id) + 1) * 1000 + else: + sub_id = 1000 + name = f"{v.name} ({v.area})" + ret[name] = BASE_ID + main_id + sub_id + + return ret + + +locations_to_id = get_locations_to_id() + + +class LinksAwakeningLocation(Location): + game = LINKS_AWAKENING + dungeon = None + + def __init__(self, player: int, region, ladxr_item): + name = meta_to_name(ladxr_item.metadata) + + self.event = ladxr_item.event is not None + if self.event: + name = ladxr_item.event + + address = None + if not self.event: + address = locations_to_id[name] + super().__init__(player, name, address) + self.parent_region = region + self.ladxr_item = ladxr_item + + def filter_item(item): + if not ladxr_item.MULTIWORLD and item.player != player: + return False + return True + add_item_rule(self, filter_item) + + +def has_free_weapon(state: "CollectionState", player: int) -> bool: + return state.has("Progressive Sword", player) or state.has("Magic Rod", player) or state.has("Boomerang", player) or state.has("Hookshot", player) + +# If the player has access to farm enough rupees to afford a game, we assume that they can keep beating the game +def can_farm_rupees(state: "CollectionState", player: int) -> bool: + return has_free_weapon(state, player) and (state.has("Can Play Trendy Game", player=player) or state.has("RAFT", player=player)) + + +class LinksAwakeningLogic(LogicMixin): + rupees = { + ItemName.RUPEES_20: 0, + ItemName.RUPEES_50: 0, + ItemName.RUPEES_100: 100, + ItemName.RUPEES_200: 200, + ItemName.RUPEES_500: 500, + } + + def get_credits(self, player: int): + if can_farm_rupees(self, player): + return 999999999 + return sum(self.count(item_name, player) * amount for item_name, amount in self.rupees.items()) + + +class LinksAwakeningRegion(Region): + dungeon_index = None + ladxr_region = None + + def __init__(self, name, ladxr_region, hint, player, world): + super().__init__(name, player, world, hint) + if ladxr_region: + self.ladxr_region = ladxr_region + if ladxr_region.dungeon: + self.dungeon_index = ladxr_region.dungeon + + +def translate_item_name(item): + if item in ladxr_item_to_la_item_name: + return ladxr_item_to_la_item_name[item] + + return item + + +class GameStateAdapater: + def __init__(self, state, player): + self.state = state + self.player = player + + def __contains__(self, item): + if item.endswith("_USED"): + return False + if item in ladxr_item_to_la_item_name: + item = ladxr_item_to_la_item_name[item] + + return self.state.has(item, self.player) + + def get(self, item, default): + if item == "RUPEES": + return self.state.get_credits(self.player) + elif item.endswith("_USED"): + return 0 + else: + item = ladxr_item_to_la_item_name[item] + return self.state.prog_items.get((item, self.player), default) + + +class LinksAwakeningEntrance(Entrance): + def __init__(self, player: int, name, region, condition): + super().__init__(player, name, region) + if isinstance(condition, str): + if condition in ladxr_item_to_la_item_name: + # Test if in inventory + self.condition = ladxr_item_to_la_item_name[condition] + else: + # Event + self.condition = condition + elif condition: + # rewrite condition + # .copyWithModifiedItemNames(translate_item_name) + self.condition = condition + else: + self.condition = None + + def access_rule(self, state): + if isinstance(self.condition, str): + return state.has(self.condition, self.player) + if self.condition is None: + return True + + return self.condition.test(GameStateAdapater(state, self.player)) + + +# Helper to apply function to every ladxr region +def walk_ladxdr(f, n, walked=set()): + if n in walked: + return + f(n) + walked.add(n) + + for o, req in n.simple_connections: + walk_ladxdr(f, o, walked) + for o, req in n.gated_connections: + walk_ladxdr(f, o, walked) + + +def ladxr_region_to_name(n): + name = n.name + if not name: + if len(n.items) == 1: + meta = n.items[0].metadata + name = f"{meta.name} ({meta.area})" + elif n.dungeon: + name = f"D{n.dungeon} Room" + else: + name = "No Name" + + return name + + +def create_regions_from_ladxr(player, multiworld, logic): + tmp = set() + + def print_items(n): + print(f"Creating Region {ladxr_region_to_name(n)}") + print("Has simple connections:") + for region, info in n.simple_connections: + print(" " + ladxr_region_to_name(region) + " | " + str(info)) + print("Has gated connections:") + + for region, info in n.gated_connections: + print(" " + ladxr_region_to_name(region) + " | " + str(info)) + + print("Has Locations:") + for item in n.items: + print(" " + str(item.metadata)) + print() + + used_names = {} + + regions = {} + + # Create regions + for l in logic.location_list: + # Temporarily uniqueify the name, until all regions are named + name = ladxr_region_to_name(l) + index = used_names.get(name, 0) + 1 + used_names[name] = index + if index != 1: + name += f" {index}" + + r = LinksAwakeningRegion( + name=name, ladxr_region=l, hint="", player=player, world=multiworld) + r.locations = [LinksAwakeningLocation(player, r, i) for i in l.items] + regions[l] = r + + for ladxr_location in logic.location_list: + for connection_location, connection_condition in ladxr_location.simple_connections + ladxr_location.gated_connections: + region_a = regions[ladxr_location] + region_b = regions[connection_location] + # TODO: This name ain't gonna work for entrance rando, we need to cross reference with logic.world.overworld_entrance + entrance = LinksAwakeningEntrance( + player, f"{region_a.name} -> {region_b.name}", region_a, connection_condition) + region_a.exits.append(entrance) + entrance.connect(region_b) + + return list(regions.values()) diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py new file mode 100644 index 000000000000..37055b7a2fa1 --- /dev/null +++ b/worlds/ladx/Options.py @@ -0,0 +1,366 @@ +import os.path +import typing +import logging +from Options import Choice, Option, Toggle, DefaultOnToggle, Range, FreeText +from collections import defaultdict + +DefaultOffToggle = Toggle + +logger = logging.getLogger("Link's Awakening Logger") + + +class LADXROption: + def to_ladxr_option(self, all_options): + if not self.ladxr_name: + return None, None + + return (self.ladxr_name, self.name_lookup[self.value].replace("_", "")) + + +class Logic(Choice, LADXROption): + """Affects where items are allowed to be placed. + [Normal] Playable without using any tricks or glitches. Can require knowledge from a vanilla playthrough, such as how to open Color Dungeon. + [Hard] More advanced techniques may be required, but glitches are not. Examples include tricky jumps, killing enemies with only pots. + [Glitched] Advanced glitches and techniques may be required, but extremely difficult or tedious tricks are not required. Examples include Bomb Triggers, Super Jumps and Jesus Jumps. + [Hell] Obscure knowledge and hard techniques may be required. Examples include featherless jumping with boots and/or hookshot, sequential pit buffers and unclipped superjumps. Things in here can be extremely hard to do or very time consuming.""" + display_name = "Logic" + ladxr_name = "logic" + # option_casual = 0 + option_normal = 1 + option_hard = 2 + option_glitched = 3 + option_hell = 4 + + default = option_normal + +class TradeQuest(DefaultOffToggle, LADXROption): + """ + On - adds the trade items to the pool (the trade locations will always be local items) + Off - (default) doesn't add them + """ + ladxr_name = "tradequest" + +class Boomerang(Choice): + """ + [Normal], requires Magnifying Lens to get the boomerang. + [Gift], The boomerang salesman will give you a random item, and the boomerang is shuffled. + """ + + normal = 0 + gift = 1 + default = gift + +# TODO: translate to lttp parlance +class EntranceShuffle(Choice, LADXROption): + """ + [WARNING] Experimental, may break generation + Randomizes where overworld entrances lead to. + [Simple] Single-entrance caves/houses that have items are shuffled amongst each other. + [Advanced] Simple, but two-way connector caves are shuffled in their own pool as well. + [Expert] Advanced, but caves/houses without items are also shuffled into the Simple entrance pool. + [Insanity] Expert, but the Raft Minigame hut and Mamu's cave are added to the non-connector pool. + If random start location and/or dungeon shuffle is enabled, then these will be shuffled with all the non-connector entrance pool. + Note, some entrances can lead into water, use the warp-to-home from the save&quit menu to escape this.""" + option_none = 0 + option_simple = 1 + #option_advanced = 2 + #option_expert = 3 + #option_insanity = 4 + default = option_none + ladxr_name = "entranceshuffle" + +class DungeonShuffle(DefaultOffToggle, LADXROption): + """ + [WARNING] Experimental, may break generation + Randomizes + """ + ladxr_name = "dungeonshuffle" + +class BossShuffle(Choice): + none = 0 + shuffle = 1 + random = 2 + default = none + + +class DungeonItemShuffle(Choice): + option_original_dungeon = 0 + option_own_dungeons = 1 + option_own_world = 2 + option_any_world = 3 + option_different_world = 4 + #option_delete = 5 + #option_start_with = 6 + alias_true = 3 + alias_false = 0 + +class ShuffleNightmareKeys(DungeonItemShuffle): + """ + Shuffle Nightmare Keys + """ + ladxr_item = "NIGHTMARE_KEY" +class ShuffleSmallKeys(DungeonItemShuffle): + """ + Shuffle Small Keys + """ + ladxr_item = "KEY" +class ShuffleMaps(DungeonItemShuffle): + """ + Shuffle Dungeon Maps + """ + ladxr_item = "MAP" +class ShuffleCompasses(DungeonItemShuffle): + """ + Shuffle Dungeon Compasses + """ + ladxr_item = "COMPASS" +class ShuffleStoneBeaks(DungeonItemShuffle): + """ + Shuffle Owl Beaks + """ + ladxr_item = "STONE_BEAK" +class Goal(Choice, LADXROption): + """ + [Instruments] The Wind Fish's Egg will only open if you have the required number of Instruments of the Sirens, and play the Ballad of the Wind Fish. + [Seashells] The Egg will open when you bring 20 seashells. The Ballad and Ocarina are not needed. + [Open] The Egg will start pre-opened. + """ + display_name = "Goal" + ladxr_name = "goal" + option_instruments = 1 + option_seashells = 2 + option_open = 3 + + default = option_instruments + + + def to_ladxr_option(self, all_options): + + if self.value == self.option_instruments: + return ("goal", all_options["instrument_count"]) + else: + return LADXROption.to_ladxr_option(self, all_options) + +class InstrumentCount(Range, LADXROption): + ladxr_name = None + range_start = 0 + range_end = 8 + default = 8 + +#class SeashellCount(Range): +# range_start = 0 +# range_end = 20 +# default = 20 + +# Setting('goal', 'Gameplay', 'G', 'Goal', options=[('8', '8', '8 instruments'), ('7', '7', '7 instruments'), ('6', '6', '6 instruments'), +# ('5', '5', '5 instruments'), ('4', '4', '4 instruments'), ('3', '3', '3 instruments'), +# ('2', '2', '2 instruments'), ('1', '1', '1 instrument'), ('0', '0', 'No instruments'), +# ('open', 'O', 'Egg already open'), ('random', 'R', 'Random instrument count'), +# ('open-4', '<', 'Random short game (0-4)'), ('5-8', '>', 'Random long game (5-8)'), +# ('seashells', 'S', 'Seashell hunt (20)'), ('bingo', 'b', 'Bingo!'), +# ('bingo-full', 'B', 'Bingo-25!')], default='8', +# description="""Changes the goal of the game. +# [1-8 instruments], number of instruments required to open the egg. +# [No instruments] open the egg without instruments, still requires the ocarina with the balled of the windfish +# [Egg already open] the egg is already open, just head for it once you have the items needed to defeat the boss. +# [Randomized instrument count] random number of instruments required to open the egg, between 0 and 8. +# [Random short/long game] random number of instruments required to open the egg, chosen between 0-4 and 5-8 respectively. +# [Seashell hunt] egg will open once you collected 20 seashells. Instruments are replaced by seashells and shuffled. +# [Bingo] Generate a 5x5 bingo board with various goals. Complete one row/column or diagonal to win! +# [Bingo-25] Bingo, but need to fill the whole bingo card to win!"""), +class ItemPool(Choice): + """Effects which items are shuffled. +[Casual] Places multiple copies of key items. +[More keys] Adds additional small/nightmare keys so that dungeons are faster. +[Path of Pain]. Adds negative heart containers to the item pool.""" + casual = 0 + more_keys = 1 + normal = 2 + painful = 3 + default = normal + +# Setting('hpmode', 'Gameplay', 'm', 'Health mode', options=[('default', '', 'Normal'), ('inverted', 'i', 'Inverted'), ('1', '1', 'Start with 1 heart'), ('low', 'l', 'Low max')], default='default', +# description=""" +# [Normal} health works as you would expect. +# [Inverted] you start with 9 heart containers, but killing a boss will take a heartcontainer instead of giving one. +# [Start with 1] normal game, you just start with 1 heart instead of 3. +# [Low max] replace heart containers with heart pieces."""), + +# Setting('hardmode', 'Gameplay', 'X', 'Hard mode', options=[('none', '', 'Disabled'), ('oracle', 'O', 'Oracle'), ('hero', 'H', 'Hero'), ('ohko', '1', 'One hit KO')], default='none', +# description=""" +# [Oracle] Less iframes and heath from drops. Bombs damage yourself. Water damages you without flippers. No piece of power or acorn. +# [Hero] Switch version hero mode, double damage, no heart/fairy drops. +# [One hit KO] You die on a single hit, always."""), + +# Setting('steal', 'Gameplay', 't', 'Stealing from the shop', +# options=[('always', 'a', 'Always'), ('never', 'n', 'Never'), ('default', '', 'Normal')], default='default', +# description="""Effects when you can steal from the shop. Stealing is bad and never in logic. +# [Normal] requires the sword before you can steal. +# [Always] you can always steal from the shop +# [Never] you can never steal from the shop."""), +class Bowwow(Choice): + """Allows BowWow to be taken into any area. Certain enemies and bosses are given a new weakness to BowWow. + [Normal] BowWow is in the item pool, but can be logically expected as a damage source. + [Swordless] The progressive swords are removed from the item pool. + """ + normal = 0 + swordless = 1 + default = normal + +class Overworld(Choice, LADXROption): + """ + [Dungeon Dive] Create a different overworld where all the dungeons are directly accessible and almost no chests are located in the overworld. + [Tiny dungeons] All dungeons only consist of a boss fight and a instrument reward. Rest of the dungeon is removed. + """ + display_name = "Overworld" + ladxr_name = "overworld" + option_normal = 0 + option_dungeon_dive = 1 + option_tiny_dungeons = 2 + # option_shuffled = 3 + default = option_normal + +# Ugh, this will change what 'progression' means?? +#Setting('owlstatues', 'Special', 'o', 'Owl statues', options=[('', '', 'Never'), ('dungeon', 'D', 'In dungeons'), ('overworld', 'O', 'On the overworld'), ('both', 'B', 'Dungeons and Overworld')], default='', +# description='Replaces the hints from owl statues with additional randomized items'), + +#Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False, +# description='All items will be more powerful, faster, harder, bigger stronger. You name it.'), +#Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none', +# description='Adds that the select button swaps with either A or B. The item is swapped with the top inventory slot. The map is not available when quickswap is enabled.', +# aesthetic=True), +# Setting('textmode', 'User options', 'f', 'Text mode', options=[('fast', '', 'Fast'), ('default', 'd', 'Normal'), ('none', 'n', 'No-text')], default='fast', +# description="""[Fast] makes text appear twice as fast. +# [No-Text] removes all text from the game""", aesthetic=True), +# Setting('lowhpbeep', 'User options', 'p', 'Low HP beeps', options=[('none', 'D', 'Disabled'), ('slow', 'S', 'Slow'), ('default', 'N', 'Normal')], default='slow', +# description='Slows or disables the low health beeping sound', aesthetic=True), +# Setting('noflash', 'User options', 'l', 'Remove flashing lights', default=True, +# description='Remove the flashing light effects from Mamu, shopkeeper and MadBatter. Useful for capture cards and people that are sensitive for these things.', +# aesthetic=True), +# Setting('nagmessages', 'User options', 'S', 'Show nag messages', default=False, +# description='Enables the nag messages normally shown when touching stones and crystals', +# aesthetic=True), +# Setting('gfxmod', 'User options', 'c', 'Graphics', options=gfx_options, default='', +# description='Generally affects at least Link\'s sprite, but can alter any graphics in the game', +# aesthetic=True), +# Setting('linkspalette', 'User options', 'C', "Link's color", +# options=[('-1', '-', 'Normal'), ('0', '0', 'Green'), ('1', '1', 'Yellow'), ('2', '2', 'Red'), ('3', '3', 'Blue'), +# ('4', '4', '?? A'), ('5', '5', '?? B'), ('6', '6', '?? C'), ('7', '7', '?? D')], default='-1', aesthetic=True, +# description="""Allows you to force a certain color on link. +# [Normal] color of link depends on the tunic. +# [Green/Yellow/Red/Blue] forces link into one of these colors. +# [?? A/B/C/D] colors of link are usually inverted and color depends on the area you are in."""), +# Setting('music', 'User options', 'M', 'Music', options=[('', '', 'Default'), ('random', 'r', 'Random'), ('off', 'o', 'Disable')], default='', +# description=""" +# [Random] Randomizes overworld and dungeon music' +# [Disable] no music in the whole game""", +# aesthetic=True), + +class LinkPalette(Choice, LADXROption): + """ + A-D are color palettes usually used during the damage animation and can change based on where you are. + """ + display_name = "Links Palette" + ladxr_name = "linkspalette" + option_normal = -1 + option_green = 0 + option_yellow = 1 + option_red = 2 + option_blue = 3 + option_invert_a = 4 + option_invert_b = 5 + option_invert_c = 6 + option_invert_d = 7 + default = option_normal + + def to_ladxr_option(self, all_options): + return self.ladxr_name, str(self.value) + +class TrendyGame(Choice): + """ + [Easy] All of the items hold still for you + [Normal] The vanilla behavior + [Hard] ? + [Harder] ??? + [Hardest] ???? + [Impossible] ????? + """ + option_easy = 0 + option_normal = 1 + option_hard = 2 + option_harder = 3 + option_hardest = 4 + option_impossible = 5 + default = option_normal + +class GfxMod(FreeText, LADXROption): + """ + options here correlate with sprite and name files in data/sprites/ladx + """ + display_name = "GFX Modification" + ladxr_name = "gfxmod" + normal = '' + default = 'Link' + + __spriteFiles: typing.DefaultDict[str, typing.List[str]] = defaultdict(list) + __spriteDir = os.path.join('data', 'sprites','ladx') + + extensions = [".bin", ".bdiff", ".png", ".bmp"] + def __init__(self, value: str): + super().__init__(value) + if not GfxMod.__spriteFiles: + for file in os.listdir(GfxMod.__spriteDir): + name, extension = os.path.splitext(file) + if extension in self.extensions: + GfxMod.__spriteFiles[name].append(file) + + + def to_ladxr_option(self, all_options): + if self.value == -1 or self.value == "Link": + return None, None + elif self.value in GfxMod.__spriteFiles: + if len(GfxMod.__spriteFiles[self.value]) > 1: + logger.warning(f"{self.value} does not uniquely identify a file. Possible matches: {GfxMod.__spriteFiles[self.value]}. Using {GfxMod.__spriteFiles[self.value][0]}") + return self.ladxr_name, GfxMod.__spriteFiles[self.value][0] + return self.ladxr_name, GfxMod.__spriteFiles[self.value][0] + logger.error(f"Spritesheet {self.value} not found. Falling back to default sprite.") + return None, None + +class Palette(Choice): + option_normal = 0 + option_1bit = 1 + option_2bit = 2 + option_greyscale = 3 + option_pink = 4 + option_inverted = 5 + +links_awakening_options: typing.Dict[str, typing.Type[Option]] = { + 'logic': Logic, + # 'heartpiece': DefaultOnToggle, # description='Includes heart pieces in the item pool'), + # 'seashells': DefaultOnToggle, # description='Randomizes the secret sea shells hiding in the ground/trees. (chest are always randomized)'), + # 'heartcontainers': DefaultOnToggle, # description='Includes boss heart container drops in the item pool'), + # 'instruments': DefaultOffToggle, # description='Instruments are placed on random locations, dungeon goal will just contain a random item.'), + 'tradequest': TradeQuest, # description='Trade quest items are randomized, each NPC takes its normal trade quest item, but gives a random item'), + # 'witch': DefaultOnToggle, # description='Adds both the toadstool and the reward for giving the toadstool to the witch to the item pool'), + # 'rooster': DefaultOnToggle, # description='Adds the rooster to the item pool. Without this option, the rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means.'), + # 'boomerang': Boomerang, + # 'randomstartlocation': DefaultOffToggle, # 'Randomize where your starting house is located'), + 'experimental_dungeon_shuffle': DungeonShuffle, # 'Randomizes the dungeon that each dungeon entrance leads to'), + 'experimental_entrance_shuffle': EntranceShuffle, + # 'bossshuffle': BossShuffle, + # 'minibossshuffle': BossShuffle, + 'goal': Goal, + 'instrument_count': InstrumentCount, + # 'itempool': ItemPool, + # 'bowwow': Bowwow, + # 'overworld': Overworld, + 'link_palette': LinkPalette, + 'trendy_game': TrendyGame, + 'gfxmod': GfxMod, + 'palette': Palette, + 'shuffle_nightmare_keys': ShuffleNightmareKeys, + 'shuffle_small_keys': ShuffleSmallKeys, + 'shuffle_maps': ShuffleMaps, + 'shuffle_compasses': ShuffleCompasses, + 'shuffle_stone_beaks': ShuffleStoneBeaks, +} diff --git a/worlds/ladx/Rom.py b/worlds/ladx/Rom.py new file mode 100644 index 000000000000..eb573fe5b2cb --- /dev/null +++ b/worlds/ladx/Rom.py @@ -0,0 +1,40 @@ + +import worlds.Files +import hashlib +import Utils +import os +LADX_HASH = "07c211479386825042efb4ad31bb525f" + +class LADXDeltaPatch(worlds.Files.APDeltaPatch): + hash = LADX_HASH + game = "Links Awakening DX" + patch_file_ending = ".apladx" + result_file_ending: str = ".gbc" + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + + +def get_base_rom_bytes(file_name: str = "") -> bytes: + base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) + if not base_rom_bytes: + file_name = get_base_rom_path(file_name) + base_rom_bytes = bytes(open(file_name, "rb").read()) + + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if LADX_HASH != basemd5.hexdigest(): + raise Exception('Supplied Base Rom does not match known MD5 for USA release. ' + 'Get the correct game and version, then dump it') + get_base_rom_bytes.base_rom_bytes = base_rom_bytes + return base_rom_bytes + + +def get_base_rom_path(file_name: str = "") -> str: + options = Utils.get_options() + if not file_name: + file_name = options["ladx_options"]["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.user_path(file_name) + return file_name diff --git a/worlds/ladx/Tracker.py b/worlds/ladx/Tracker.py new file mode 100644 index 000000000000..b3995db01988 --- /dev/null +++ b/worlds/ladx/Tracker.py @@ -0,0 +1,236 @@ +from worlds.ladx.LADXR.checkMetadata import checkMetadataTable +import json +import logging +import websockets +import asyncio + +logger = logging.getLogger("Tracker") + + +# kbranch you're a hero +# https://github.com/kbranch/Magpie/blob/master/autotracking/checks.py +class Check: + def __init__(self, id, address, mask, alternateAddress=None): + self.id = id + self.address = address + self.alternateAddress = alternateAddress + self.mask = mask + self.value = None + self.diff = 0 + + def set(self, bytes): + oldValue = self.value + + self.value = 0 + + for byte in bytes: + maskedByte = byte + if self.mask: + maskedByte &= self.mask + + self.value |= int(maskedByte > 0) + + if oldValue != self.value: + self.diff += self.value - (oldValue or 0) +# Todo: unify this with existing item tables? + + +class LocationTracker: + all_checks = [] + + def __init__(self, gameboy): + self.gameboy = gameboy + maskOverrides = { + '0x106': 0x20, + '0x12B': 0x20, + '0x15A': 0x20, + '0x166': 0x20, + '0x185': 0x20, + '0x1E4': 0x20, + '0x1BC': 0x20, + '0x1E0': 0x20, + '0x1E1': 0x20, + '0x1E2': 0x20, + '0x223': 0x20, + '0x234': 0x20, + '0x2A3': 0x20, + '0x2FD': 0x20, + '0x2A7': 0x20, + '0x1F5': 0x06, + '0x301-0': 0x10, + '0x301-1': 0x10, + } + + addressOverrides = { + '0x30A-Owl': 0xDDEA, + '0x30F-Owl': 0xDDEF, + '0x308-Owl': 0xDDE8, + '0x302': 0xDDE2, + '0x306': 0xDDE6, + '0x307': 0xDDE7, + '0x308': 0xDDE8, + '0x30F': 0xDDEF, + '0x311': 0xDDF1, + '0x314': 0xDDF4, + '0x1F5': 0xDB7D, + '0x301-0': 0xDDE1, + '0x301-1': 0xDDE1, + '0x223': 0xDA2E, + '0x169': 0xD97C, + '0x2A7': 0xD800 + 0x2A1 + } + + alternateAddresses = { + '0x0F2': 0xD8B2, + } + + blacklist = {'None', '0x2A1-2'} + + # in no dungeons boss shuffle, the d3 boss in d7 set 0x20 in fascade's room (0x1BC) + # after beating evil eagile in D6, 0x1BC is now 0xAC (other things may have happened in between) + # entered d3, slime eye flag had already been set (0x15A 0x20). after killing angler fish, bits 0x0C were set + lowest_check = 0xffff + highest_check = 0 + + for check_id in [x for x in checkMetadataTable if x not in blacklist]: + room = check_id.split('-')[0] + mask = 0x10 + address = addressOverrides[check_id] if check_id in addressOverrides else 0xD800 + int( + room, 16) + + if 'Trade' in check_id or 'Owl' in check_id: + mask = 0x20 + + if check_id in maskOverrides: + mask = maskOverrides[check_id] + + lowest_check = min(lowest_check, address) + highest_check = max(highest_check, address) + if check_id in alternateAddresses: + lowest_check = min(lowest_check, alternateAddresses[check_id]) + highest_check = max( + highest_check, alternateAddresses[check_id]) + + check = Check(check_id, address, mask, + alternateAddresses[check_id] if check_id in alternateAddresses else None) + if check_id == '0x2A3': + self.start_check = check + self.all_checks.append(check) + self.remaining_checks = [check for check in self.all_checks] + self.gameboy.set_cache_limits( + lowest_check, highest_check - lowest_check + 1) + + def has_start_item(self): + return self.start_check not in self.remaining_checks + + async def readChecks(self, cb): + new_checks = [] + for check in self.remaining_checks: + addresses = [check.address] + if check.alternateAddress: + addresses.append(check.alternateAddress) + bytes = await self.gameboy.read_memory_cache(addresses) + if not bytes: + return False + check.set(list(bytes.values())) + + if check.value: + self.remaining_checks.remove(check) + new_checks.append(check) + if new_checks: + cb(new_checks) + return True + + +class MagpieBridge: + port = 17026 + server = None + checks = None + item_tracker = None + ws = None + + async def handler(self, websocket): + self.ws = websocket + while True: + message = json.loads(await websocket.recv()) + if message["type"] == "handshake": + logger.info( + f"Connected, supported features: {message['features']}") + if "items" in message["features"]: + await self.send_all_inventory() + if "checks" in message["features"]: + await self.send_all_checks() + + async def send_all_checks(self): + while self.checks == None: + await asyncio.sleep(0.1) + logger.info("sending all checks to magpie") + # Translate renamed IDs back to LADXR IDs + def fixup_id(the_id): + if the_id == "0x2A1": + return "0x2A1-0" + if the_id == "0x2A7": + return "0x2A1-1" + return the_id + + message = { + "type": "check", + "refresh": True, + "version": "1.0", + "diff": False, + "checks": [{"id": fixup_id(check.id), "checked": check.value} for check in self.checks] + } + + await self.ws.send(json.dumps(message)) + + async def send_new_checks(self, checks): + if not self.ws: + return + + logger.debug("Sending new {checks} to magpie") + message = { + "type": "check", + "refresh": True, + "version": "1.0", + "diff": True, + "checks": [{"id": check, "checked": True} for check in checks] + } + + await self.ws.send(json.dumps(message)) + + async def send_all_inventory(self): + logger.info("Sending inventory to magpie") + + while self.item_tracker == None: + await asyncio.sleep(0.1) + + await self.item_tracker.sendItems(self.ws) + + async def send_inventory_diffs(self): + if not self.ws: + return + if not self.item_tracker: + return + await self.item_tracker.sendItems(self.ws, diff=True) + + async def send_gps(self, gps): + if not self.ws: + return + await gps.send_location(self.ws) + + async def serve(self): + async with websockets.serve(lambda w: self.handler(w), "", 17026, logger=logger): + await asyncio.Future() # run forever + + def set_checks(self, checks): + self.checks = checks + + async def set_item_tracker(self, item_tracker): + stale_tracker = self.item_tracker != item_tracker + self.item_tracker = item_tracker + if stale_tracker: + if self.ws: + await self.send_all_inventory() + else: + await self.send_inventory_diffs() + diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py new file mode 100644 index 000000000000..2c213fc75b3c --- /dev/null +++ b/worlds/ladx/__init__.py @@ -0,0 +1,418 @@ +import binascii +import os + +from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial +from Fill import fill_restrictive +from worlds.AutoWorld import WebWorld, World + +from .Common import * +from .Items import (DungeonItemData, DungeonItemType, LinksAwakeningItem, + ladxr_item_to_la_item_name, links_awakening_items, + links_awakening_items_by_name) +from .LADXR import generator +from .LADXR.itempool import ItemPool as LADXRItemPool +from .LADXR.locations.tradeSequence import TradeSequenceItem +from .LADXR.logic import Logic as LAXDRLogic +from .LADXR.main import get_parser +from .LADXR.settings import Settings as LADXRSettings +from .LADXR.worldSetup import WorldSetup as LADXRWorldSetup +from .LADXR.locations.instrument import Instrument +from .LADXR.locations.constants import CHEST_ITEMS +from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion, + create_regions_from_ladxr, get_locations_to_id) +from .Options import links_awakening_options +from .Rom import LADXDeltaPatch + +DEVELOPER_MODE = False + +class LinksAwakeningWebWorld(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up Links Awakening DX for MultiWorld.", + "English", + "setup_en.md", + "setup/en", + ["zig"] + )] + theme = "dirt" + +class LinksAwakeningWorld(World): + """Insert description of the world/game here.""" + game: str = LINKS_AWAKENING # name of the game/world + web = LinksAwakeningWebWorld() + + option_definitions = links_awakening_options # options the player can set + topology_present = True # show path to required location checks in spoiler + + # data_version is used to signal that items, locations or their names + # changed. Set this to 0 during development so other games' clients do not + # cache any texts, then increase by 1 for each release that makes changes. + data_version = 1 + + # ID of first item and location, could be hard-coded but code may be easier + # to read with this as a propery. + base_id = BASE_ID + # Instead of dynamic numbering, IDs could be part of data. + + # The following two dicts are required for the generation to know which + # items exist. They could be generated from json or something else. They can + # include events, but don't have to since events will be placed manually. + item_name_to_id = { + item.item_name : BASE_ID + item.item_id for item in links_awakening_items + } + + item_name_to_data = links_awakening_items_by_name + + location_name_to_id = get_locations_to_id() + + # Items can be grouped using their names to allow easy checking if any item + # from that group has been collected. Group names can also be used for !hint + #item_name_groups = { + # "weapons": {"sword", "lance"} + #} + + prefill_dungeon_items = None + + player_options = None + + def convert_ap_options_to_ladxr_logic(self): + self.player_options = { + option: getattr(self.multiworld, option)[self.player] for option in self.option_definitions + } + + self.laxdr_options = LADXRSettings(self.player_options) + + self.laxdr_options.validate() + world_setup = LADXRWorldSetup() + world_setup.randomize(self.laxdr_options, self.multiworld.random) + self.ladxr_logic = LAXDRLogic(configuration_options=self.laxdr_options, world_setup=world_setup) + self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.laxdr_options, self.multiworld.random).toDict() + + + def create_regions(self) -> None: + # Initialize + self.convert_ap_options_to_ladxr_logic() + regions = create_regions_from_ladxr(self.player, self.multiworld, self.ladxr_logic) + self.multiworld.regions += regions + + # Connect Menu -> Start + start = None + for region in regions: + if region.name == "Start House": + start = region + break + + assert(start) + + menu_region = LinksAwakeningRegion("Menu", None, "Menu", self.player, self.multiworld) + menu_region.exits = [Entrance(self.player, "Start Game", menu_region)] + menu_region.exits[0].connect(start) + + self.multiworld.regions.append(menu_region) + + # Place RAFT, other access events + for region in regions: + for loc in region.locations: + if loc.event: + loc.place_locked_item(self.create_event(loc.ladxr_item.event)) + + # Connect Windfish -> Victory + windfish = self.multiworld.get_region("Windfish", self.player) + l = Location(self.player, "Windfish", parent=windfish) + windfish.locations = [l] + + l.place_locked_item(self.create_event("An Alarm Clock")) + + self.multiworld.completion_condition[self.player] = lambda state: state.has("An Alarm Clock", player=self.player) + + def create_item(self, item_name: str): + return LinksAwakeningItem(self.item_name_to_data[item_name], self, self.player) + + def create_event(self, event: str): + return Item(event, ItemClassification.progression, None, self.player) + + def create_items(self) -> None: + exclude = [item.name for item in self.multiworld.precollected_items[self.player]] + + self.trade_items = [] + + dungeon_item_types = { + + } + from .Options import DungeonItemShuffle + self.prefill_original_dungeon = [ [], [], [], [], [], [], [], [], [] ] + self.prefill_own_dungeons = [] + # For any and different world, set item rule instead + + for option in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks"]: + option = "shuffle_" + option + option = self.player_options[option] + + dungeon_item_types[option.ladxr_item] = option.value + + if option.value == DungeonItemShuffle.option_own_world: + self.multiworld.local_items[self.player].value |= { + ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, 10) + } + elif option.value == DungeonItemShuffle.option_different_world: + self.multiworld.non_local_items[self.player].value |= { + ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, 10) + } + # option_original_dungeon = 0 + # option_own_dungeons = 1 + # option_own_world = 2 + # option_any_world = 3 + # option_different_world = 4 + # option_delete = 5 + + for ladx_item_name, count in self.ladxr_itempool.items(): + # event + if ladx_item_name not in ladxr_item_to_la_item_name: + continue + item_name = ladxr_item_to_la_item_name[ladx_item_name] + for _ in range(count): + if item_name in exclude: + exclude.remove(item_name) # this is destructive. create unique list above + self.multiworld.itempool.append(self.create_item("Master Stalfos' Message")) + else: + item = self.create_item(item_name) + + if not self.multiworld.tradequest[self.player] and ladx_item_name.startswith("TRADING_"): + self.trade_items.append(item) + continue + if isinstance(item.item_data, DungeonItemData): + if item.item_data.dungeon_item_type == DungeonItemType.INSTRUMENT: + # Find instrument, lock + # TODO: we should be able to pinpoint the region we want, save a lookup table please + found = False + for r in self.multiworld.get_regions(): + if r.player != self.player: + continue + if r.dungeon_index != item.item_data.dungeon_index: + continue + for loc in r.locations: + if not isinstance(loc, LinksAwakeningLocation): + continue + if not isinstance(loc.ladxr_item, Instrument): + continue + loc.place_locked_item(item) + found = True + break + if found: + break + else: + item_type = item.item_data.ladxr_id[:-1] + shuffle_type = dungeon_item_types[item_type] + if shuffle_type == DungeonItemShuffle.option_original_dungeon: + self.prefill_original_dungeon[item.item_data.dungeon_index - 1].append(item) + elif shuffle_type == DungeonItemShuffle.option_own_dungeons: + self.prefill_own_dungeons.append(item) + else: + self.multiworld.itempool.append(item) + else: + self.multiworld.itempool.append(item) + + def pre_fill(self): + dungeon_locations = [] + dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []] + all_state = self.multiworld.get_all_state(use_cache=False) + + # Add special case for trendy shop access + trendy_region = self.multiworld.get_region("Trendy Shop", self.player) + event_location = Location(self.player, "Can Play Trendy Game", parent=trendy_region) + trendy_region.locations.insert(0, event_location) + event_location.place_locked_item(self.create_event("Can Play Trendy Game")) + + # For now, special case first item + FORCE_START_ITEM = True + if FORCE_START_ITEM: + start_loc = self.multiworld.get_location("Tarin's Gift (Mabe Village)", self.player) + if not start_loc.item: + possible_start_items = [index for index, item in enumerate(self.multiworld.itempool) + if item.player == self.player + and item.item_data.ladxr_id in start_loc.ladxr_item.OPTIONS] + + index = self.multiworld.random.choice(possible_start_items) + start_item = self.multiworld.itempool.pop(index) + start_loc.place_locked_item(start_item) + + for r in self.multiworld.get_regions(): + if r.player != self.player: + continue + + # Set aside dungeon locations + if r.dungeon_index: + dungeon_locations += r.locations + dungeon_locations_by_dungeon[r.dungeon_index - 1] += r.locations + for location in r.locations: + if location.name == "Pit Button Chest (Tail Cave)": + # Don't place dungeon items on pit button chest, to reduce chance of the filler blowing up + # TODO: no need for this if small key shuffle + dungeon_locations.remove(location) + dungeon_locations_by_dungeon[r.dungeon_index - 1].remove(location) + # Properly fill locations within dungeon + location.dungeon = r.dungeon_index + + # Tell the filler that if we're placing a dungeon item, restrict it to the dungeon the item associates with + # This will need changed once keysanity is implemented + #orig_rule = location.item_rule + #location.item_rule = lambda item, orig_rule=orig_rule: \ + # (not isinstance(item, DungeonItemData) or item.dungeon_index == location.dungeon) and orig_rule(item) + + for location in r.locations: + # If tradequests are disabled, place trade items directly in their proper location + if not self.multiworld.tradequest[self.player] and isinstance(location, LinksAwakeningLocation) and isinstance(location.ladxr_item, TradeSequenceItem): + item = next(i for i in self.trade_items if i.item_data.ladxr_id == location.ladxr_item.default_item) + location.place_locked_item(item) + + for dungeon_index in range(0, 9): + locs = dungeon_locations_by_dungeon[dungeon_index] + locs = [loc for loc in locs if not loc.item] + self.multiworld.random.shuffle(locs) + self.multiworld.random.shuffle(self.prefill_original_dungeon[dungeon_index]) + fill_restrictive(self.multiworld, all_state, locs, self.prefill_original_dungeon[dungeon_index], lock=True) + assert not self.prefill_original_dungeon[dungeon_index] + + # Fill dungeon items first, to not torture the fill algo + dungeon_locations = [loc for loc in dungeon_locations if not loc.item] + # dungeon_items = sorted(self.prefill_own_dungeons, key=lambda item: item.item_data.dungeon_item_type) + self.multiworld.random.shuffle(self.prefill_own_dungeons) + self.multiworld.random.shuffle(dungeon_locations) + fill_restrictive(self.multiworld, all_state, dungeon_locations, self.prefill_own_dungeons, lock=True) + + name_cache = {} + + # Tries to associate an icon from another game with an icon we have + def guess_icon_for_other_world(self, other): + if not self.name_cache: + forbidden = [ + "TRADING", + "ITEM", + "BAD", + "SINGLE", + "UPGRADE", + "BLUE", + "RED", + "NOTHING", + "MESSAGE", + ] + for item in ladxr_item_to_la_item_name.keys(): + self.name_cache[item] = item + splits = item.split("_") + self.name_cache["".join(splits)] = item + if 'RUPEES' in splits: + self.name_cache["".join(reversed(splits))] = item + + for word in item.split("_"): + if word not in forbidden and not word.isnumeric(): + self.name_cache[word] = item + others = { + 'KEY': 'KEY', + 'COMPASS': 'COMPASS', + 'BIGKEY': 'NIGHTMARE_KEY', + 'MAP': 'MAP', + 'FLUTE': 'OCARINA', + 'SONG': 'OCARINA', + 'MUSHROOM': 'TOADSTOOL', + 'GLOVE': 'POWER_BRACELET', + 'BOOT': 'PEGASUS_BOOTS', + 'SHOE': 'PEGASUS_BOOTS', + 'SHOES': 'PEGASUS_BOOTS', + 'SANCTUARYHEARTCONTAINER': 'HEART_CONTAINER', + 'BOSSHEARTCONTAINER': 'HEART_CONTAINER', + 'HEARTCONTAINER': 'HEART_CONTAINER', + 'ENERGYTANK': 'HEART_CONTAINER', + 'MISSILE': 'SINGLE_ARROW', + 'BOMBS': 'BOMB', + 'BLUEBOOMERANG': 'BOOMERANG', + 'MAGICMIRROR': 'TRADING_ITEM_MAGNIFYING_GLASS', + 'MIRROR': 'TRADING_ITEM_MAGNIFYING_GLASS', + 'MESSAGE': 'TRADING_ITEM_LETTER', + # TODO: Also use AP item name + } + for name in others.values(): + assert name in self.name_cache, name + assert name in CHEST_ITEMS, name + self.name_cache.update(others) + + + uppered = other.upper() + if "BIG KEY" in uppered: + return 'NIGHTMARE_KEY' + possibles = other.upper().split(" ") + rejoined = "".join(possibles) + if rejoined in self.name_cache: + return self.name_cache[rejoined] + for name in possibles: + if name in self.name_cache: + return self.name_cache[name] + + return "TRADING_ITEM_LETTER" + + + + + def generate_output(self, output_directory: str): + # copy items back to locations + for r in self.multiworld.get_regions(self.player): + for loc in r.locations: + if isinstance(loc, LinksAwakeningLocation): + assert(loc.item) + # If we're a links awakening item, just use the item + if isinstance(loc.item, LinksAwakeningItem): + loc.ladxr_item.item = loc.item.item_data.ladxr_id + + # TODO: if the item name contains "sword", use a sword icon, etc + # Otherwise, use a cute letter as the icon + else: + loc.ladxr_item.item = self.guess_icon_for_other_world(loc.item.name) + loc.ladxr_item.custom_item_name = loc.item.name + + if loc.item: + loc.ladxr_item.item_owner = loc.item.player + else: + loc.ladxr_item.item_owner = self.player + + # Kind of kludge, make it possible for the location to differentiate between local and remote items + loc.ladxr_item.location_owner = self.player + + rom_path = "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc" + out_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.multiworld.player_name[self.player]}.gbc" + out_file = os.path.join(output_directory, out_name) + + rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.gbc") + + + + parser = get_parser() + args = parser.parse_args([rom_path, "-o", out_name, "--dump"]) + + name_for_rom = self.multiworld.player_name[self.player] + + all_names = [self.multiworld.player_name[i + 1] for i in range(len(self.multiworld.player_name))] + rom = generator.generateRom( + args, + self.laxdr_options, + self.player_options, + bytes.fromhex(self.multiworld.seed_name), + self.ladxr_logic, + rnd=self.multiworld.per_slot_randoms[self.player], + player_name=name_for_rom, + player_names=all_names, + player_id = self.player) + + handle = open(rompath, "wb") + rom.save(handle, name="LADXR") + handle.close() + patch = LADXDeltaPatch(os.path.splitext(rompath)[0]+LADXDeltaPatch.patch_file_ending, player=self.player, + player_name=self.multiworld.player_name[self.player], patched_path=rompath) + patch.write() + if not DEVELOPER_MODE: + os.unlink(rompath) + + def generate_multi_key(self): + return bytes.fromhex(self.multiworld.seed_name) + self.player.to_bytes(2, 'big') + + def modify_multidata(self, multidata: dict): + multi_key = binascii.hexlify(self.generate_multi_key()).decode() + multidata["connect_names"][multi_key] = multidata["connect_names"][self.multiworld.player_name[self.player]] diff --git a/worlds/ladx/docs/en_Links Awakening DX.md b/worlds/ladx/docs/en_Links Awakening DX.md new file mode 100644 index 000000000000..d667490c8a46 --- /dev/null +++ b/worlds/ladx/docs/en_Links Awakening DX.md @@ -0,0 +1,79 @@ +# Links Awakening DX + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game is +always able to be completed, but because of the item shuffle the player may need to access certain areas before they +would in the vanilla game. + +## What items and locations get shuffled? + +All main inventory items, collectables, and ammunition can be shuffled, and all locations in the game which could +contain any of those items may have their contents changed. + +## Which items can be in another player's world? + +Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit +certain items to your own world. + +## What does another world's item look like in Link's Awakening? + +The game will try to pick an appropriate sprite for the item (a LttP sword will be a sword!) - it may, however, be a little odd (a Missile Pack may be a single arrow). + +If there's no appropriate sprite, a Letter will be shown. + +## When the player receives an item, what happens? + +When the player receives an item, Link will hold the item above his head and display it to the world. It's good for +business! + +## I don't know what to do! + +That's not a question - but I'd suggest clicking the crow icon on your client, which will load an AP compatible autotracker for LADXR. + +## What is this randomizer based on? + +This randomizer is based on (forked from) the wonderful work daid did on LADXR - https://github.com/daid/LADXR + +The autotracker code for communication with magpie tracker is directly copied from kbranch's repo - https://github.com/kbranch/Magpie/tree/master/autotracking + +## Some tips from LADXR... + +

Locations

+

All chests and dungeon keys are always randomized. Also, the 3 songs (Marin, Mambo, and Manu) give a you an item if you present them the Ocarina. The seashell mansion 20 shells reward is also shuffled, but the 5 and 10 shell reward is not, as those can be missed.

+

The moblin cave with Bowwow contains a chest instead. The color dungeon gives 2 items at the end instead of a choice of tunic. Other item locations are: The toadstool, the reward for delivering the toadstool, hidden seashells, heart pieces, heart containers, golden leaves, the Mad Batters (capacity upgrades), the shovel/bow in the shop, the rooster's grave, and all of the keys' (tail,slime,angler,face,bird) locations.

+

Finally, new players often forget the following locations: the heart piece hidden in the water at the castle, the heart piece hidden in the bomb cave (screen before the honey), bonk seashells (run with pegasus boots against the tree in at the Tail Cave, and the tree right of Mabe Village, next to the phone booth), and the hookshop drop from Master Stalfos in D5.

+ +

Color Dungeon

+

The Color Dungeon is part of the item shuffle, and the red/blue tunics are shuffled in the item pool. Which means the fairy at the end of the color dungeon gives out two random items.

+

To access the color dungeon, you need the power bracelet, and you need to push the gravestones in the right order: "down, left, up, right, up", going from the lower right gravestone, to the one left of it, above it, and then to the right.

+ +

Bowwow

+

Bowwow is in a chest, somewhere. After you find him, he will always be in the swamp with you, but not anywhere else.

+ +

Added things

+

In your save and quit menu, there is a 3rd option to return to your home. This has two main uses: it speeds up the game, and prevents softlocks (common in entrance rando).

+

If you have weapons that require ammunition (bombs, powder, arrows), a ghost will show up inside Marin's house. He will refill you up to 10 ammunition, so you do not run out.

+

The flying rooster is (optionally) available as an item.

+

You can access the Bird Key cave item with the L2 Power Bracelet.

+

Boomerang cave is now a random item gift by default (available post-bombs), and boomerang is in the item pool.

+

Your inventory has been increased by four, to accommodate these items now coexisting with eachother.

+ +

Removed things

+

The ghost mini-quest after D4 never shows up, his seashell reward is always available.

+

The walrus is moved a bit, so that you can access the desert without taking Marin on a date.

+ +

Logic

+

Depending on your settings, you can only steal after you find the sword, always, or never.

+

Do not forget that there are two items in the rafting ride. You can access this with just Hookshot or Flippers.

+

Killing enemies with bombs is in normal logic. You can switch to casual logic if you do not want this.

+

D7 confuses some people, but by dropping down pits on the 2nd floor you can access almost all of this dungeon, even without feather and power bracelet.

+ +

Tech

+

The toadstool and magic powder used to be the same type of item. LADXR turns this into two items that you can have a the same time. 4 extra item slots in your inventory were added to support this extra item, and have the ability to own the boomerang.

+

The glitch where the slime key is effectively a 6th golden leaf is fixed, and golden leaves can be collected fine next to the slime key.

diff --git a/worlds/ladx/docs/setup_en.md b/worlds/ladx/docs/setup_en.md new file mode 100644 index 000000000000..abd60dc82eba --- /dev/null +++ b/worlds/ladx/docs/setup_en.md @@ -0,0 +1,93 @@ +# Links Awakening DX Multiworld Setup Guide + +## Required Software + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `Links Awakening DX` +- Software capable of loading and playing GBC ROM files + - Currently only [RetroArch](https://retroarch.com?page=platforms) 1.10.3 or newer) is supported. + - Bizhawk support will come at a later date. +- Your American 1.0 ROM file, probably named `Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc` + +## Installation Procedures + +1. Download and install LinksAwakeningClient from the link above, making sure to install the most recent version. + **The installer file is located in the assets section at the bottom of the version information**. + - During setup, you will be asked to locate your base ROM file. This is your Links Awakening DX ROM file. + +2. You should assign your emulator as your default program for launching ROM + files. + 1. Extract your emulator's folder to your Desktop, or somewhere you will remember. + 2. Right-click on a ROM file and select **Open with...** + 3. Check the box next to **Always use this app to open .gbc files** + 4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC** + 5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you + extracted in step one. + +## Create a Config (.yaml) File + +### What is a config file and why do I need one? + +Your config file contains a set of configuration options which provide the generator with information about how it +should generate your game. Each player of a multiworld will provide their own config file. This setup allows each player +to enjoy an experience customized for their taste, and different players in the same multiworld can all have different +options. + +### Where do I get a config file? + +The [Player Settings](/games/Links%20Awakening%20DX/player-settings) page on the website allows you to configure +your personal settings and export a config file from them. + +### Verifying your config file + +If you would like to validate your config file to make sure it works, you may do so on the +[YAML Validator](/mysterycheck) page. + +## Generating a Single-Player Game + +1. Navigate to the [Player Settings](/games/Links%20Awakening%20DX/player-settings) page, configure your options, + and click the "Generate Game" button. +2. You will be presented with a "Seed Info" page. +3. Click the "Create New Room" link. +4. You will be presented with a server page, from which you can download your patch file. +5. Double-click on your patch file, and Links Awakening DX will launch automatically, and create your ROM from the patch file. +6. Since this is a single-player game, you will no longer need the client, so feel free to close it. + +## Joining a MultiWorld Game + +### Obtain your patch file and create your ROM + +When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done, +the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch +files. Your patch file should have a `.apladx` extension. + +Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the +client, and will also create your ROM in the same place as your patch file. + +### Connect to the client + +##### RetroArch 1.10.3 or newer + +You only have to do these steps once. Note, RetroArch 1.9.x will not work as it is older than 1.10.3. + +1. Enter the RetroArch main menu screen. +2. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON. +3. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default + Network Command Port at 55355. + +![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png) +4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - Gameboy / Color (SameBoy)". + +### Connect to the Archipelago Server + +The patch file which launched your client should have automatically connected you to the AP Server. There are a few +reasons this may not happen, however, including if the game is hosted on the website but was generated elsewhere. If the +client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it +into the "Server" input field then press enter. + +The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected". + +### Play the game + +When the client shows both Retroarch and Server as connected, you're ready to begin playing. Congratulations on +successfully joining a multiworld game! You can execute various commands in your client. For more information regarding +these commands you can use `/help` for local client commands and `!help` for server commands.