Compare commits

...

1 Commits

Author SHA1 Message Date
Berserker
e47f0cc360 LADX: no pickle 2026-01-19 21:04:25 +01:00
16 changed files with 185 additions and 168 deletions

View File

@@ -2,7 +2,6 @@ import binascii
import importlib.util import importlib.util
import importlib.machinery import importlib.machinery
import random import random
import pickle
import Utils import Utils
from collections import defaultdict from collections import defaultdict
from typing import Dict from typing import Dict
@@ -61,7 +60,11 @@ from .patches import bank34
from .roomEditor import RoomEditor, Object from .roomEditor import RoomEditor, Object
from .patches.aesthetics import rgb_to_bin, bin_to_rgb from .patches.aesthetics import rgb_to_bin, bin_to_rgb
from .. import Options from .logic import Logic as LADXRLogic
from .settings import Settings as LADXRSettings
from .worldSetup import WorldSetup as LADXRWorldSetup
from .locations.keyLocation import KeyLocation
class VersionError(Exception): class VersionError(Exception):
pass pass
@@ -86,8 +89,27 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
random.seed(patch_data["seed"] + patch_data["player"]) random.seed(patch_data["seed"] + patch_data["player"])
multi_key = binascii.unhexlify(patch_data["multi_key"].encode()) multi_key = binascii.unhexlify(patch_data["multi_key"].encode())
item_list = pickle.loads(binascii.unhexlify(patch_data["item_list"].encode()))
options = patch_data["options"] ladxr_settings = LADXRSettings(patch_data["ladxr_settings_dict"])
world_setup = LADXRWorldSetup()
world_setup.goal = patch_data["world_setup"]["goal"]
world_setup.multichest = patch_data["world_setup"]["multichest"]
world_setup.entrance_mapping = patch_data["world_setup"]["entrance_mapping"]
world_setup.boss_mapping = patch_data["world_setup"]["boss_mapping"]
world_setup.miniboss_mapping = patch_data["world_setup"]["miniboss_mapping"]
ladxr_logic = LADXRLogic(configuration_options=ladxr_settings, world_setup=world_setup)
item_list = [item for item in ladxr_logic.iteminfo_list if not isinstance(item, KeyLocation)]
for spot in patch_data["rom_item_placements"]:
ladxr_item = next((item for item in item_list if item.nameId == spot["name_id"]), None)
if not ladxr_item:
continue
ladxr_item.item = spot["item"][1:] if spot["item"].startswith('*') else spot["item"]
ladxr_item.custom_item_name = spot["custom_item_name"]
mw = None
if patch_data["player"] != spot["item_owner"]:
mw = min(spot["item_owner"], 101)
ladxr_item.mw = mw
rom_patches = [] rom_patches = []
rom = ROMWithTables(base_rom, rom_patches) rom = ROMWithTables(base_rom, rom_patches)
rom.player_names = patch_data["other_player_names"] rom.player_names = patch_data["other_player_names"]
@@ -101,7 +123,7 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
for pymod in pymods: for pymod in pymods:
pymod.prePatch(rom) pymod.prePatch(rom)
if options["gfxmod"]: if ladxr_settings.gfxmod:
try: try:
gfx_mod_file = LinksAwakeningWorld.settings.gfx_mod_file gfx_mod_file = LinksAwakeningWorld.settings.gfx_mod_file
patches.aesthetics.gfxMod(rom, gfx_mod_file) patches.aesthetics.gfxMod(rom, gfx_mod_file)
@@ -136,7 +158,7 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
assembler.const("wLinkSpawnDelay", 0xDE13) assembler.const("wLinkSpawnDelay", 0xDE13)
#assembler.const("HARDWARE_LINK", 1) #assembler.const("HARDWARE_LINK", 1)
assembler.const("HARD_MODE", 1 if options["hard_mode"] else 0) assembler.const("HARD_MODE", 1 if ladxr_settings.hardmode else 0)
patches.core.cleanup(rom) patches.core.cleanup(rom)
patches.save.singleSaveSlot(rom) patches.save.singleSaveSlot(rom)
@@ -159,17 +181,16 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
patches.core.alwaysAllowSecretBook(rom) patches.core.alwaysAllowSecretBook(rom)
patches.core.injectMainLoop(rom) patches.core.injectMainLoop(rom)
if options["shuffle_small_keys"] != Options.ShuffleSmallKeys.option_original_dungeon or\ if ladxr_settings.shufflesmallkeys != 'originaldungeon' or ladxr_settings.shufflenightmarekeys != 'originaldungeon':
options["shuffle_nightmare_keys"] != Options.ShuffleNightmareKeys.option_original_dungeon:
patches.inventory.advancedInventorySubscreen(rom) patches.inventory.advancedInventorySubscreen(rom)
patches.inventory.moreSlots(rom) patches.inventory.moreSlots(rom)
# if ladxr_settings["witch"]: # if ladxr_settings.witch:
patches.witch.updateWitch(rom) patches.witch.updateWitch(rom)
patches.softlock.fixAll(rom) patches.softlock.fixAll(rom)
if not options["rooster"]: if not ladxr_settings.rooster:
patches.maptweaks.tweakMap(rom) patches.maptweaks.tweakMap(rom)
patches.maptweaks.tweakBirdKeyRoom(rom) patches.maptweaks.tweakBirdKeyRoom(rom)
if options["overworld"] == Options.Overworld.option_open_mabe: if ladxr_settings.overworld == 'openmabe':
patches.maptweaks.openMabe(rom) patches.maptweaks.openMabe(rom)
patches.chest.fixChests(rom) patches.chest.fixChests(rom)
patches.shop.fixShop(rom) patches.shop.fixShop(rom)
@@ -181,9 +202,9 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
patches.tarin.updateTarin(rom) patches.tarin.updateTarin(rom)
patches.fishingMinigame.updateFinishingMinigame(rom) patches.fishingMinigame.updateFinishingMinigame(rom)
patches.health.upgradeHealthContainers(rom) patches.health.upgradeHealthContainers(rom)
# if ladxr_settings["owlstatues"] in ("dungeon", "both"): # if ladxr_settings.owlstatues in ("dungeon", "both"):
# patches.owl.upgradeDungeonOwlStatues(rom) # patches.owl.upgradeDungeonOwlStatues(rom)
# if ladxr_settings["owlstatues"] in ("overworld", "both"): # if ladxr_settings.owlstatues in ("overworld", "both"):
# patches.owl.upgradeOverworldOwlStatues(rom) # patches.owl.upgradeOverworldOwlStatues(rom)
patches.goldenLeaf.fixGoldenLeaf(rom) patches.goldenLeaf.fixGoldenLeaf(rom)
patches.heartPiece.fixHeartPiece(rom) patches.heartPiece.fixHeartPiece(rom)
@@ -194,17 +215,17 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
patches.songs.upgradeManbo(rom) patches.songs.upgradeManbo(rom)
patches.songs.upgradeMamu(rom) patches.songs.upgradeMamu(rom)
patches.tradeSequence.patchTradeSequence(rom, options) patches.tradeSequence.patchTradeSequence(rom, ladxr_settings)
patches.bowwow.fixBowwow(rom, everywhere=False) patches.bowwow.fixBowwow(rom, everywhere=False)
# if ladxr_settings["bowwow"] != 'normal': # if ladxr_settings.bowwow != 'normal':
# patches.bowwow.bowwowMapPatches(rom) # patches.bowwow.bowwowMapPatches(rom)
patches.desert.desertAccess(rom) patches.desert.desertAccess(rom)
# if ladxr_settings["overworld"] == 'dungeondive': # if ladxr_settings.overworld == 'dungeondive':
# patches.overworld.patchOverworldTilesets(rom) # patches.overworld.patchOverworldTilesets(rom)
# patches.overworld.createDungeonOnlyOverworld(rom) # patches.overworld.createDungeonOnlyOverworld(rom)
# elif ladxr_settings["overworld"] == 'nodungeons': # elif ladxr_settings.overworld == 'nodungeons':
# patches.dungeon.patchNoDungeons(rom) # patches.dungeon.patchNoDungeons(rom)
#elif world.ladxr_settings["overworld"] == 'random': #elif ladxr_settings.overworld == 'random':
# patches.overworld.patchOverworldTilesets(rom) # patches.overworld.patchOverworldTilesets(rom)
# mapgen.store_map(rom, world.ladxr_logic.world.map) # mapgen.store_map(rom, world.ladxr_logic.world.map)
#if settings.dungeon_items == 'keysy': #if settings.dungeon_items == 'keysy':
@@ -212,102 +233,94 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
# patches.reduceRNG.slowdownThreeOfAKind(rom) # patches.reduceRNG.slowdownThreeOfAKind(rom)
patches.reduceRNG.fixHorseHeads(rom) patches.reduceRNG.fixHorseHeads(rom)
patches.bomb.onlyDropBombsWhenHaveBombs(rom) patches.bomb.onlyDropBombsWhenHaveBombs(rom)
if options["music_change_condition"] == Options.MusicChangeCondition.option_always: if ladxr_settings.musicchange == 'always':
patches.aesthetics.noSwordMusic(rom) patches.aesthetics.noSwordMusic(rom)
patches.aesthetics.reduceMessageLengths(rom, random) patches.aesthetics.reduceMessageLengths(rom, random)
patches.aesthetics.allowColorDungeonSpritesEverywhere(rom) patches.aesthetics.allowColorDungeonSpritesEverywhere(rom)
if options["music"] == Options.Music.option_shuffled: if ladxr_settings.music == 'shuffled':
patches.music.randomizeMusic(rom, random) patches.music.randomizeMusic(rom, random)
elif options["music"] == Options.Music.option_off: elif ladxr_settings.music == 'off':
patches.music.noMusic(rom) patches.music.noMusic(rom)
if options["no_flash"]: if ladxr_settings.noflash:
patches.aesthetics.removeFlashingLights(rom) patches.aesthetics.removeFlashingLights(rom)
if options["hard_mode"] == Options.HardMode.option_oracle: if ladxr_settings.hardmode == 'oracle':
patches.hardMode.oracleMode(rom) patches.hardMode.oracleMode(rom)
elif options["hard_mode"] == Options.HardMode.option_hero: elif ladxr_settings.hardmode == 'hero':
patches.hardMode.heroMode(rom) patches.hardMode.heroMode(rom)
elif options["hard_mode"] == Options.HardMode.option_ohko: elif ladxr_settings.hardmode == 'ohko':
patches.hardMode.oneHitKO(rom) patches.hardMode.oneHitKO(rom)
#if ladxr_settings["superweapons"]: #if ladxr_settings.superweapons:
# patches.weapons.patchSuperWeapons(rom) # patches.weapons.patchSuperWeapons(rom)
if options["text_mode"] == Options.TextMode.option_fast: if ladxr_settings.textmode == 'fast':
patches.aesthetics.fastText(rom) patches.aesthetics.fastText(rom)
#if ladxr_settings["textmode"] == 'none': #if ladxr_settings.textmode == 'none':
# patches.aesthetics.fastText(rom) # patches.aesthetics.fastText(rom)
# patches.aesthetics.noText(rom) # patches.aesthetics.noText(rom)
if not options["nag_messages"]: if not ladxr_settings.nagmessages:
patches.aesthetics.removeNagMessages(rom) patches.aesthetics.removeNagMessages(rom)
if options["low_hp_beep"] == Options.LowHpBeep.option_slow: if ladxr_settings.lowhpbeep == 'slow':
patches.aesthetics.slowLowHPBeep(rom) patches.aesthetics.slowLowHPBeep(rom)
if options["low_hp_beep"] == Options.LowHpBeep.option_none: if ladxr_settings.lowhpbeep == 'none':
patches.aesthetics.removeLowHPBeep(rom) patches.aesthetics.removeLowHPBeep(rom)
if 0 <= options["link_palette"]: if 0 <= int(ladxr_settings.linkspalette):
patches.aesthetics.forceLinksPalette(rom, options["link_palette"]) patches.aesthetics.forceLinksPalette(rom, int(ladxr_settings.linkspalette))
if args.romdebugmode: if args.romdebugmode:
# The default rom has this build in, just need to set a flag and we get this save. # The default rom has this build in, just need to set a flag and we get this save.
rom.patch(0, 0x0003, "00", "01") rom.patch(0, 0x0003, "00", "01")
# Patch the sword check on the shopkeeper turning around. # Patch the sword check on the shopkeeper turning around.
if options["stealing"] == Options.Stealing.option_disabled: if ladxr_settings.steal == 'disabled':
rom.patch(4, 0x36F9, "FA4EDB", "3E0000") rom.patch(4, 0x36F9, "FA4EDB", "3E0000")
rom.texts[0x2E] = utils.formatText("Hey! Welcome! Did you know that I have eyes on the back of my head?") rom.texts[0x2E] = utils.formatText("Hey! Welcome! Did you know that I have eyes on the back of my head?")
rom.texts[0x2F] = utils.formatText("Nothing escapes my gaze! Your thieving ways shall never prosper!") rom.texts[0x2F] = utils.formatText("Nothing escapes my gaze! Your thieving ways shall never prosper!")
#if ladxr_settings["hpmode"] == 'inverted': #if ladxr_settings.hpmode == 'inverted':
# patches.health.setStartHealth(rom, 9) # patches.health.setStartHealth(rom, 9)
#elif ladxr_settings["hpmode"] == '1': #elif ladxr_settings.hpmode == '1':
# patches.health.setStartHealth(rom, 1) # patches.health.setStartHealth(rom, 1)
patches.inventory.songSelectAfterOcarinaSelect(rom) patches.inventory.songSelectAfterOcarinaSelect(rom)
if options["quickswap"] == Options.Quickswap.option_a: if ladxr_settings.quickswap == 'a':
patches.core.quickswap(rom, 1) patches.core.quickswap(rom, 1)
elif options["quickswap"] == Options.Quickswap.option_b: elif ladxr_settings.quickswap == 'b':
patches.core.quickswap(rom, 0) patches.core.quickswap(rom, 0)
patches.core.addBootsControls(rom, options["boots_controls"]) patches.core.addBootsControls(rom, ladxr_settings.bootscontrols)
random.seed(patch_data["seed"] + patch_data["player"]) random.seed(patch_data["seed"] + patch_data["player"])
hints.addHints(rom, random, patch_data["hint_texts"]) hints.addHints(rom, random, patch_data["hint_texts"])
if patch_data["world_setup"]["goal"] == "raft": if world_setup.goal == "raft":
patches.goal.setRaftGoal(rom) patches.goal.setRaftGoal(rom)
elif patch_data["world_setup"]["goal"] in ("bingo", "bingo-full"): elif world_setup.goal in ("bingo", "bingo-full"):
patches.bingo.setBingoGoal(rom, patch_data["world_setup"]["bingo_goals"], patch_data["world_setup"]["goal"]) patches.bingo.setBingoGoal(rom, world_setup.bingo_goals, world_setup.goal)
elif patch_data["world_setup"]["goal"] == "seashells": elif world_setup.goal == "seashells":
patches.goal.setSeashellGoal(rom, 20) patches.goal.setSeashellGoal(rom, 20)
else: else:
patches.goal.setRequiredInstrumentCount(rom, patch_data["world_setup"]["goal"]) patches.goal.setRequiredInstrumentCount(rom, world_setup.goal)
# Patch the generated logic into the rom # Patch the generated logic into the rom
patches.chest.setMultiChest(rom, patch_data["world_setup"]["multichest"]) patches.chest.setMultiChest(rom, world_setup.multichest)
#if ladxr_settings["overworld"] not in {"dungeondive", "random"}: #if ladxr_settings.overworld not in {"dungeondive", "random"}:
patches.entrances.changeEntrances(rom, patch_data["world_setup"]["entrance_mapping"]) patches.entrances.changeEntrances(rom, world_setup.entrance_mapping)
for spot in item_list: for ladxr_item in item_list:
if spot.item and spot.item.startswith("*"): ladxr_item.patch(rom, ladxr_item.item, multiworld=ladxr_item.mw)
spot.item = spot.item[1:] patches.enemies.changeBosses(rom, world_setup.boss_mapping)
mw = None patches.enemies.changeMiniBosses(rom, world_setup.miniboss_mapping)
if spot.item_owner != spot.location_owner:
mw = spot.item_owner
if mw > 101:
# There are only 101 player name slots (99 + "The Server" + "another world"), so don't use more than that
mw = 101
spot.patch(rom, spot.item, multiworld=mw)
patches.enemies.changeBosses(rom, patch_data["world_setup"]["boss_mapping"])
patches.enemies.changeMiniBosses(rom, patch_data["world_setup"]["miniboss_mapping"])
if not args.romdebugmode: if not args.romdebugmode:
patches.core.addFrameCounter(rom, len(item_list)) patches.core.addFrameCounter(rom, len(item_list))
patches.core.warpHome(rom) # Needs to be done after setting the start location. patches.core.warpHome(rom) # Needs to be done after setting the start location.
patches.titleScreen.setRomInfo(rom, patch_data) patches.titleScreen.setRomInfo(rom, patch_data)
if options["ap_title_screen"]: if ladxr_settings.aptitlescreen:
patches.titleScreen.setTitleGraphics(rom) patches.titleScreen.setTitleGraphics(rom)
patches.endscreen.updateEndScreen(rom) patches.endscreen.updateEndScreen(rom)
patches.aesthetics.updateSpriteData(rom) patches.aesthetics.updateSpriteData(rom)
if args.doubletrouble: if args.doubletrouble:
patches.enemies.doubleTrouble(rom) patches.enemies.doubleTrouble(rom)
if options["text_shuffle"]: if ladxr_settings.textshuffle:
excluded_ids = [ excluded_ids = [
# Overworld owl statues # Overworld owl statues
0x1B6, 0x1B7, 0x1B8, 0x1B9, 0x1BA, 0x1BB, 0x1BC, 0x1BD, 0x1BE, 0x22D, 0x1B6, 0x1B7, 0x1B8, 0x1B9, 0x1BA, 0x1BB, 0x1BC, 0x1BD, 0x1BE, 0x22D,
@@ -366,14 +379,14 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
rom.texts[shuffled[bucket_idx][0]] = data rom.texts[shuffled[bucket_idx][0]] = data
if options["trendy_game"] != Options.TrendyGame.option_normal: if ladxr_settings.trendygame != 'normal':
# TODO: if 0 or 4, 5, remove inaccurate conveyor tiles # TODO: if 0 or 4, 5, remove inaccurate conveyor tiles
room_editor = RoomEditor(rom, 0x2A0) room_editor = RoomEditor(rom, 0x2A0)
if options["trendy_game"] == Options.TrendyGame.option_easy: if ladxr_settings.trendygame == 'easy':
# Set physics flag on all objects # Set physics flag on all objects
for i in range(0, 6): for i in range(0, 6):
rom.banks[0x4][0x6F1E + i -0x4000] = 0x4 rom.banks[0x4][0x6F1E + i -0x4000] = 0x4
@@ -384,7 +397,7 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
# Add new conveyor to "push" yoshi (it's only a visual) # Add new conveyor to "push" yoshi (it's only a visual)
room_editor.objects.append(Object(5, 3, 0xD0)) room_editor.objects.append(Object(5, 3, 0xD0))
if options["trendy_game"] >= Options.TrendyGame.option_harder: if ladxr_settings.trendygame in ('harder', 'hardest', 'impossible'):
""" """
Data_004_76A0:: Data_004_76A0::
db $FC, $00, $04, $00, $00 db $FC, $00, $04, $00, $00
@@ -393,18 +406,18 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
db $00, $04, $00, $FC, $00 db $00, $04, $00, $FC, $00
""" """
speeds = { speeds = {
Options.TrendyGame.option_harder: (3, 8), 'harder': (3, 8),
Options.TrendyGame.option_hardest: (3, 8), 'hardest': (3, 8),
Options.TrendyGame.option_impossible: (3, 16), 'impossible': (3, 16),
} }
def speed(): def speed():
random.seed(patch_data["seed"] + patch_data["player"]) random.seed(patch_data["seed"] + patch_data["player"])
return random.randint(*speeds[options["trendy_game"]]) return random.randint(*speeds[ladxr_settings.trendygame])
rom.banks[0x4][0x76A0-0x4000] = 0xFF - speed() rom.banks[0x4][0x76A0-0x4000] = 0xFF - speed()
rom.banks[0x4][0x76A2-0x4000] = speed() rom.banks[0x4][0x76A2-0x4000] = speed()
rom.banks[0x4][0x76A6-0x4000] = speed() rom.banks[0x4][0x76A6-0x4000] = speed()
rom.banks[0x4][0x76A8-0x4000] = 0xFF - speed() rom.banks[0x4][0x76A8-0x4000] = 0xFF - speed()
if options["trendy_game"] >= Options.TrendyGame.option_hardest: if ladxr_settings.trendygame in ('hardest', 'impossible'):
rom.banks[0x4][0x76A1-0x4000] = 0xFF - speed() rom.banks[0x4][0x76A1-0x4000] = 0xFF - speed()
rom.banks[0x4][0x76A3-0x4000] = speed() rom.banks[0x4][0x76A3-0x4000] = speed()
rom.banks[0x4][0x76A5-0x4000] = speed() rom.banks[0x4][0x76A5-0x4000] = speed()
@@ -428,11 +441,11 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
for channel in range(3): for channel in range(3):
color[channel] = color[channel] * 31 // 0xbc color[channel] = color[channel] * 31 // 0xbc
if options["warps"] != Options.Warps.option_vanilla: if ladxr_settings.warps != 'vanilla':
patches.core.addWarpImprovements(rom, options["warps"] == Options.Warps.option_improved_additional) patches.core.addWarpImprovements(rom, ladxr_settings.warps == 'improved_additional')
palette = options["palette"] palette = ladxr_settings.palette
if palette != Options.Palette.option_normal: if palette != 'normal':
ranges = { ranges = {
# Object palettes # Object palettes
# Overworld palettes # Overworld palettes
@@ -462,22 +475,22 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
r,g,b = bin_to_rgb(packed) r,g,b = bin_to_rgb(packed)
# 1 bit # 1 bit
if palette == Options.Palette.option_1bit: if palette == '1bit':
r &= 0b10000 r &= 0b10000
g &= 0b10000 g &= 0b10000
b &= 0b10000 b &= 0b10000
# 2 bit # 2 bit
elif palette == Options.Palette.option_1bit: elif palette == '1bit':
r &= 0b11000 r &= 0b11000
g &= 0b11000 g &= 0b11000
b &= 0b11000 b &= 0b11000
# Invert # Invert
elif palette == Options.Palette.option_inverted: elif palette == 'inverted':
r = 31 - r r = 31 - r
g = 31 - g g = 31 - g
b = 31 - b b = 31 - b
# Pink # Pink
elif palette == Options.Palette.option_pink: elif palette == 'pink':
r = r // 2 r = r // 2
r += 16 r += 16
r = int(r) r = int(r)
@@ -486,7 +499,7 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
b += 16 b += 16
b = int(b) b = int(b)
b = clamp(b, 0, 0x1F) b = clamp(b, 0, 0x1F)
elif palette == Options.Palette.option_greyscale: elif palette == 'greyscale':
# gray=int(0.299*r+0.587*g+0.114*b) # gray=int(0.299*r+0.587*g+0.114*b)
gray = (r + g + b) // 3 gray = (r + g + b) // 3
r = g = b = gray r = g = b = gray

View File

@@ -20,7 +20,7 @@ class Dungeon1:
if options.owlstatues == "both" or options.owlstatues == "dungeon": if options.owlstatues == "both" or options.owlstatues == "dungeon":
Location(dungeon=1).add(OwlStatue(0x10A)).connect(dungeon1_right_side, STONE_BEAK1) Location(dungeon=1).add(OwlStatue(0x10A)).connect(dungeon1_right_side, STONE_BEAK1)
dungeon1_3_of_a_kind = Location(dungeon=1).add(DungeonChest(0x10A)).connect(dungeon1_right_side, OR(r.attack_hookshot_no_bomb, SHIELD)) # three of a kind, shield stops the suit from changing dungeon1_3_of_a_kind = Location(dungeon=1).add(DungeonChest(0x10A)).connect(dungeon1_right_side, OR(r.attack_hookshot_no_bomb, 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_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) dungeon1_boss = Location(dungeon=1).connect(dungeon1_miniboss, NIGHTMARE_KEY1)
boss = Location(dungeon=1).add(HeartContainer(0x106), Instrument(0x102)).connect(dungeon1_boss, r.boss_requirements[world_setup.boss_mapping[0]]) boss = Location(dungeon=1).add(HeartContainer(0x106), Instrument(0x102)).connect(dungeon1_boss, r.boss_requirements[world_setup.boss_mapping[0]])
@@ -30,7 +30,7 @@ class Dungeon1:
if options.logic == 'glitched' or options.logic == 'hell': if options.logic == 'glitched' or options.logic == 'hell':
boss_key.connect(entrance, r.super_jump_feather) # super jump boss_key.connect(entrance, r.super_jump_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 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': 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 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

View File

@@ -23,7 +23,7 @@ class Dungeon2:
dungeon2_r5 = Location(dungeon=2).connect(dungeon2_r4, AND(KEY2, FOUND(KEY2, 3))) # push two blocks together room with owl statue 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": if options.owlstatues == "both" or options.owlstatues == "dungeon":
Location(dungeon=2).add(OwlStatue(0x12F)).connect(dungeon2_r5, STONE_BEAK2) # owl statue is before miniboss 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 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": if options.owlstatues == "both" or options.owlstatues == "dungeon":
Location(dungeon=2).add(OwlStatue(0x129)).connect(miniboss, STONE_BEAK2) # owl statue after the miniboss Location(dungeon=2).add(OwlStatue(0x129)).connect(miniboss, STONE_BEAK2) # owl statue after the miniboss
@@ -45,7 +45,7 @@ class Dungeon2:
dungeon2_map_chest.connect(dungeon2_l2, AND(r.attack_hookshot_powder, r.boots_bonk_pit)) # use boots to jump over the pits dungeon2_map_chest.connect(dungeon2_l2, AND(r.attack_hookshot_powder, r.boots_bonk_pit)) # use boots to jump over the pits
dungeon2_r4.connect(dungeon2_r3, OR(r.boots_bonk_pit, r.hookshot_spam_pit)) # can use both pegasus boots bonks or hookshot spam to cross the pit room dungeon2_r4.connect(dungeon2_r3, OR(r.boots_bonk_pit, r.hookshot_spam_pit)) # 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 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(r.boots_dash_2d, r.miniboss_requirements[world_setup.miniboss_mapping[1]])) # use boots to dash over the spikes in the 2d section miniboss.connect(dungeon2_r5, AND(r.boots_dash_2d, 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_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, r.boots_jump)) # use a bomb to lower the last platform, or boots + feather to cross over top (only relevant in hell logic) dungeon2_post_stairs_boss.connect(dungeon2_pre_stairs_boss, OR(BOMB, r.boots_jump)) # 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(r.boots_bonk_pit, r.hookshot_spam_pit)) # boots bonk off bottom wall + hookshot spam across the two 1 tile pits vertically dungeon2_pre_boss.connect(dungeon2_post_stairs_boss, AND(r.boots_bonk_pit, r.hookshot_spam_pit)) # boots bonk off bottom wall + hookshot spam across the two 1 tile pits vertically

View File

@@ -33,7 +33,7 @@ class Dungeon3:
Location(dungeon=3).add(DroppedKey(0x14D)).connect(area_right, r.attack_hookshot_powder) # key after the stairs. 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_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 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 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 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(DroppedKey(0x148)).connect(area_right, r.attack_no_boomerang) # 2 zol 2 owl drop key

View File

@@ -29,7 +29,7 @@ class Dungeon4:
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 = 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(0x16D)) # gel chest
left_water_area.add(DungeonChest(0x168)) # key chest near the puzzle 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]])) 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 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 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.add(DungeonChest(0x160)) # flippers chest

View File

@@ -15,7 +15,7 @@ class Dungeon5:
Location(dungeon=5).add(OwlStatue(0x19A)).connect(area2, STONE_BEAK5) Location(dungeon=5).add(OwlStatue(0x19A)).connect(area2, STONE_BEAK5)
Location(dungeon=5).add(DungeonChest(0x19B)).connect(area2, r.attack_hookshot_powder) # map chest 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 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 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 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_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 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

View File

@@ -24,7 +24,7 @@ class Dungeon6:
# right side # right side
to_miniboss = Location(dungeon=6).connect(entrance, KEY6) 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]])) 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(r.attack_wizrobe, COUNT(POWER_BRACELET, 2))) # waterway key lower_right_side = Location(dungeon=6).add(DungeonChest(0x1BE)).connect(entrance, AND(r.attack_wizrobe, COUNT(POWER_BRACELET, 2))) # waterway key
medicine_chest = Location(dungeon=6).add(DungeonChest(0x1D1)).connect(lower_right_side, FEATHER) # ledge chest medicine medicine_chest = Location(dungeon=6).add(DungeonChest(0x1D1)).connect(lower_right_side, FEATHER) # ledge chest medicine
if options.owlstatues == "both" or options.owlstatues == "dungeon": if options.owlstatues == "both" or options.owlstatues == "dungeon":

View File

@@ -22,7 +22,7 @@ class Dungeon7:
# Most of the dungeon can be accessed at this point. # Most of the dungeon can be accessed at this point.
if options.owlstatues == "both" or options.owlstatues == "dungeon": if options.owlstatues == "both" or options.owlstatues == "dungeon":
bottomleft_owl = Location(dungeon=7).add(OwlStatue(0x21C)).connect(bottomleftF2_area, AND(BOMB, STONE_BEAK7)) 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 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.hit_switch) # mirror shield chest, need to be able to hit a switch to reach or mirror_shield_chest = Location(dungeon=7).add(DungeonChest(0x21A)).connect(bottomleftF2_area, r.hit_switch) # 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 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.hit_switch) # chest on the F1 right ledge. Added attack_hookshot since switch needs to be hit to get back up toprightF1_chest = Location(dungeon=7).add(DungeonChest(0x204)).connect(bottomleftF2_area, r.hit_switch) # chest on the F1 right ledge. Added attack_hookshot since switch needs to be hit to get back up

View File

@@ -40,7 +40,7 @@ class Dungeon8:
middle_center_2 = Location(dungeon=8).connect(middle_center_1, AND(KEY8, FOUND(KEY8, 4))) 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) 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_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 = 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 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)))) up_left = Location(dungeon=8).connect(upper_center, AND(r.attack_hookshot_powder, AND(KEY8, FOUND(KEY8, 4))))
@@ -94,7 +94,7 @@ class Dungeon8:
entrance.connect(bottomright_pot_chest, r.shaq_jump, one_way=True) # use NW zamboni staircase backwards, and get a naked shaq jump off the bottom wall in the bottom right corner to pass by the pot entrance.connect(bottomright_pot_chest, r.shaq_jump, one_way=True) # use NW zamboni staircase backwards, and get a naked shaq jump off the bottom wall in the bottom right corner to pass by the pot
gibdos_drop_key.connect(upper_center, AND(FEATHER, SHIELD)) # lock gibdos into pits and crack the tile they stand on, then use shield to bump them into the pit gibdos_drop_key.connect(upper_center, AND(FEATHER, SHIELD)) # lock gibdos into pits and crack the tile they stand on, then use shield to bump them into the pit
medicine_chest.connect(upper_center, AND(r.pit_buffer_boots, HOOKSHOT)) # boots bonk + lava buffer to the bottom wall, then bonk onto the middle section medicine_chest.connect(upper_center, AND(r.pit_buffer_boots, HOOKSHOT)) # boots bonk + lava buffer to the bottom wall, then bonk onto the middle section
miniboss.connect(miniboss_entrance, AND(r.boots_bonk_2d_hell, r.miniboss_requirements[world_setup.miniboss_mapping[7]])) # get through 2d section with boots bonks miniboss.connect(miniboss_entrance, AND(r.boots_bonk_2d_hell, r.miniboss_requirements[world_setup.miniboss_mapping['7']])) # get through 2d section with boots bonks
top_left_stairs.connect(map_chest, AND(r.jesus_buffer, r.boots_bonk_2d_hell, MAGIC_ROD)) # boots bonk + lava buffer from map chest to entrance_up, then boots bonk through 2d section top_left_stairs.connect(map_chest, AND(r.jesus_buffer, r.boots_bonk_2d_hell, 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(r.boots_bonk_pit, SWORD, FOUND(KEY8, 7))) # use a boots bonk to cross the 2d section + the lava in cueball room nightmare_key.connect(top_left_stairs, AND(r.boots_bonk_pit, 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, r.jesus_buffer), 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 bottom_right.connect(entrance_up, AND(POWER_BRACELET, r.jesus_buffer), 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

View File

@@ -541,8 +541,8 @@ OAMData:
rom.banks[0x38][0x1400+n*0x20:0x1410+n*0x20] = utils.createTileData(gfx_high) rom.banks[0x38][0x1400+n*0x20:0x1410+n*0x20] = utils.createTileData(gfx_high)
rom.banks[0x38][0x1410+n*0x20:0x1420+n*0x20] = utils.createTileData(gfx_low) rom.banks[0x38][0x1410+n*0x20:0x1420+n*0x20] = utils.createTileData(gfx_low)
def addBootsControls(rom, boots_controls: int): def addBootsControls(rom, bootscontrols):
if boots_controls == BootsControls.option_vanilla: if bootscontrols == 'vanilla':
return return
consts = { consts = {
"INVENTORY_PEGASUS_BOOTS": 0x8, "INVENTORY_PEGASUS_BOOTS": 0x8,
@@ -560,25 +560,25 @@ def addBootsControls(rom, boots_controls: int):
BOOTS_START_ADDR = 0x11E8 BOOTS_START_ADDR = 0x11E8
condition = { condition = {
BootsControls.option_bracelet: """ 'bracelet': """
ld a, [hl] ld a, [hl]
; Check if we are using the bracelet ; Check if we are using the bracelet
cp INVENTORY_POWER_BRACELET cp INVENTORY_POWER_BRACELET
jr z, .yesBoots jr z, .yesBoots
""", """,
BootsControls.option_press_a: """ 'pressa': """
; Check if we are using the A slot ; Check if we are using the A slot
cp J_A cp J_A
jr z, .yesBoots jr z, .yesBoots
ld a, [hl] ld a, [hl]
""", """,
BootsControls.option_press_b: """ 'pressb': """
; Check if we are using the B slot ; Check if we are using the B slot
cp J_B cp J_B
jr z, .yesBoots jr z, .yesBoots
ld a, [hl] ld a, [hl]
""" """
}[boots_controls] }[bootscontrols]
# The new code fits exactly within Nintendo's poorly space optimzied code while having more features # The new code fits exactly within Nintendo's poorly space optimzied code while having more features
boots_code = assembler.ASM(""" boots_code = assembler.ASM("""

View File

@@ -387,7 +387,7 @@ def patchVarious(rom, settings):
# Boomerang trade guy # Boomerang trade guy
# if settings.boomerang not in {'trade', 'gift'} or settings.overworld in {'normal', 'nodungeons'}: # if settings.boomerang not in {'trade', 'gift'} or settings.overworld in {'normal', 'nodungeons'}:
if settings["tradequest"]: if settings.tradequest:
# Update magnifier checks # Update magnifier checks
rom.patch(0x19, 0x05EC, ASM("ld a, [wTradeSequenceItem]\ncp $0E\njp nz, $7E61"), ASM("ld a, [wTradeSequenceItem2]\nand $20\njp z, $7E61")) # show the guy rom.patch(0x19, 0x05EC, ASM("ld a, [wTradeSequenceItem]\ncp $0E\njp nz, $7E61"), ASM("ld a, [wTradeSequenceItem2]\nand $20\njp z, $7E61")) # show the guy
rom.patch(0x00, 0x3199, ASM("ld a, [wTradeSequenceItem]\ncp $0E\njr nz, $06"), ASM("ld a, [wTradeSequenceItem2]\nand $20\njr z, $06")) # load the proper room layout rom.patch(0x00, 0x3199, ASM("ld a, [wTradeSequenceItem]\ncp $0E\njr nz, $06"), ASM("ld a, [wTradeSequenceItem2]\nand $20\njr z, $06")) # load the proper room layout

View File

@@ -68,7 +68,7 @@ class Setting:
class Settings: class Settings:
def __init__(self, ap_options): def __init__(self, settings_dict):
self.__all = [ self.__all = [
Setting('seed', 'Main', '<', 'Seed', placeholder='Leave empty for random seed', default="", multiworld=False, Setting('seed', 'Main', '<', 'Seed', placeholder='Leave empty for random seed', default="", multiworld=False,
description="""For multiple people to generate the same randomization result, enter the generated seed number here. description="""For multiple people to generate the same randomization result, enter the generated seed number here.
@@ -178,6 +178,14 @@ Note, some entrances can lead into water, use the warp-to-home from the save&qui
description='Replaces the hints from owl statues with additional randomized items'), description='Replaces the hints from owl statues with additional randomized items'),
Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False, Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False,
description='All items will be more powerful, faster, harder, bigger stronger. You name it.'), description='All items will be more powerful, faster, harder, bigger stronger. You name it.'),
Setting('trendygame', 'Special', 'a', 'Trendy Game', description="",
options=[('easy', 'e', 'Easy'), ('normal', 'n', 'Normal'), ('hard', 'h', 'Hard'), ('harder', 'r', 'Harder'), ('hardest', 't', 'Hardest'), ('impossible', 'i', 'Impossible')], default='normal'),
Setting('warps', 'Special', 'a', 'Warps', description="",
options=[('vanilla', 'v', 'Vanilla'), ('improved', 'i', 'Improved'), ('improvedadditional', 'a', 'Improved Additional')], default='vanilla'),
Setting('shufflenightmarekeys', 'Special', 'a', 'Shuffle Nightmare Keys', description="",
options=[('originaldungeon', '0', 'Original Dungeon'), ('owndungeons', '1', 'Own Dungeons'), ('ownworld', '2', 'Own World'), ('anyworld', '3', 'Any World'), ('differentworld', '4', 'Different World')], default="originaldungeon"),
Setting('shufflesmallkeys', 'Special', 'a', 'Shuffle Small Keys', description="",
options=[('originaldungeon', '0', 'Original Dungeon'), ('owndungeons', '1', 'Own Dungeons'), ('ownworld', '2', 'Own World'), ('anyworld', '3', 'Any World'), ('differentworld', '4', 'Different World')], default="originaldungeon"),
Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none', Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none',
description='Adds that the select button swaps with either A or B. The item is swapped with the top inventory slot. The map is not available when quickswap is enabled.', description='Adds that the select button swaps with either A or B. The item is swapped with the top inventory slot. The map is not available when quickswap is enabled.',
aesthetic=True), aesthetic=True),
@@ -192,7 +200,7 @@ Note, some entrances can lead into water, use the warp-to-home from the save&qui
Setting('nagmessages', 'User options', 'S', 'Show nag messages', default=False, Setting('nagmessages', 'User options', 'S', 'Show nag messages', default=False,
description='Enables the nag messages normally shown when touching stones and crystals', description='Enables the nag messages normally shown when touching stones and crystals',
aesthetic=True), aesthetic=True),
Setting('gfxmod', 'User options', 'c', 'Graphics', default='', Setting('gfxmod', 'User options', 'c', 'Graphics', default=False,
description='Generally affects at least Link\'s sprite, but can alter any graphics in the game', description='Generally affects at least Link\'s sprite, but can alter any graphics in the game',
aesthetic=True), aesthetic=True),
Setting('linkspalette', 'User options', 'C', "Link's color", Setting('linkspalette', 'User options', 'C', "Link's color",
@@ -202,25 +210,31 @@ Note, some entrances can lead into water, use the warp-to-home from the save&qui
[Normal] color of link depends on the tunic. [Normal] color of link depends on the tunic.
[Green/Yellow/Red/Blue] forces link into one of these colors. [Green/Yellow/Red/Blue] forces link into one of these colors.
[?? A/B/C/D] colors of link are usually inverted and color depends on the area you are in."""), [?? A/B/C/D] colors of link are usually inverted and color depends on the area you are in."""),
Setting('palette', 'User options', 'a', 'Palette', description="",
options=[('normal', 'n', 'Normal'), ('1bit', '1', '1 Bit'), ('2bit', '2', '2 Bit'), ('greyscale', 'g', 'Greyscale'), ('pink', 'p', 'Pink'), ('inverted', 'i', 'Inverted')], default='normal', aesthetic=True),
Setting('music', 'User options', 'M', 'Music', options=[('', '', 'Default'), ('random', 'r', 'Random'), ('off', 'o', 'Disable')], default='', Setting('music', 'User options', 'M', 'Music', options=[('', '', 'Default'), ('random', 'r', 'Random'), ('off', 'o', 'Disable')], default='',
description=""" description="""
[Random] Randomizes overworld and dungeon music' [Random] Randomizes overworld and dungeon music'
[Disable] no music in the whole game""", [Disable] no music in the whole game""",
aesthetic=True), aesthetic=True),
Setting('musicchange', 'User options', 'a', 'Music Change Condition', description="",
options=[('always', 'a', 'Always'), ('sword', 's', 'Sword')], default='always', aesthetic=True),
Setting('bootscontrols', 'User options', 'a', 'Boots Controls', description="",
options=[('vanilla', 'v', 'Vanilla'), ('bracelet', 'p', 'Bracelet'), ('pressa', 'a', 'Press A'), ('pressb', 'b', 'Press B')], default='vanilla', aesthetic=True),
Setting('foreignitemicons', 'User options', 'a', 'Foreign Item Icons', description="",
options=[('guessbyname', 'g', 'Guess By Name'), ('indicateprogression', 'p', 'Indicate Progression')], default="guessbyname", aesthetic=True),
Setting('aptitlescreen', 'User options', 'a', 'AP Title Screen', description="", default=True),
Setting('textshuffle', 'User options', 'a', 'Text Shuffle', description="", default=False),
] ]
self.__by_key = {s.key: s for s in self.__all} self.__by_key = {s.key: s for s in self.__all}
# Make sure all short keys are unique # don't worry about unique short keys for AP
short_keys = set() #short_keys = set()
for s in self.__all: #for s in self.__all:
assert s.short_key not in short_keys, s.label # assert s.short_key not in short_keys, s.label
short_keys.add(s.short_key) # short_keys.add(s.short_key)
self.ap_options = ap_options
for option in self.ap_options.values(): for name, value in settings_dict.items():
if not hasattr(option, 'to_ladxr_option'):
continue
name, value = option.to_ladxr_option(self.ap_options)
if value == "true": if value == "true":
value = 1 value = 1
elif value == "false": elif value == "false":

View File

@@ -28,7 +28,7 @@ class WorldSetup:
self.boss_mapping = list(range(9)) self.boss_mapping = list(range(9))
self.miniboss_mapping = { self.miniboss_mapping = {
# Main minibosses # Main minibosses
0: "ROLLING_BONES", 1: "HINOX", 2: "DODONGO", 3: "CUE_BALL", 4: "GHOMA", 5: "SMASHER", 6: "GRIM_CREEPER", 7: "BLAINO", '0': "ROLLING_BONES", '1': "HINOX", '2': "DODONGO", '3': "CUE_BALL", '4': "GHOMA", '5': "SMASHER", '6': "GRIM_CREEPER", '7': "BLAINO",
# Color dungeon needs to be special, as always. # Color dungeon needs to be special, as always.
"c1": "AVALAUNCH", "c2": "GIANT_BUZZ_BLOB", "c1": "AVALAUNCH", "c2": "GIANT_BUZZ_BLOB",
# Overworld # Overworld

View File

@@ -60,11 +60,12 @@ class TradeQuest(DefaultOffToggle, LADXROption):
ladxr_name = "tradequest" ladxr_name = "tradequest"
class TextShuffle(DefaultOffToggle): class TextShuffle(DefaultOffToggle, LADXROption):
""" """
Shuffles all text in the game. Shuffles all text in the game.
""" """
display_name = "Text Shuffle" display_name = "Text Shuffle"
ladxr_name = "textshuffle"
class Rooster(DefaultOnToggle, LADXROption): class Rooster(DefaultOnToggle, LADXROption):
@@ -112,11 +113,12 @@ class DungeonShuffle(DefaultOffToggle, LADXROption):
ladxr_name = "dungeonshuffle" ladxr_name = "dungeonshuffle"
class APTitleScreen(DefaultOnToggle): class APTitleScreen(DefaultOnToggle, LADXROption):
""" """
Enables AP specific title screen and disables the intro cutscene. Enables AP specific title screen and disables the intro cutscene.
""" """
display_name = "AP Title Screen" display_name = "AP Title Screen"
ladxr_name = "aptitlescreen"
class BossShuffle(Choice): class BossShuffle(Choice):
@@ -142,7 +144,7 @@ class DungeonItemShuffle(Choice):
ladxr_item: str ladxr_item: str
class ShuffleNightmareKeys(DungeonItemShuffle): class ShuffleNightmareKeys(DungeonItemShuffle, LADXROption):
""" """
**Original Dungeon:** The item will be within its original dungeon. **Original Dungeon:** The item will be within its original dungeon.
@@ -156,9 +158,10 @@ class ShuffleNightmareKeys(DungeonItemShuffle):
""" """
display_name = "Shuffle Nightmare Keys" display_name = "Shuffle Nightmare Keys"
ladxr_item = "NIGHTMARE_KEY" ladxr_item = "NIGHTMARE_KEY"
ladxr_name = "shufflenightmarekeys"
class ShuffleSmallKeys(DungeonItemShuffle): class ShuffleSmallKeys(DungeonItemShuffle, LADXROption):
""" """
**Original Dungeon:** The item will be within its original dungeon. **Original Dungeon:** The item will be within its original dungeon.
@@ -172,6 +175,7 @@ class ShuffleSmallKeys(DungeonItemShuffle):
""" """
display_name = "Shuffle Small Keys" display_name = "Shuffle Small Keys"
ladxr_item = "KEY" ladxr_item = "KEY"
ladxr_name = "shufflesmallkeys"
class ShuffleMaps(DungeonItemShuffle): class ShuffleMaps(DungeonItemShuffle):
@@ -266,7 +270,7 @@ class Goal(Choice, LADXROption):
def to_ladxr_option(self, all_options): def to_ladxr_option(self, all_options):
if self.value == self.option_instruments: if self.value == self.option_instruments:
return ("goal", all_options["instrument_count"]) return ("goal", int(all_options["instrument_count"]))
else: else:
return LADXROption.to_ladxr_option(self, all_options) return LADXROption.to_ladxr_option(self, all_options)
@@ -291,7 +295,7 @@ class NagMessages(DefaultOffToggle, LADXROption):
ladxr_name = "nagmessages" ladxr_name = "nagmessages"
class MusicChangeCondition(Choice): class MusicChangeCondition(Choice, LADXROption):
""" """
Controls how the music changes. Controls how the music changes.
@@ -304,6 +308,7 @@ class MusicChangeCondition(Choice):
option_sword = 0 option_sword = 0
option_always = 1 option_always = 1
default = option_always default = option_always
ladxr_name = "musicchange"
class HardMode(Choice, LADXROption): class HardMode(Choice, LADXROption):
@@ -396,7 +401,7 @@ class NoFlash(DefaultOnToggle, LADXROption):
ladxr_name = "noflash" ladxr_name = "noflash"
class BootsControls(Choice): class BootsControls(Choice, LADXROption):
""" """
Adds an additional button to activate Pegasus Boots (does nothing if you Adds an additional button to activate Pegasus Boots (does nothing if you
haven't picked up your boots!) haven't picked up your boots!)
@@ -418,6 +423,7 @@ class BootsControls(Choice):
alias_a = 2 alias_a = 2
option_press_b = 3 option_press_b = 3
alias_b = 3 alias_b = 3
ladxr_name = "bootscontrols"
class LinkPalette(Choice, LADXROption): class LinkPalette(Choice, LADXROption):
@@ -444,7 +450,7 @@ class LinkPalette(Choice, LADXROption):
return self.ladxr_name, str(self.value) return self.ladxr_name, str(self.value)
class TrendyGame(Choice): class TrendyGame(Choice, LADXROption):
""" """
**Easy:** All of the items hold still for you. **Easy:** All of the items hold still for you.
@@ -468,16 +474,18 @@ class TrendyGame(Choice):
option_hardest = 4 option_hardest = 4
option_impossible = 5 option_impossible = 5
default = option_normal default = option_normal
ladxr_name = "trendygame"
class GfxMod(DefaultOffToggle): class GfxMod(DefaultOffToggle, LADXROption):
""" """
If enabled, the patcher will prompt the user for a modification file to change sprites in the game and optionally some text. If enabled, the patcher will prompt the user for a modification file to change sprites in the game and optionally some text.
""" """
display_name = "GFX Modification" display_name = "GFX Modification"
ladxr_name = "gfxmod"
class Palette(Choice): class Palette(Choice, LADXROption):
""" """
Sets the palette for the game. Sets the palette for the game.
@@ -504,6 +512,7 @@ class Palette(Choice):
option_greyscale = 3 option_greyscale = 3
option_pink = 4 option_pink = 4
option_inverted = 5 option_inverted = 5
ladxr_name = "palette"
class Music(Choice, LADXROption): class Music(Choice, LADXROption):
@@ -530,7 +539,7 @@ class Music(Choice, LADXROption):
return self.ladxr_name, s return self.ladxr_name, s
class Warps(Choice): class Warps(Choice, LADXROption):
""" """
**Improved:** Adds remake style warp screen to the game. Choose your warp **Improved:** Adds remake style warp screen to the game. Choose your warp
destination on the map after jumping in a portal and press *B* to select. destination on the map after jumping in a portal and press *B* to select.
@@ -544,6 +553,7 @@ class Warps(Choice):
option_improved = 1 option_improved = 1
option_improved_additional = 2 option_improved_additional = 2
default = option_vanilla default = option_vanilla
ladxr_name = 'warps'
class InGameHints(DefaultOnToggle): class InGameHints(DefaultOnToggle):
@@ -583,7 +593,7 @@ class StabilizeItemPool(DefaultOffToggle):
rich_text_doc = True rich_text_doc = True
class ForeignItemIcons(Choice): class ForeignItemIcons(Choice, LADXROption):
""" """
Choose how to display foreign items. Choose how to display foreign items.
@@ -597,6 +607,7 @@ class ForeignItemIcons(Choice):
option_guess_by_name = 0 option_guess_by_name = 0
option_indicate_progression = 1 option_indicate_progression = 1
default = option_guess_by_name default = option_guess_by_name
ladxr_name = 'foreignitemicons'
ladx_option_groups = [ ladx_option_groups = [

View File

@@ -6,13 +6,11 @@ import json
import pkgutil import pkgutil
import bsdiff4 import bsdiff4
import binascii import binascii
import pickle
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from .Common import * from .Common import *
from .LADXR import generator from .LADXR import generator
from .LADXR.main import get_parser from .LADXR.main import get_parser
from .LADXR.hints import generate_hint_texts from .LADXR.hints import generate_hint_texts
from .LADXR.locations.keyLocation import KeyLocation
LADX_HASH = "07c211479386825042efb4ad31bb525f" LADX_HASH = "07c211479386825042efb4ad31bb525f"
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -35,7 +33,7 @@ class LADXPatchExtensions(worlds.Files.APPatchExtension):
@staticmethod @staticmethod
def patch_title_screen(caller: worlds.Files.APProcedurePatch, rom: bytes, data_file: str) -> bytes: def patch_title_screen(caller: worlds.Files.APProcedurePatch, rom: bytes, data_file: str) -> bytes:
patch_data = json.loads(caller.get_file(data_file).decode("utf-8")) patch_data = json.loads(caller.get_file(data_file).decode("utf-8"))
if patch_data["options"]["ap_title_screen"]: if patch_data["ladxr_settings_dict"]["aptitlescreen"] == 'true':
return bsdiff4.patch(rom, pkgutil.get_data(__name__, "LADXR/patches/title_screen.bdiff4")) return bsdiff4.patch(rom, pkgutil.get_data(__name__, "LADXR/patches/title_screen.bdiff4"))
return rom return rom
@@ -56,7 +54,6 @@ class LADXProcedurePatch(worlds.Files.APProcedurePatch):
def write_patch_data(world: "LinksAwakeningWorld", patch: LADXProcedurePatch): def write_patch_data(world: "LinksAwakeningWorld", patch: LADXProcedurePatch):
item_list = pickle.dumps([item for item in world.ladxr_logic.iteminfo_list if not isinstance(item, KeyLocation)])
data_dict = { data_dict = {
"generated_world_version": world.world_version.as_simple_string(), "generated_world_version": world.world_version.as_simple_string(),
"out_base": world.multiworld.get_out_file_name_base(patch.player), "out_base": world.multiworld.get_out_file_name_base(patch.player),
@@ -67,44 +64,16 @@ def write_patch_data(world: "LinksAwakeningWorld", patch: LADXProcedurePatch):
"player": patch.player, "player": patch.player,
"player_name": patch.player_name, "player_name": patch.player_name,
"other_player_names": list(world.multiworld.player_name.values()), "other_player_names": list(world.multiworld.player_name.values()),
"item_list": binascii.hexlify(item_list).decode(), "rom_item_placements": world.rom_item_placements,
"hint_texts": generate_hint_texts(world), "hint_texts": generate_hint_texts(world),
"world_setup": { "world_setup": {
"goal": world.ladxr_logic.world_setup.goal, "goal": world.ladxr_logic.world_setup.goal,
"bingo_goals": world.ladxr_logic.world_setup.bingo_goals,
"multichest": world.ladxr_logic.world_setup.multichest, "multichest": world.ladxr_logic.world_setup.multichest,
"entrance_mapping": world.ladxr_logic.world_setup.entrance_mapping, "entrance_mapping": world.ladxr_logic.world_setup.entrance_mapping,
"boss_mapping": world.ladxr_logic.world_setup.boss_mapping, "boss_mapping": world.ladxr_logic.world_setup.boss_mapping,
"miniboss_mapping": world.ladxr_logic.world_setup.miniboss_mapping, "miniboss_mapping": world.ladxr_logic.world_setup.miniboss_mapping,
}, },
"options": world.options.as_dict( "ladxr_settings_dict": world.ladxr_settings_dict,
"tradequest",
"rooster",
"experimental_dungeon_shuffle",
"experimental_entrance_shuffle",
"goal",
"instrument_count",
"link_palette",
"warps",
"trendy_game",
"gfxmod",
"palette",
"text_shuffle",
"shuffle_nightmare_keys",
"shuffle_small_keys",
"music",
"music_change_condition",
"nag_messages",
"ap_title_screen",
"boots_controls",
"stealing",
"quickswap",
"hard_mode",
"low_hp_beep",
"text_mode",
"no_flash",
"overworld",
),
} }
patch.write_file("data.json", json.dumps(data_dict).encode('utf-8')) patch.write_file("data.json", json.dumps(data_dict).encode('utf-8'))

View File

@@ -180,7 +180,17 @@ class LinksAwakeningWorld(World):
} }
def convert_ap_options_to_ladxr_logic(self): def convert_ap_options_to_ladxr_logic(self):
self.ladxr_settings = LADXRSettings(dataclasses.asdict(self.options)) # store a dict of ladxr settings as a middle step so that we can also create a
# ladxr settings object on the other side of the patch
options_dict = dataclasses.asdict(self.options)
self.ladxr_settings_dict = {}
for option in options_dict.values():
if not hasattr(option, 'to_ladxr_option'):
continue
name, value = option.to_ladxr_option(options_dict)
if name:
self.ladxr_settings_dict[name] = value
self.ladxr_settings = LADXRSettings(self.ladxr_settings_dict)
self.ladxr_settings.validate() self.ladxr_settings.validate()
world_setup = LADXRWorldSetup() world_setup = LADXRWorldSetup()
@@ -503,36 +513,36 @@ class LinksAwakeningWorld(World):
return "TRADING_ITEM_LETTER" return "TRADING_ITEM_LETTER"
def generate_output(self, output_directory: str): def generate_output(self, output_directory: str):
# copy items back to locations self.rom_item_placements = []
for r in self.multiworld.get_regions(self.player): for r in self.multiworld.get_regions(self.player):
for loc in r.locations: for loc in r.locations:
if isinstance(loc, LinksAwakeningLocation): if isinstance(loc, LinksAwakeningLocation):
assert(loc.item) assert(loc.item)
spot = {}
# If we're a links awakening item, just use the item # If we're a links awakening item, just use the item
if isinstance(loc.item, LinksAwakeningItem): if isinstance(loc.item, LinksAwakeningItem):
loc.ladxr_item.item = loc.item.item_data.ladxr_id spot["item"] = loc.item.item_data.ladxr_id
# If the item name contains "sword", use a sword icon, etc # If the item name contains "sword", use a sword icon, etc
# Otherwise, use a cute letter as the icon # Otherwise, use a cute letter as the icon
elif self.options.foreign_item_icons == 'guess_by_name': elif self.options.foreign_item_icons == 'guess_by_name':
loc.ladxr_item.item = self.guess_icon_for_other_world(loc.item) spot["item"] = self.guess_icon_for_other_world(loc.item)
loc.ladxr_item.setCustomItemName(loc.item.name)
else: else:
if loc.item.advancement: if loc.item.advancement:
loc.ladxr_item.item = 'PIECE_OF_POWER' spot["item"] = 'PIECE_OF_POWER'
else: else:
loc.ladxr_item.item = 'GUARDIAN_ACORN' spot["item"] = 'GUARDIAN_ACORN'
loc.ladxr_item.setCustomItemName(loc.item.name)
spot["custom_item_name"] = loc.item.name
if loc.item: if loc.item:
loc.ladxr_item.item_owner = loc.item.player spot["item_owner"] = loc.item.player
else: else:
loc.ladxr_item.item_owner = self.player spot["item_owner"] = self.player
# Kind of kludge, make it possible for the location to differentiate between local and remote items spot["name_id"] = loc.ladxr_item.nameId
loc.ladxr_item.location_owner = self.player self.rom_item_placements.append(spot)
patch = LADXProcedurePatch(player=self.player, player_name=self.player_name) patch = LADXProcedurePatch(player=self.player, player_name=self.player_name)