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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
180 changed files with 24191 additions and 2 deletions

View File

@ -134,6 +134,8 @@ components: Iterable[Component] = (
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'),

609
LinksAwakeningClient.py Normal file
View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -93,6 +93,10 @@ sni_options:
lttp_options:
# File name of the v1.0 J rom
rom_file: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
ladx_options:
# File name of the Link's Awakening DX rom
rom_file: "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"
lufia2ac_options:
# File name of the US rom
rom_file: "Lufia II - Rise of the Sinistrals (USA).sfc"
@ -163,3 +167,4 @@ zillion_options:
# RetroArch doesn't make it easy to launch a game from the command line.
# You have to know the path to the emulator core library on the user's computer.
rom_start: "retroarch"

View File

@ -63,8 +63,10 @@ Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full
Name: "generator/zl"; Description: "Zillion ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 150000; Flags: disablenouninstallwarning
Name: "generator/pkmn_r"; Description: "Pokemon Red ROM Setup"; Types: full hosting
Name: "generator/pkmn_b"; Description: "Pokemon Blue ROM Setup"; Types: full hosting
Name: "generator/ladx"; Description: "Link's Awakening DX ROM Setup"; Types: full hosting
Name: "server"; Description: "Server"; Types: full hosting
Name: "client"; Description: "Clients"; Types: full playing
Name: "client/la"; Description: "Links Awakening DX Client"; Types: full playing
Name: "client/sni"; Description: "SNI Client"; Types: full playing
Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
@ -78,6 +80,7 @@ Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing
Name: "client/pkmn"; Description: "Pokemon Client"
Name: "client/pkmn/red"; Description: "Pokemon Client - Pokemon Red Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576
Name: "client/pkmn/blue"; Description: "Pokemon Client - Pokemon Blue Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576
Name: "client/ladx"; Description: "Link's Awakening Client"; Types: full playing; ExtraDiskSpaceRequired: 1048576
Name: "client/cf"; Description: "ChecksFinder"; Types: full playing
Name: "client/sc2"; Description: "Starcraft 2"; Types: full playing
Name: "client/zl"; Description: "Zillion"; Types: full playing
@ -97,6 +100,7 @@ Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda
Source: "{code:GetZlROMPath}"; DestDir: "{app}"; DestName: "Zillion (UE) [!].sms"; Flags: external; Components: client/zl or generator/zl
Source: "{code:GetRedROMPath}"; DestDir: "{app}"; DestName: "Pokemon Red (UE) [S][!].gb"; Flags: external; Components: client/pkmn/red or generator/pkmn_r
Source: "{code:GetBlueROMPath}"; DestDir: "{app}"; DestName: "Pokemon Blue (UE) [S][!].gb"; Flags: external; Components: client/pkmn/blue or generator/pkmn_b
Source: "{code:GetLADXROMPath}"; DestDir: "{app}"; DestName: "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"; Flags: external; Components: client/ladx or generator/ladx
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
@ -106,6 +110,7 @@ Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignorev
Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
Source: "{#source_path}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text
Source: "{#source_path}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni
Source: "{#source_path}\ArchipelagoLinksAwakeningClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/la
Source: "{#source_path}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp
Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
Source: "{#source_path}\ArchipelagoOoTClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
@ -219,6 +224,11 @@ Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Ar
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/ladx
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/ladx
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: ""; Components: client/ladx
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLinksAwakeningClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/ladx
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: server
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: server
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; Components: server
@ -286,6 +296,9 @@ var RedROMFilePage: TInputFileWizardPage;
var bluerom: string;
var BlueROMFilePage: TInputFileWizardPage;
var ladxrom: string;
var LADXROMFilePage: TInputFileWizardPage;
function GetSNESMD5OfFile(const rom: string): string;
var data: AnsiString;
begin
@ -440,6 +453,12 @@ begin
Result := not (OoTROMFilePage.Values[0] = '')
else if (assigned(ZlROMFilePage)) and (CurPageID = ZlROMFilePage.ID) then
Result := not (ZlROMFilePage.Values[0] = '')
else if (assigned(RedROMFilePage)) and (CurPageID = RedROMFilePage.ID) then
Result := not (RedROMFilePage.Values[0] = '')
else if (assigned(BlueROMFilePage)) and (CurPageID = BlueROMFilePage.ID) then
Result := not (BlueROMFilePage.Values[0] = '')
else if (assigned(LADXROMFilePage)) and (CurPageID = LADXROMFilePage.ID) then
Result := not (LADXROMFilePage.Values[0] = '')
else
Result := True;
end;
@ -576,7 +595,7 @@ function GetRedROMPath(Param: string): string;
begin
if Length(redrom) > 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;

2
worlds/ladx/Common.py Normal file
View File

@ -0,0 +1,2 @@
LINKS_AWAKENING = "Links Awakening DX"
BASE_ID = 10000000

92
worlds/ladx/GpsTracker.py Normal file
View File

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

283
worlds/ladx/ItemTracker.py Normal file
View File

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

304
worlds/ladx/Items.py Normal file
View File

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

18
worlds/ladx/LADXR/.tinyci Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

52
worlds/ladx/LADXR/main.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
def xyrange(w, h):
for y in range(h):
for x in range(w):
yield x, y

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More