Links Awakening: Implement New Game (#1334)

Adds Link's Awakening: DX. Fully imports and forks LADXR, with permission - https://github.com/daid/LADXR
This commit is contained in:
zig-for
2023-03-21 01:26:03 +09:00
committed by GitHub
parent 67bf12369a
commit 81a239325d
180 changed files with 24191 additions and 2 deletions

View File

@@ -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 *

View File

@@ -0,0 +1,6 @@
from .droppedKey import DroppedKey
class AnglerKey(DroppedKey):
def __init__(self):
super().__init__(0x0CE)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()

View File

@@ -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)

View File

@@ -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,
}

View File

@@ -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)

View File

@@ -0,0 +1,6 @@
from .droppedKey import DroppedKey
class FaceKey(DroppedKey):
def __init__(self):
super().__init__(0x27F)

View File

@@ -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]

View File

@@ -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)

View File

@@ -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]

View File

@@ -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]

View File

@@ -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)

View File

@@ -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)]

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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))

View File

@@ -0,0 +1,5 @@
from .droppedKey import DroppedKey
class Song(DroppedKey):
pass

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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))