LADX: Converted to new options API (+other small refactors) (#3542)

* Refactored various things

* Renamed hidden variable in dungeon item shuffle block

* Fixed LADXRSettings initialization

* Rename ladxr_options -> ladxr_settings

* Remove unnecessary int cast

* Update worlds/ladx/LADXR/generator.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
This commit is contained in:
Star Rauchenberger 2024-06-17 22:48:15 -04:00 committed by GitHub
parent 898509e7ee
commit af213c9e5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 191 additions and 162 deletions

View File

@ -4,6 +4,7 @@ import importlib.machinery
import os
import pkgutil
from collections import defaultdict
from typing import TYPE_CHECKING
from .romTables import ROMWithTables
from . import assembler
@ -67,10 +68,14 @@ from BaseClasses import ItemClassification
from ..Locations import LinksAwakeningLocation
from ..Options import TrendyGame, Palette, MusicChangeCondition, BootsControls
if TYPE_CHECKING:
from .. import LinksAwakeningWorld
# Function to generate a final rom, this patches the rom with all required patches
def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, multiworld=None, player_name=None, player_names=[], player_id = 0):
def generateRom(args, world: "LinksAwakeningWorld"):
rom_patches = []
player_names = list(world.multiworld.player_name.values())
rom = ROMWithTables(args.input_filename, rom_patches)
rom.player_names = player_names
@ -84,10 +89,10 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
for pymod in pymods:
pymod.prePatch(rom)
if settings.gfxmod:
patches.aesthetics.gfxMod(rom, os.path.join("data", "sprites", "ladx", settings.gfxmod))
if world.ladxr_settings.gfxmod:
patches.aesthetics.gfxMod(rom, os.path.join("data", "sprites", "ladx", world.ladxr_settings.gfxmod))
item_list = [item for item in logic.iteminfo_list if not isinstance(item, KeyLocation)]
item_list = [item for item in world.ladxr_logic.iteminfo_list if not isinstance(item, KeyLocation)]
assembler.resetConsts()
assembler.const("INV_SIZE", 16)
@ -116,7 +121,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
assembler.const("wLinkSpawnDelay", 0xDE13)
#assembler.const("HARDWARE_LINK", 1)
assembler.const("HARD_MODE", 1 if settings.hardmode != "none" else 0)
assembler.const("HARD_MODE", 1 if world.ladxr_settings.hardmode != "none" else 0)
patches.core.cleanup(rom)
patches.save.singleSaveSlot(rom)
@ -130,7 +135,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
patches.core.easyColorDungeonAccess(rom)
patches.owl.removeOwlEvents(rom)
patches.enemies.fixArmosKnightAsMiniboss(rom)
patches.bank3e.addBank3E(rom, auth, player_id, player_names)
patches.bank3e.addBank3E(rom, world.multi_key, world.player, player_names)
patches.bank3f.addBank3F(rom)
patches.bank34.addBank34(rom, item_list)
patches.core.removeGhost(rom)
@ -141,10 +146,11 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
from ..Options import ShuffleSmallKeys, ShuffleNightmareKeys
if ap_settings["shuffle_small_keys"] != ShuffleSmallKeys.option_original_dungeon or ap_settings["shuffle_nightmare_keys"] != ShuffleNightmareKeys.option_original_dungeon:
if world.options.shuffle_small_keys != ShuffleSmallKeys.option_original_dungeon or\
world.options.shuffle_nightmare_keys != ShuffleNightmareKeys.option_original_dungeon:
patches.inventory.advancedInventorySubscreen(rom)
patches.inventory.moreSlots(rom)
if settings.witch:
if world.ladxr_settings.witch:
patches.witch.updateWitch(rom)
patches.softlock.fixAll(rom)
patches.maptweaks.tweakMap(rom)
@ -158,9 +164,9 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
patches.tarin.updateTarin(rom)
patches.fishingMinigame.updateFinishingMinigame(rom)
patches.health.upgradeHealthContainers(rom)
if settings.owlstatues in ("dungeon", "both"):
if world.ladxr_settings.owlstatues in ("dungeon", "both"):
patches.owl.upgradeDungeonOwlStatues(rom)
if settings.owlstatues in ("overworld", "both"):
if world.ladxr_settings.owlstatues in ("overworld", "both"):
patches.owl.upgradeOverworldOwlStatues(rom)
patches.goldenLeaf.fixGoldenLeaf(rom)
patches.heartPiece.fixHeartPiece(rom)
@ -170,106 +176,110 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
patches.songs.upgradeMarin(rom)
patches.songs.upgradeManbo(rom)
patches.songs.upgradeMamu(rom)
if settings.tradequest:
patches.tradeSequence.patchTradeSequence(rom, settings.boomerang)
if world.ladxr_settings.tradequest:
patches.tradeSequence.patchTradeSequence(rom, world.ladxr_settings.boomerang)
else:
# Monkey bridge patch, always have the bridge there.
rom.patch(0x00, 0x333D, assembler.ASM("bit 4, e\njr Z, $05"), b"", fill_nop=True)
patches.bowwow.fixBowwow(rom, everywhere=settings.bowwow != 'normal')
if settings.bowwow != 'normal':
patches.bowwow.fixBowwow(rom, everywhere=world.ladxr_settings.bowwow != 'normal')
if world.ladxr_settings.bowwow != 'normal':
patches.bowwow.bowwowMapPatches(rom)
patches.desert.desertAccess(rom)
if settings.overworld == 'dungeondive':
if world.ladxr_settings.overworld == 'dungeondive':
patches.overworld.patchOverworldTilesets(rom)
patches.overworld.createDungeonOnlyOverworld(rom)
elif settings.overworld == 'nodungeons':
elif world.ladxr_settings.overworld == 'nodungeons':
patches.dungeon.patchNoDungeons(rom)
elif settings.overworld == 'random':
elif world.ladxr_settings.overworld == 'random':
patches.overworld.patchOverworldTilesets(rom)
mapgen.store_map(rom, logic.world.map)
mapgen.store_map(rom, world.ladxr_logic.world.map)
#if settings.dungeon_items == 'keysy':
# patches.dungeon.removeKeyDoors(rom)
# patches.reduceRNG.slowdownThreeOfAKind(rom)
patches.reduceRNG.fixHorseHeads(rom)
patches.bomb.onlyDropBombsWhenHaveBombs(rom)
if ap_settings['music_change_condition'] == MusicChangeCondition.option_always:
if world.options.music_change_condition == MusicChangeCondition.option_always:
patches.aesthetics.noSwordMusic(rom)
patches.aesthetics.reduceMessageLengths(rom, rnd)
patches.aesthetics.reduceMessageLengths(rom, world.random)
patches.aesthetics.allowColorDungeonSpritesEverywhere(rom)
if settings.music == 'random':
patches.music.randomizeMusic(rom, rnd)
elif settings.music == 'off':
if world.ladxr_settings.music == 'random':
patches.music.randomizeMusic(rom, world.random)
elif world.ladxr_settings.music == 'off':
patches.music.noMusic(rom)
if settings.noflash:
if world.ladxr_settings.noflash:
patches.aesthetics.removeFlashingLights(rom)
if settings.hardmode == "oracle":
if world.ladxr_settings.hardmode == "oracle":
patches.hardMode.oracleMode(rom)
elif settings.hardmode == "hero":
elif world.ladxr_settings.hardmode == "hero":
patches.hardMode.heroMode(rom)
elif settings.hardmode == "ohko":
elif world.ladxr_settings.hardmode == "ohko":
patches.hardMode.oneHitKO(rom)
if settings.superweapons:
if world.ladxr_settings.superweapons:
patches.weapons.patchSuperWeapons(rom)
if settings.textmode == 'fast':
if world.ladxr_settings.textmode == 'fast':
patches.aesthetics.fastText(rom)
if settings.textmode == 'none':
if world.ladxr_settings.textmode == 'none':
patches.aesthetics.fastText(rom)
patches.aesthetics.noText(rom)
if not settings.nagmessages:
if not world.ladxr_settings.nagmessages:
patches.aesthetics.removeNagMessages(rom)
if settings.lowhpbeep == 'slow':
if world.ladxr_settings.lowhpbeep == 'slow':
patches.aesthetics.slowLowHPBeep(rom)
if settings.lowhpbeep == 'none':
if world.ladxr_settings.lowhpbeep == 'none':
patches.aesthetics.removeLowHPBeep(rom)
if 0 <= int(settings.linkspalette):
patches.aesthetics.forceLinksPalette(rom, int(settings.linkspalette))
if 0 <= int(world.ladxr_settings.linkspalette):
patches.aesthetics.forceLinksPalette(rom, int(world.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 settings.steal == 'never':
if world.ladxr_settings.steal == 'never':
rom.patch(4, 0x36F9, "FA4EDB", "3E0000")
elif settings.steal == 'always':
elif world.ladxr_settings.steal == 'always':
rom.patch(4, 0x36F9, "FA4EDB", "3E0100")
if settings.hpmode == 'inverted':
if world.ladxr_settings.hpmode == 'inverted':
patches.health.setStartHealth(rom, 9)
elif settings.hpmode == '1':
elif world.ladxr_settings.hpmode == '1':
patches.health.setStartHealth(rom, 1)
patches.inventory.songSelectAfterOcarinaSelect(rom)
if settings.quickswap == 'a':
if world.ladxr_settings.quickswap == 'a':
patches.core.quickswap(rom, 1)
elif settings.quickswap == 'b':
elif world.ladxr_settings.quickswap == 'b':
patches.core.quickswap(rom, 0)
patches.core.addBootsControls(rom, ap_settings['boots_controls'])
patches.core.addBootsControls(rom, world.options.boots_controls)
world_setup = logic.world_setup
world_setup = world.ladxr_logic.world_setup
JUNK_HINT = 0.33
RANDOM_HINT= 0.66
# USEFUL_HINT = 1.0
# TODO: filter events, filter unshuffled keys
all_items = multiworld.get_items()
our_items = [item for item in all_items if item.player == player_id and item.location and item.code is not None and item.location.show_in_spoiler]
all_items = world.multiworld.get_items()
our_items = [item for item in all_items
if item.player == world.player
and item.location
and item.code is not None
and item.location.show_in_spoiler]
our_useful_items = [item for item in our_items if ItemClassification.progression in item.classification]
def gen_hint():
chance = rnd.uniform(0, 1)
chance = world.random.uniform(0, 1)
if chance < JUNK_HINT:
return None
elif chance < RANDOM_HINT:
location = rnd.choice(our_items).location
location = world.random.choice(our_items).location
else: # USEFUL_HINT
location = rnd.choice(our_useful_items).location
location = world.random.choice(our_useful_items).location
if location.item.player == player_id:
if location.item.player == world.player:
name = "Your"
else:
name = f"{multiworld.player_name[location.item.player]}'s"
name = f"{world.multiworld.player_name[location.item.player]}'s"
if isinstance(location, LinksAwakeningLocation):
location_name = location.ladxr_item.metadata.name
@ -277,8 +287,8 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
location_name = location.name
hint = f"{name} {location.item} is at {location_name}"
if location.player != player_id:
hint += f" in {multiworld.player_name[location.player]}'s world"
if location.player != world.player:
hint += f" in {world.multiworld.player_name[location.player]}'s world"
# Cap hint size at 85
# Realistically we could go bigger but let's be safe instead
@ -286,7 +296,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
return hint
hints.addHints(rom, rnd, gen_hint)
hints.addHints(rom, world.random, gen_hint)
if world_setup.goal == "raft":
patches.goal.setRaftGoal(rom)
@ -299,7 +309,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
# Patch the generated logic into the rom
patches.chest.setMultiChest(rom, world_setup.multichest)
if settings.overworld not in {"dungeondive", "random"}:
if world.ladxr_settings.overworld not in {"dungeondive", "random"}:
patches.entrances.changeEntrances(rom, world_setup.entrance_mapping)
for spot in item_list:
if spot.item and spot.item.startswith("*"):
@ -318,15 +328,16 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
patches.core.addFrameCounter(rom, len(item_list))
patches.core.warpHome(rom) # Needs to be done after setting the start location.
patches.titleScreen.setRomInfo(rom, auth, seed_name, settings, player_name, player_id)
if ap_settings["ap_title_screen"]:
patches.titleScreen.setRomInfo(rom, world.multi_key, world.multiworld.seed_name, world.ladxr_settings,
world.player_name, world.player)
if world.options.ap_title_screen:
patches.titleScreen.setTitleGraphics(rom)
patches.endscreen.updateEndScreen(rom)
patches.aesthetics.updateSpriteData(rom)
if args.doubletrouble:
patches.enemies.doubleTrouble(rom)
if ap_settings["text_shuffle"]:
if world.options.text_shuffle:
buckets = defaultdict(list)
# For each ROM bank, shuffle text within the bank
for n, data in enumerate(rom.texts._PointerTable__data):
@ -336,20 +347,20 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
for bucket in buckets.values():
# For each bucket, make a copy and shuffle
shuffled = bucket.copy()
rnd.shuffle(shuffled)
world.random.shuffle(shuffled)
# Then put new text in
for bucket_idx, (orig_idx, data) in enumerate(bucket):
rom.texts[shuffled[bucket_idx][0]] = data
if ap_settings["trendy_game"] != TrendyGame.option_normal:
if world.options.trendy_game != TrendyGame.option_normal:
# TODO: if 0 or 4, 5, remove inaccurate conveyor tiles
room_editor = RoomEditor(rom, 0x2A0)
if ap_settings["trendy_game"] == TrendyGame.option_easy:
if world.options.trendy_game == TrendyGame.option_easy:
# Set physics flag on all objects
for i in range(0, 6):
rom.banks[0x4][0x6F1E + i -0x4000] = 0x4
@ -360,7 +371,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
# Add new conveyor to "push" yoshi (it's only a visual)
room_editor.objects.append(Object(5, 3, 0xD0))
if int(ap_settings["trendy_game"]) >= TrendyGame.option_harder:
if world.options.trendy_game >= TrendyGame.option_harder:
"""
Data_004_76A0::
db $FC, $00, $04, $00, $00
@ -374,12 +385,12 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
TrendyGame.option_impossible: (3, 16),
}
def speed():
return rnd.randint(*speeds[ap_settings["trendy_game"]])
return world.random.randint(*speeds[world.options.trendy_game])
rom.banks[0x4][0x76A0-0x4000] = 0xFF - speed()
rom.banks[0x4][0x76A2-0x4000] = speed()
rom.banks[0x4][0x76A6-0x4000] = speed()
rom.banks[0x4][0x76A8-0x4000] = 0xFF - speed()
if int(ap_settings["trendy_game"]) >= TrendyGame.option_hardest:
if world.options.trendy_game >= TrendyGame.option_hardest:
rom.banks[0x4][0x76A1-0x4000] = 0xFF - speed()
rom.banks[0x4][0x76A3-0x4000] = speed()
rom.banks[0x4][0x76A5-0x4000] = speed()
@ -403,10 +414,10 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
for channel in range(3):
color[channel] = color[channel] * 31 // 0xbc
if ap_settings["warp_improvements"]:
patches.core.addWarpImprovements(rom, ap_settings["additional_warp_points"])
if world.options.warp_improvements:
patches.core.addWarpImprovements(rom, world.options.additional_warp_points)
palette = ap_settings["palette"]
palette = world.options.palette
if palette != Palette.option_normal:
ranges = {
# Object palettes
@ -472,8 +483,8 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
SEED_LOCATION = 0x0134
# Patch over the title
assert(len(auth) == 12)
rom.patch(0x00, SEED_LOCATION, None, binascii.hexlify(auth))
assert(len(world.multi_key) == 12)
rom.patch(0x00, SEED_LOCATION, None, binascii.hexlify(world.multi_key))
for pymod in pymods:
pymod.postPatch(rom)

View File

@ -1,7 +1,9 @@
from dataclasses import dataclass
import os.path
import typing
import logging
from Options import Choice, Option, Toggle, DefaultOnToggle, Range, FreeText
from Options import Choice, Toggle, DefaultOnToggle, Range, FreeText, PerGameCommonOptions
from collections import defaultdict
import Utils
@ -14,7 +16,7 @@ class LADXROption:
def to_ladxr_option(self, all_options):
if not self.ladxr_name:
return None, None
return (self.ladxr_name, self.name_lookup[self.value].replace("_", ""))
@ -32,9 +34,10 @@ class Logic(Choice, LADXROption):
option_hard = 2
option_glitched = 3
option_hell = 4
default = option_normal
class TradeQuest(DefaultOffToggle, LADXROption):
"""
[On] adds the trade items to the pool (the trade locations will always be local items)
@ -43,12 +46,14 @@ class TradeQuest(DefaultOffToggle, LADXROption):
display_name = "Trade Quest"
ladxr_name = "tradequest"
class TextShuffle(DefaultOffToggle):
"""
[On] Shuffles all the text in the game
[Off] (default) doesn't shuffle them.
"""
class Rooster(DefaultOnToggle, LADXROption):
"""
[On] Adds the rooster to the item pool.
@ -57,6 +62,7 @@ class Rooster(DefaultOnToggle, LADXROption):
display_name = "Rooster"
ladxr_name = "rooster"
class Boomerang(Choice):
"""
[Normal] requires Magnifying Lens to get the boomerang.
@ -67,6 +73,7 @@ class Boomerang(Choice):
gift = 1
default = gift
class EntranceShuffle(Choice, LADXROption):
"""
[WARNING] Experimental, may fail to fill
@ -75,19 +82,20 @@ class EntranceShuffle(Choice, LADXROption):
If random start location and/or dungeon shuffle is enabled, then these will be shuffled with all the non-connector entrance pool.
Note, some entrances can lead into water, use the warp-to-home from the save&quit menu to escape this."""
#[Advanced] Simple, but two-way connector caves are shuffled in their own pool as well.
#[Expert] Advanced, but caves/houses without items are also shuffled into the Simple entrance pool.
#[Insanity] Expert, but the Raft Minigame hut and Mamu's cave are added to the non-connector pool.
# [Advanced] Simple, but two-way connector caves are shuffled in their own pool as well.
# [Expert] Advanced, but caves/houses without items are also shuffled into the Simple entrance pool.
# [Insanity] Expert, but the Raft Minigame hut and Mamu's cave are added to the non-connector pool.
option_none = 0
option_simple = 1
#option_advanced = 2
#option_expert = 3
#option_insanity = 4
# option_advanced = 2
# option_expert = 3
# option_insanity = 4
default = option_none
display_name = "Experimental Entrance Shuffle"
ladxr_name = "entranceshuffle"
class DungeonShuffle(DefaultOffToggle, LADXROption):
"""
[WARNING] Experimental, may fail to fill
@ -96,12 +104,14 @@ class DungeonShuffle(DefaultOffToggle, LADXROption):
display_name = "Experimental Dungeon Shuffle"
ladxr_name = "dungeonshuffle"
class APTitleScreen(DefaultOnToggle):
"""
Enables AP specific title screen and disables the intro cutscene
"""
display_name = "AP Title Screen"
class BossShuffle(Choice):
none = 0
shuffle = 1
@ -115,10 +125,12 @@ class DungeonItemShuffle(Choice):
option_own_world = 2
option_any_world = 3
option_different_world = 4
#option_delete = 5
#option_start_with = 6
# option_delete = 5
# option_start_with = 6
alias_true = 3
alias_false = 0
ladxr_item: str
class ShuffleNightmareKeys(DungeonItemShuffle):
"""
@ -132,6 +144,7 @@ class ShuffleNightmareKeys(DungeonItemShuffle):
display_name = "Shuffle Nightmare Keys"
ladxr_item = "NIGHTMARE_KEY"
class ShuffleSmallKeys(DungeonItemShuffle):
"""
Shuffle Small Keys
@ -143,6 +156,8 @@ class ShuffleSmallKeys(DungeonItemShuffle):
"""
display_name = "Shuffle Small Keys"
ladxr_item = "KEY"
class ShuffleMaps(DungeonItemShuffle):
"""
Shuffle Dungeon Maps
@ -155,6 +170,7 @@ class ShuffleMaps(DungeonItemShuffle):
display_name = "Shuffle Maps"
ladxr_item = "MAP"
class ShuffleCompasses(DungeonItemShuffle):
"""
Shuffle Dungeon Compasses
@ -167,6 +183,7 @@ class ShuffleCompasses(DungeonItemShuffle):
display_name = "Shuffle Compasses"
ladxr_item = "COMPASS"
class ShuffleStoneBeaks(DungeonItemShuffle):
"""
Shuffle Owl Beaks
@ -179,6 +196,7 @@ class ShuffleStoneBeaks(DungeonItemShuffle):
display_name = "Shuffle Stone Beaks"
ladxr_item = "STONE_BEAK"
class ShuffleInstruments(DungeonItemShuffle):
"""
Shuffle Instruments
@ -195,6 +213,7 @@ class ShuffleInstruments(DungeonItemShuffle):
option_vanilla = 100
alias_false = 100
class Goal(Choice, LADXROption):
"""
The Goal of the game
@ -207,7 +226,7 @@ class Goal(Choice, LADXROption):
option_instruments = 1
option_seashells = 2
option_open = 3
default = option_instruments
def to_ladxr_option(self, all_options):
@ -216,6 +235,7 @@ class Goal(Choice, LADXROption):
else:
return LADXROption.to_ladxr_option(self, all_options)
class InstrumentCount(Range, LADXROption):
"""
Sets the number of instruments required to open the Egg
@ -226,6 +246,7 @@ class InstrumentCount(Range, LADXROption):
range_end = 8
default = 8
class NagMessages(DefaultOffToggle, LADXROption):
"""
Controls if nag messages are shown when rocks and crystals are touched. Useful for glitches, annoying for everyone else.
@ -233,6 +254,7 @@ class NagMessages(DefaultOffToggle, LADXROption):
display_name = "Nag Messages"
ladxr_name = "nagmessages"
class MusicChangeCondition(Choice):
"""
Controls how the music changes.
@ -243,6 +265,8 @@ class MusicChangeCondition(Choice):
option_sword = 0
option_always = 1
default = option_always
# Setting('hpmode', 'Gameplay', 'm', 'Health mode', options=[('default', '', 'Normal'), ('inverted', 'i', 'Inverted'), ('1', '1', 'Start with 1 heart'), ('low', 'l', 'Low max')], default='default',
# description="""
# [Normal} health works as you would expect.
@ -271,6 +295,7 @@ class Bowwow(Choice):
swordless = 1
default = normal
class Overworld(Choice, LADXROption):
"""
[Dungeon Dive] Create a different overworld where all the dungeons are directly accessible and almost no chests are located in the overworld.
@ -284,9 +309,10 @@ class Overworld(Choice, LADXROption):
# option_shuffled = 3
default = option_normal
#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.'),
#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.',
# aesthetic=True),
# Setting('textmode', 'User options', 'f', 'Text mode', options=[('fast', '', 'Fast'), ('default', 'd', 'Normal'), ('none', 'n', 'No-text')], default='fast',
@ -329,7 +355,7 @@ class BootsControls(Choice):
option_bracelet = 1
option_press_a = 2
option_press_b = 3
class LinkPalette(Choice, LADXROption):
"""
@ -352,6 +378,7 @@ class LinkPalette(Choice, LADXROption):
def to_ladxr_option(self, all_options):
return self.ladxr_name, str(self.value)
class TrendyGame(Choice):
"""
[Easy] All of the items hold still for you
@ -370,6 +397,7 @@ class TrendyGame(Choice):
option_impossible = 5
default = option_normal
class GfxMod(FreeText, LADXROption):
"""
Sets the sprite for link, among other things
@ -380,7 +408,7 @@ class GfxMod(FreeText, LADXROption):
normal = ''
default = 'Link'
__spriteDir: str = Utils.local_path(os.path.join('data', 'sprites','ladx'))
__spriteDir: str = Utils.local_path(os.path.join('data', 'sprites', 'ladx'))
__spriteFiles: typing.DefaultDict[str, typing.List[str]] = defaultdict(list)
extensions = [".bin", ".bdiff", ".png", ".bmp"]
@ -389,16 +417,15 @@ class GfxMod(FreeText, LADXROption):
name, extension = os.path.splitext(file)
if extension in extensions:
__spriteFiles[name].append(file)
def __init__(self, value: str):
super().__init__(value)
def verify(self, world, player_name: str, plando_options) -> None:
if self.value == "Link" or self.value in GfxMod.__spriteFiles:
return
raise Exception(f"LADX Sprite '{self.value}' not found. Possible sprites are: {['Link'] + list(GfxMod.__spriteFiles.keys())}")
raise Exception(
f"LADX Sprite '{self.value}' not found. Possible sprites are: {['Link'] + list(GfxMod.__spriteFiles.keys())}")
def to_ladxr_option(self, all_options):
if self.value == -1 or self.value == "Link":
@ -407,10 +434,12 @@ class GfxMod(FreeText, LADXROption):
assert self.value in GfxMod.__spriteFiles
if len(GfxMod.__spriteFiles[self.value]) > 1:
logger.warning(f"{self.value} does not uniquely identify a file. Possible matches: {GfxMod.__spriteFiles[self.value]}. Using {GfxMod.__spriteFiles[self.value][0]}")
logger.warning(
f"{self.value} does not uniquely identify a file. Possible matches: {GfxMod.__spriteFiles[self.value]}. Using {GfxMod.__spriteFiles[self.value][0]}")
return self.ladxr_name, self.__spriteDir + "/" + GfxMod.__spriteFiles[self.value][0]
class Palette(Choice):
"""
Sets the palette for the game.
@ -430,6 +459,7 @@ class Palette(Choice):
option_pink = 4
option_inverted = 5
class Music(Choice, LADXROption):
"""
[Vanilla] Regular Music
@ -441,7 +471,6 @@ class Music(Choice, LADXROption):
option_shuffled = 1
option_off = 2
def to_ladxr_option(self, all_options):
s = ""
if self.value == self.option_shuffled:
@ -450,55 +479,57 @@ class Music(Choice, LADXROption):
s = "off"
return self.ladxr_name, s
class WarpImprovements(DefaultOffToggle):
"""
[On] 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.
[Off] No change
"""
class AdditionalWarpPoints(DefaultOffToggle):
"""
[On] (requires warp improvements) Adds a warp point at Crazy Tracy's house (the Mambo teleport spot) and Eagle's Tower
[Off] No change
"""
links_awakening_options: typing.Dict[str, typing.Type[Option]] = {
'logic': Logic,
@dataclass
class LinksAwakeningOptions(PerGameCommonOptions):
logic: Logic
# 'heartpiece': DefaultOnToggle, # description='Includes heart pieces in the item pool'),
# 'seashells': DefaultOnToggle, # description='Randomizes the secret sea shells hiding in the ground/trees. (chest are always randomized)'),
# 'heartcontainers': DefaultOnToggle, # description='Includes boss heart container drops in the item pool'),
# 'instruments': DefaultOffToggle, # description='Instruments are placed on random locations, dungeon goal will just contain a random item.'),
'tradequest': TradeQuest, # description='Trade quest items are randomized, each NPC takes its normal trade quest item, but gives a random item'),
tradequest: TradeQuest # description='Trade quest items are randomized, each NPC takes its normal trade quest item, but gives a random item'),
# 'witch': DefaultOnToggle, # description='Adds both the toadstool and the reward for giving the toadstool to the witch to the item pool'),
'rooster': Rooster, # description='Adds the rooster to the item pool. Without this option, the rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means.'),
rooster: Rooster # description='Adds the rooster to the item pool. Without this option, the rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means.'),
# 'boomerang': Boomerang,
# 'randomstartlocation': DefaultOffToggle, # 'Randomize where your starting house is located'),
'experimental_dungeon_shuffle': DungeonShuffle, # 'Randomizes the dungeon that each dungeon entrance leads to'),
'experimental_entrance_shuffle': EntranceShuffle,
experimental_dungeon_shuffle: DungeonShuffle # 'Randomizes the dungeon that each dungeon entrance leads to'),
experimental_entrance_shuffle: EntranceShuffle
# 'bossshuffle': BossShuffle,
# 'minibossshuffle': BossShuffle,
'goal': Goal,
'instrument_count': InstrumentCount,
goal: Goal
instrument_count: InstrumentCount
# 'itempool': ItemPool,
# 'bowwow': Bowwow,
# 'overworld': Overworld,
'link_palette': LinkPalette,
'warp_improvements': WarpImprovements,
'additional_warp_points': AdditionalWarpPoints,
'trendy_game': TrendyGame,
'gfxmod': GfxMod,
'palette': Palette,
'text_shuffle': TextShuffle,
'shuffle_nightmare_keys': ShuffleNightmareKeys,
'shuffle_small_keys': ShuffleSmallKeys,
'shuffle_maps': ShuffleMaps,
'shuffle_compasses': ShuffleCompasses,
'shuffle_stone_beaks': ShuffleStoneBeaks,
'music': Music,
'shuffle_instruments': ShuffleInstruments,
'music_change_condition': MusicChangeCondition,
'nag_messages': NagMessages,
'ap_title_screen': APTitleScreen,
'boots_controls': BootsControls,
}
link_palette: LinkPalette
warp_improvements: WarpImprovements
additional_warp_points: AdditionalWarpPoints
trendy_game: TrendyGame
gfxmod: GfxMod
palette: Palette
text_shuffle: TextShuffle
shuffle_nightmare_keys: ShuffleNightmareKeys
shuffle_small_keys: ShuffleSmallKeys
shuffle_maps: ShuffleMaps
shuffle_compasses: ShuffleCompasses
shuffle_stone_beaks: ShuffleStoneBeaks
music: Music
shuffle_instruments: ShuffleInstruments
music_change_condition: MusicChangeCondition
nag_messages: NagMessages
ap_title_screen: APTitleScreen
boots_controls: BootsControls

View File

@ -1,4 +1,4 @@
import settings
import worlds.Files
import hashlib
import Utils
@ -32,7 +32,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
def get_base_rom_path(file_name: str = "") -> str:
options = Utils.get_options()
options = settings.get_settings()
if not file_name:
file_name = options["ladx_options"]["rom_file"]
if not os.path.exists(file_name):

View File

@ -1,4 +1,5 @@
import binascii
import dataclasses
import os
import pkgutil
import tempfile
@ -17,13 +18,13 @@ from .LADXR import generator
from .LADXR.itempool import ItemPool as LADXRItemPool
from .LADXR.locations.constants import CHEST_ITEMS
from .LADXR.locations.instrument import Instrument
from .LADXR.logic import Logic as LAXDRLogic
from .LADXR.logic import Logic as LADXRLogic
from .LADXR.main import get_parser
from .LADXR.settings import Settings as LADXRSettings
from .LADXR.worldSetup import WorldSetup as LADXRWorldSetup
from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion,
create_regions_from_ladxr, get_locations_to_id)
from .Options import DungeonItemShuffle, links_awakening_options, ShuffleInstruments
from .Options import DungeonItemShuffle, ShuffleInstruments, LinksAwakeningOptions
from .Rom import LADXDeltaPatch, get_base_rom_path
DEVELOPER_MODE = False
@ -73,8 +74,9 @@ class LinksAwakeningWorld(World):
"""
game = LINKS_AWAKENING # name of the game/world
web = LinksAwakeningWebWorld()
option_definitions = links_awakening_options # options the player can set
options_dataclass = LinksAwakeningOptions
options: LinksAwakeningOptions
settings: typing.ClassVar[LinksAwakeningSettings]
topology_present = True # show path to required location checks in spoiler
@ -102,7 +104,11 @@ class LinksAwakeningWorld(World):
prefill_dungeon_items = None
player_options = None
ladxr_settings: LADXRSettings
ladxr_logic: LADXRLogic
ladxr_itempool: LADXRItemPool
multi_key: bytearray
rupees = {
ItemName.RUPEES_20: 20,
@ -113,17 +119,13 @@ class LinksAwakeningWorld(World):
}
def convert_ap_options_to_ladxr_logic(self):
self.player_options = {
option: getattr(self.multiworld, option)[self.player] for option in self.option_definitions
}
self.ladxr_settings = LADXRSettings(dataclasses.asdict(self.options))
self.laxdr_options = LADXRSettings(self.player_options)
self.laxdr_options.validate()
self.ladxr_settings.validate()
world_setup = LADXRWorldSetup()
world_setup.randomize(self.laxdr_options, self.multiworld.random)
self.ladxr_logic = LAXDRLogic(configuration_options=self.laxdr_options, world_setup=world_setup)
self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.laxdr_options, self.multiworld.random).toDict()
world_setup.randomize(self.ladxr_settings, self.random)
self.ladxr_logic = LADXRLogic(configuration_options=self.ladxr_settings, world_setup=world_setup)
self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.ladxr_settings, self.random).toDict()
def create_regions(self) -> None:
# Initialize
@ -180,8 +182,8 @@ class LinksAwakeningWorld(World):
# For any and different world, set item rule instead
for dungeon_item_type in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks", "instruments"]:
option = "shuffle_" + dungeon_item_type
option = self.player_options[option]
option_name = "shuffle_" + dungeon_item_type
option: DungeonItemShuffle = getattr(self.options, option_name)
dungeon_item_types[option.ladxr_item] = option.value
@ -189,11 +191,11 @@ class LinksAwakeningWorld(World):
num_items = 8 if dungeon_item_type == "instruments" else 9
if option.value == DungeonItemShuffle.option_own_world:
self.multiworld.local_items[self.player].value |= {
self.options.local_items.value |= {
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
}
elif option.value == DungeonItemShuffle.option_different_world:
self.multiworld.non_local_items[self.player].value |= {
self.options.non_local_items.value |= {
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
}
# option_original_dungeon = 0
@ -215,7 +217,7 @@ class LinksAwakeningWorld(World):
else:
item = self.create_item(item_name)
if not self.multiworld.tradequest[self.player] and isinstance(item.item_data, TradeItemData):
if not self.options.tradequest and isinstance(item.item_data, TradeItemData):
location = self.multiworld.get_location(item.item_data.vanilla_location, self.player)
location.place_locked_item(item)
location.show_in_spoiler = False
@ -287,7 +289,7 @@ class LinksAwakeningWorld(World):
if item.player == self.player
and item.item_data.ladxr_id in start_loc.ladxr_item.OPTIONS and not item.location]
if possible_start_items:
index = self.multiworld.random.choice(possible_start_items)
index = self.random.choice(possible_start_items)
start_item = self.multiworld.itempool.pop(index)
start_loc.place_locked_item(start_item)
@ -336,7 +338,7 @@ class LinksAwakeningWorld(World):
# Get the list of locations and shuffle
all_dungeon_locs_to_fill = sorted(all_dungeon_locs)
self.multiworld.random.shuffle(all_dungeon_locs_to_fill)
self.random.shuffle(all_dungeon_locs_to_fill)
# Get the list of items and sort by priority
def priority(item):
@ -465,34 +467,19 @@ class LinksAwakeningWorld(World):
loc.ladxr_item.location_owner = self.player
rom_name = Rom.get_base_rom_path()
out_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.multiworld.player_name[self.player]}.gbc"
out_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.player_name}.gbc"
out_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.gbc")
parser = get_parser()
args = parser.parse_args([rom_name, "-o", out_name, "--dump"])
name_for_rom = self.multiworld.player_name[self.player]
all_names = [self.multiworld.player_name[i + 1] for i in range(len(self.multiworld.player_name))]
rom = generator.generateRom(
args,
self.laxdr_options,
self.player_options,
self.multi_key,
self.multiworld.seed_name,
self.ladxr_logic,
rnd=self.multiworld.per_slot_randoms[self.player],
player_name=name_for_rom,
player_names=all_names,
player_id = self.player,
multiworld=self.multiworld)
rom = generator.generateRom(args, self)
with open(out_path, "wb") as handle:
rom.save(handle, name="LADXR")
# Write title screen after everything else is done - full gfxmods may stomp over the egg tiles
if self.player_options["ap_title_screen"]:
if self.options.ap_title_screen:
with tempfile.NamedTemporaryFile(delete=False) as title_patch:
title_patch.write(pkgutil.get_data(__name__, "LADXR/patches/title_screen.bdiff4"))
@ -500,16 +487,16 @@ class LinksAwakeningWorld(World):
os.unlink(title_patch.name)
patch = LADXDeltaPatch(os.path.splitext(out_path)[0]+LADXDeltaPatch.patch_file_ending, player=self.player,
player_name=self.multiworld.player_name[self.player], patched_path=out_path)
player_name=self.player_name, patched_path=out_path)
patch.write()
if not DEVELOPER_MODE:
os.unlink(out_path)
def generate_multi_key(self):
return bytearray(self.multiworld.random.getrandbits(8) for _ in range(10)) + self.player.to_bytes(2, 'big')
return bytearray(self.random.getrandbits(8) for _ in range(10)) + self.player.to_bytes(2, 'big')
def modify_multidata(self, multidata: dict):
multidata["connect_names"][binascii.hexlify(self.multi_key).decode()] = multidata["connect_names"][self.multiworld.player_name[self.player]]
multidata["connect_names"][binascii.hexlify(self.multi_key).decode()] = multidata["connect_names"][self.player_name]
def collect(self, state, item: Item) -> bool:
change = super().collect(state, item)