LADX: no pickle (#5849)

This commit is contained in:
Fabian Dill
2026-01-19 21:28:25 +01:00
committed by GitHub
parent e1322df8b0
commit 646a52a2e7
16 changed files with 185 additions and 168 deletions

View File

@@ -2,7 +2,6 @@ import binascii
import importlib.util
import importlib.machinery
import random
import pickle
import Utils
from collections import defaultdict
from typing import Dict
@@ -61,7 +60,11 @@ from .patches import bank34
from .roomEditor import RoomEditor, Object
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):
pass
@@ -86,8 +89,27 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
random.seed(patch_data["seed"] + patch_data["player"])
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 = ROMWithTables(base_rom, rom_patches)
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:
pymod.prePatch(rom)
if options["gfxmod"]:
if ladxr_settings.gfxmod:
try:
gfx_mod_file = LinksAwakeningWorld.settings.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("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.save.singleSaveSlot(rom)
@@ -159,17 +181,16 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
patches.core.alwaysAllowSecretBook(rom)
patches.core.injectMainLoop(rom)
if options["shuffle_small_keys"] != Options.ShuffleSmallKeys.option_original_dungeon or\
options["shuffle_nightmare_keys"] != Options.ShuffleNightmareKeys.option_original_dungeon:
if ladxr_settings.shufflesmallkeys != 'originaldungeon' or ladxr_settings.shufflenightmarekeys != 'originaldungeon':
patches.inventory.advancedInventorySubscreen(rom)
patches.inventory.moreSlots(rom)
# if ladxr_settings["witch"]:
# if ladxr_settings.witch:
patches.witch.updateWitch(rom)
patches.softlock.fixAll(rom)
if not options["rooster"]:
if not ladxr_settings.rooster:
patches.maptweaks.tweakMap(rom)
patches.maptweaks.tweakBirdKeyRoom(rom)
if options["overworld"] == Options.Overworld.option_open_mabe:
if ladxr_settings.overworld == 'openmabe':
patches.maptweaks.openMabe(rom)
patches.chest.fixChests(rom)
patches.shop.fixShop(rom)
@@ -181,9 +202,9 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
patches.tarin.updateTarin(rom)
patches.fishingMinigame.updateFinishingMinigame(rom)
patches.health.upgradeHealthContainers(rom)
# if ladxr_settings["owlstatues"] in ("dungeon", "both"):
# if ladxr_settings.owlstatues in ("dungeon", "both"):
# patches.owl.upgradeDungeonOwlStatues(rom)
# if ladxr_settings["owlstatues"] in ("overworld", "both"):
# if ladxr_settings.owlstatues in ("overworld", "both"):
# patches.owl.upgradeOverworldOwlStatues(rom)
patches.goldenLeaf.fixGoldenLeaf(rom)
patches.heartPiece.fixHeartPiece(rom)
@@ -194,17 +215,17 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
patches.songs.upgradeManbo(rom)
patches.songs.upgradeMamu(rom)
patches.tradeSequence.patchTradeSequence(rom, options)
patches.tradeSequence.patchTradeSequence(rom, ladxr_settings)
patches.bowwow.fixBowwow(rom, everywhere=False)
# if ladxr_settings["bowwow"] != 'normal':
# if ladxr_settings.bowwow != 'normal':
# patches.bowwow.bowwowMapPatches(rom)
patches.desert.desertAccess(rom)
# if ladxr_settings["overworld"] == 'dungeondive':
# if ladxr_settings.overworld == 'dungeondive':
# patches.overworld.patchOverworldTilesets(rom)
# patches.overworld.createDungeonOnlyOverworld(rom)
# elif ladxr_settings["overworld"] == 'nodungeons':
# elif ladxr_settings.overworld == 'nodungeons':
# patches.dungeon.patchNoDungeons(rom)
#elif world.ladxr_settings["overworld"] == 'random':
#elif ladxr_settings.overworld == 'random':
# patches.overworld.patchOverworldTilesets(rom)
# mapgen.store_map(rom, world.ladxr_logic.world.map)
#if settings.dungeon_items == 'keysy':
@@ -212,102 +233,94 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
# patches.reduceRNG.slowdownThreeOfAKind(rom)
patches.reduceRNG.fixHorseHeads(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.reduceMessageLengths(rom, random)
patches.aesthetics.allowColorDungeonSpritesEverywhere(rom)
if options["music"] == Options.Music.option_shuffled:
if ladxr_settings.music == 'shuffled':
patches.music.randomizeMusic(rom, random)
elif options["music"] == Options.Music.option_off:
elif ladxr_settings.music == 'off':
patches.music.noMusic(rom)
if options["no_flash"]:
if ladxr_settings.noflash:
patches.aesthetics.removeFlashingLights(rom)
if options["hard_mode"] == Options.HardMode.option_oracle:
if ladxr_settings.hardmode == 'oracle':
patches.hardMode.oracleMode(rom)
elif options["hard_mode"] == Options.HardMode.option_hero:
elif ladxr_settings.hardmode == 'hero':
patches.hardMode.heroMode(rom)
elif options["hard_mode"] == Options.HardMode.option_ohko:
elif ladxr_settings.hardmode == 'ohko':
patches.hardMode.oneHitKO(rom)
#if ladxr_settings["superweapons"]:
#if ladxr_settings.superweapons:
# patches.weapons.patchSuperWeapons(rom)
if options["text_mode"] == Options.TextMode.option_fast:
if ladxr_settings.textmode == 'fast':
patches.aesthetics.fastText(rom)
#if ladxr_settings["textmode"] == 'none':
#if ladxr_settings.textmode == 'none':
# patches.aesthetics.fastText(rom)
# patches.aesthetics.noText(rom)
if not options["nag_messages"]:
if not ladxr_settings.nagmessages:
patches.aesthetics.removeNagMessages(rom)
if options["low_hp_beep"] == Options.LowHpBeep.option_slow:
if ladxr_settings.lowhpbeep == 'slow':
patches.aesthetics.slowLowHPBeep(rom)
if options["low_hp_beep"] == Options.LowHpBeep.option_none:
if ladxr_settings.lowhpbeep == 'none':
patches.aesthetics.removeLowHPBeep(rom)
if 0 <= options["link_palette"]:
patches.aesthetics.forceLinksPalette(rom, options["link_palette"])
if 0 <= int(ladxr_settings.linkspalette):
patches.aesthetics.forceLinksPalette(rom, int(ladxr_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 options["stealing"] == Options.Stealing.option_disabled:
if ladxr_settings.steal == 'disabled':
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[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)
#elif ladxr_settings["hpmode"] == '1':
#elif ladxr_settings.hpmode == '1':
# patches.health.setStartHealth(rom, 1)
patches.inventory.songSelectAfterOcarinaSelect(rom)
if options["quickswap"] == Options.Quickswap.option_a:
if ladxr_settings.quickswap == 'a':
patches.core.quickswap(rom, 1)
elif options["quickswap"] == Options.Quickswap.option_b:
elif ladxr_settings.quickswap == 'b':
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"])
hints.addHints(rom, random, patch_data["hint_texts"])
if patch_data["world_setup"]["goal"] == "raft":
if world_setup.goal == "raft":
patches.goal.setRaftGoal(rom)
elif patch_data["world_setup"]["goal"] in ("bingo", "bingo-full"):
patches.bingo.setBingoGoal(rom, patch_data["world_setup"]["bingo_goals"], patch_data["world_setup"]["goal"])
elif patch_data["world_setup"]["goal"] == "seashells":
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, patch_data["world_setup"]["goal"])
patches.goal.setRequiredInstrumentCount(rom, world_setup.goal)
# Patch the generated logic into the rom
patches.chest.setMultiChest(rom, patch_data["world_setup"]["multichest"])
#if ladxr_settings["overworld"] not in {"dungeondive", "random"}:
patches.entrances.changeEntrances(rom, patch_data["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 > 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"])
patches.chest.setMultiChest(rom, world_setup.multichest)
#if ladxr_settings.overworld not in {"dungeondive", "random"}:
patches.entrances.changeEntrances(rom, world_setup.entrance_mapping)
for ladxr_item in item_list:
ladxr_item.patch(rom, ladxr_item.item, multiworld=ladxr_item.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, patch_data)
if options["ap_title_screen"]:
if ladxr_settings.aptitlescreen:
patches.titleScreen.setTitleGraphics(rom)
patches.endscreen.updateEndScreen(rom)
patches.aesthetics.updateSpriteData(rom)
if args.doubletrouble:
patches.enemies.doubleTrouble(rom)
if options["text_shuffle"]:
if ladxr_settings.textshuffle:
excluded_ids = [
# Overworld owl statues
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
if options["trendy_game"] != Options.TrendyGame.option_normal:
if ladxr_settings.trendygame != 'normal':
# TODO: if 0 or 4, 5, remove inaccurate conveyor tiles
room_editor = RoomEditor(rom, 0x2A0)
if options["trendy_game"] == Options.TrendyGame.option_easy:
if ladxr_settings.trendygame == 'easy':
# Set physics flag on all objects
for i in range(0, 6):
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)
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::
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
"""
speeds = {
Options.TrendyGame.option_harder: (3, 8),
Options.TrendyGame.option_hardest: (3, 8),
Options.TrendyGame.option_impossible: (3, 16),
'harder': (3, 8),
'hardest': (3, 8),
'impossible': (3, 16),
}
def speed():
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][0x76A2-0x4000] = speed()
rom.banks[0x4][0x76A6-0x4000] = 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][0x76A3-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):
color[channel] = color[channel] * 31 // 0xbc
if options["warps"] != Options.Warps.option_vanilla:
patches.core.addWarpImprovements(rom, options["warps"] == Options.Warps.option_improved_additional)
if ladxr_settings.warps != 'vanilla':
patches.core.addWarpImprovements(rom, ladxr_settings.warps == 'improved_additional')
palette = options["palette"]
if palette != Options.Palette.option_normal:
palette = ladxr_settings.palette
if palette != 'normal':
ranges = {
# Object palettes
# Overworld palettes
@@ -462,22 +475,22 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
r,g,b = bin_to_rgb(packed)
# 1 bit
if palette == Options.Palette.option_1bit:
if palette == '1bit':
r &= 0b10000
g &= 0b10000
b &= 0b10000
# 2 bit
elif palette == Options.Palette.option_1bit:
elif palette == '1bit':
r &= 0b11000
g &= 0b11000
b &= 0b11000
# Invert
elif palette == Options.Palette.option_inverted:
elif palette == 'inverted':
r = 31 - r
g = 31 - g
b = 31 - b
# Pink
elif palette == Options.Palette.option_pink:
elif palette == 'pink':
r = r // 2
r += 16
r = int(r)
@@ -486,7 +499,7 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
b += 16
b = int(b)
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 = (r + g + b) // 3
r = g = b = gray

View File

@@ -20,7 +20,7 @@ class Dungeon1:
if options.owlstatues == "both" or options.owlstatues == "dungeon":
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_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)
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':
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':
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
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
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
@@ -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_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
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_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

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

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

View File

@@ -15,7 +15,7 @@ class Dungeon5:
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
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

View File

@@ -24,7 +24,7 @@ class Dungeon6:
# 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]]))
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
medicine_chest = Location(dungeon=6).add(DungeonChest(0x1D1)).connect(lower_right_side, FEATHER) # ledge chest medicine
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.
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
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
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

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

View File

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

View File

@@ -387,7 +387,7 @@ def patchVarious(rom, settings):
# Boomerang trade guy
# if settings.boomerang not in {'trade', 'gift'} or settings.overworld in {'normal', 'nodungeons'}:
if settings["tradequest"]:
if settings.tradequest:
# 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(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:
def __init__(self, ap_options):
def __init__(self, settings_dict):
self.__all = [
Setting('seed', 'Main', '<', 'Seed', placeholder='Leave empty for random seed', default="", multiworld=False,
description="""For multiple people to generate the same randomization result, enter the generated seed number here.
@@ -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'),
Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False,
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',
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),
@@ -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,
description='Enables the nag messages normally shown when touching stones and crystals',
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',
aesthetic=True),
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.
[Green/Yellow/Red/Blue] forces link into one of these colors.
[?? A/B/C/D] colors of link are usually inverted and color depends on the area you are in."""),
Setting('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='',
description="""
[Random] Randomizes overworld and dungeon music'
[Disable] no music in the whole game""",
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}
# Make sure all short keys are unique
short_keys = set()
for s in self.__all:
assert s.short_key not in short_keys, s.label
short_keys.add(s.short_key)
self.ap_options = ap_options
# don't worry about unique short keys for AP
#short_keys = set()
#for s in self.__all:
# assert s.short_key not in short_keys, s.label
# short_keys.add(s.short_key)
for option in self.ap_options.values():
if not hasattr(option, 'to_ladxr_option'):
continue
name, value = option.to_ladxr_option(self.ap_options)
for name, value in settings_dict.items():
if value == "true":
value = 1
elif value == "false":

View File

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

View File

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

View File

@@ -6,13 +6,11 @@ import json
import pkgutil
import bsdiff4
import binascii
import pickle
from typing import TYPE_CHECKING
from .Common import *
from .LADXR import generator
from .LADXR.main import get_parser
from .LADXR.hints import generate_hint_texts
from .LADXR.locations.keyLocation import KeyLocation
LADX_HASH = "07c211479386825042efb4ad31bb525f"
if TYPE_CHECKING:
@@ -35,7 +33,7 @@ class LADXPatchExtensions(worlds.Files.APPatchExtension):
@staticmethod
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"))
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 rom
@@ -56,7 +54,6 @@ class LADXProcedurePatch(worlds.Files.APProcedurePatch):
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 = {
"generated_world_version": world.world_version.as_simple_string(),
"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_name": patch.player_name,
"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),
"world_setup": {
"goal": world.ladxr_logic.world_setup.goal,
"bingo_goals": world.ladxr_logic.world_setup.bingo_goals,
"multichest": world.ladxr_logic.world_setup.multichest,
"entrance_mapping": world.ladxr_logic.world_setup.entrance_mapping,
"boss_mapping": world.ladxr_logic.world_setup.boss_mapping,
"miniboss_mapping": world.ladxr_logic.world_setup.miniboss_mapping,
},
"options": world.options.as_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",
),
"ladxr_settings_dict": world.ladxr_settings_dict,
}
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):
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()
world_setup = LADXRWorldSetup()
@@ -503,36 +513,36 @@ class LinksAwakeningWorld(World):
return "TRADING_ITEM_LETTER"
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 loc in r.locations:
if isinstance(loc, LinksAwakeningLocation):
assert(loc.item)
spot = {}
# If we're a links awakening item, just use the item
if isinstance(loc.item, LinksAwakeningItem):
loc.ladxr_item.item = loc.item.item_data.ladxr_id
spot["item"] = loc.item.item_data.ladxr_id
# If the item name contains "sword", use a sword icon, etc
# Otherwise, use a cute letter as the icon
elif self.options.foreign_item_icons == 'guess_by_name':
loc.ladxr_item.item = self.guess_icon_for_other_world(loc.item)
loc.ladxr_item.setCustomItemName(loc.item.name)
spot["item"] = self.guess_icon_for_other_world(loc.item)
else:
if loc.item.advancement:
loc.ladxr_item.item = 'PIECE_OF_POWER'
spot["item"] = 'PIECE_OF_POWER'
else:
loc.ladxr_item.item = 'GUARDIAN_ACORN'
loc.ladxr_item.setCustomItemName(loc.item.name)
spot["item"] = 'GUARDIAN_ACORN'
spot["custom_item_name"] = loc.item.name
if loc.item:
loc.ladxr_item.item_owner = loc.item.player
spot["item_owner"] = loc.item.player
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
loc.ladxr_item.location_owner = self.player
spot["name_id"] = loc.ladxr_item.nameId
self.rom_item_placements.append(spot)
patch = LADXProcedurePatch(player=self.player, player_name=self.player_name)