LADX: generate without rom (#4278)

This commit is contained in:
threeandthreee
2025-07-26 16:16:00 -04:00
committed by GitHub
parent fa49fef695
commit 7a8048a8fd
11 changed files with 317 additions and 290 deletions

View File

@@ -2,13 +2,15 @@ import binascii
import importlib.util import importlib.util
import importlib.machinery import importlib.machinery
import os import os
import pkgutil import random
import pickle
import Utils
import settings
from collections import defaultdict from collections import defaultdict
from typing import TYPE_CHECKING from typing import Dict
from .romTables import ROMWithTables from .romTables import ROMWithTables
from . import assembler from . import assembler
from . import mapgen
from . import patches from . import patches
from .patches import overworld as _ from .patches import overworld as _
from .patches import dungeon as _ from .patches import dungeon as _
@@ -57,27 +59,20 @@ from .patches import tradeSequence as _
from . import hints from . import hints
from .patches import bank34 from .patches import bank34
from .utils import formatText
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 .locations.keyLocation import KeyLocation from .. import Options
from BaseClasses import ItemClassification
from ..Locations import LinksAwakeningLocation
from ..Options import TrendyGame, Palette, MusicChangeCondition, Warps
if TYPE_CHECKING:
from .. import LinksAwakeningWorld
# Function to generate a final rom, this patches the rom with all required patches # Function to generate a final rom, this patches the rom with all required patches
def generateRom(args, world: "LinksAwakeningWorld"): 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"]
rom_patches = [] rom_patches = []
player_names = list(world.multiworld.player_name.values()) rom = ROMWithTables(base_rom, rom_patches)
rom.player_names = patch_data["other_player_names"]
rom = ROMWithTables(args.input_filename, rom_patches)
rom.player_names = player_names
pymods = [] pymods = []
if args.pymod: if args.pymod:
for pymod in args.pymod: for pymod in args.pymod:
@@ -88,10 +83,13 @@ def generateRom(args, world: "LinksAwakeningWorld"):
for pymod in pymods: for pymod in pymods:
pymod.prePatch(rom) pymod.prePatch(rom)
if world.ladxr_settings.gfxmod: if options["gfxmod"]:
patches.aesthetics.gfxMod(rom, os.path.join("data", "sprites", "ladx", world.ladxr_settings.gfxmod)) user_settings = settings.get_settings()
try:
item_list = [item for item in world.ladxr_logic.iteminfo_list if not isinstance(item, KeyLocation)] gfx_mod_file = user_settings["ladx_options"]["gfx_mod_file"]
patches.aesthetics.gfxMod(rom, gfx_mod_file)
except FileNotFoundError:
pass # if user just doesnt provide gfxmod file, let patching continue
assembler.resetConsts() assembler.resetConsts()
assembler.const("INV_SIZE", 16) assembler.const("INV_SIZE", 16)
@@ -121,7 +119,7 @@ def generateRom(args, world: "LinksAwakeningWorld"):
assembler.const("wLinkSpawnDelay", 0xDE13) assembler.const("wLinkSpawnDelay", 0xDE13)
#assembler.const("HARDWARE_LINK", 1) #assembler.const("HARDWARE_LINK", 1)
assembler.const("HARD_MODE", 1 if world.ladxr_settings.hardmode != "none" else 0) assembler.const("HARD_MODE", 1 if options["hard_mode"] else 0)
patches.core.cleanup(rom) patches.core.cleanup(rom)
patches.save.singleSaveSlot(rom) patches.save.singleSaveSlot(rom)
@@ -135,7 +133,7 @@ def generateRom(args, world: "LinksAwakeningWorld"):
patches.core.easyColorDungeonAccess(rom) patches.core.easyColorDungeonAccess(rom)
patches.owl.removeOwlEvents(rom) patches.owl.removeOwlEvents(rom)
patches.enemies.fixArmosKnightAsMiniboss(rom) patches.enemies.fixArmosKnightAsMiniboss(rom)
patches.bank3e.addBank3E(rom, world.multi_key, world.player, player_names) patches.bank3e.addBank3E(rom, multi_key, patch_data["player"], patch_data["other_player_names"])
patches.bank3f.addBank3F(rom) patches.bank3f.addBank3F(rom)
patches.bank34.addBank34(rom, item_list) patches.bank34.addBank34(rom, item_list)
patches.core.removeGhost(rom) patches.core.removeGhost(rom)
@@ -144,19 +142,17 @@ def generateRom(args, world: "LinksAwakeningWorld"):
patches.core.alwaysAllowSecretBook(rom) patches.core.alwaysAllowSecretBook(rom)
patches.core.injectMainLoop(rom) patches.core.injectMainLoop(rom)
from ..Options import ShuffleSmallKeys, ShuffleNightmareKeys if options["shuffle_small_keys"] != Options.ShuffleSmallKeys.option_original_dungeon or\
options["shuffle_nightmare_keys"] != Options.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.advancedInventorySubscreen(rom)
patches.inventory.moreSlots(rom) patches.inventory.moreSlots(rom)
if world.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 world.ladxr_settings.rooster: if not options["rooster"]:
patches.maptweaks.tweakMap(rom) patches.maptweaks.tweakMap(rom)
patches.maptweaks.tweakBirdKeyRoom(rom) patches.maptweaks.tweakBirdKeyRoom(rom)
if world.ladxr_settings.overworld == "openmabe": if options["overworld"] == Options.Overworld.option_open_mabe:
patches.maptweaks.openMabe(rom) patches.maptweaks.openMabe(rom)
patches.chest.fixChests(rom) patches.chest.fixChests(rom)
patches.shop.fixShop(rom) patches.shop.fixShop(rom)
@@ -168,10 +164,10 @@ def generateRom(args, world: "LinksAwakeningWorld"):
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 world.ladxr_settings.owlstatues in ("dungeon", "both"): # if ladxr_settings["owlstatues"] in ("dungeon", "both"):
patches.owl.upgradeDungeonOwlStatues(rom) # patches.owl.upgradeDungeonOwlStatues(rom)
if world.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)
patches.seashell.fixSeashell(rom) patches.seashell.fixSeashell(rom)
@@ -180,143 +176,95 @@ def generateRom(args, world: "LinksAwakeningWorld"):
patches.songs.upgradeMarin(rom) patches.songs.upgradeMarin(rom)
patches.songs.upgradeManbo(rom) patches.songs.upgradeManbo(rom)
patches.songs.upgradeMamu(rom) patches.songs.upgradeMamu(rom)
patches.tradeSequence.patchTradeSequence(rom, world.ladxr_settings)
patches.bowwow.fixBowwow(rom, everywhere=world.ladxr_settings.bowwow != 'normal') patches.tradeSequence.patchTradeSequence(rom, options)
if world.ladxr_settings.bowwow != 'normal': patches.bowwow.fixBowwow(rom, everywhere=False)
patches.bowwow.bowwowMapPatches(rom) # if ladxr_settings["bowwow"] != 'normal':
# patches.bowwow.bowwowMapPatches(rom)
patches.desert.desertAccess(rom) patches.desert.desertAccess(rom)
if world.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 world.ladxr_settings.overworld == 'nodungeons': # elif ladxr_settings["overworld"] == 'nodungeons':
patches.dungeon.patchNoDungeons(rom) # patches.dungeon.patchNoDungeons(rom)
elif world.ladxr_settings.overworld == 'random': #elif world.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':
# patches.dungeon.removeKeyDoors(rom) # patches.dungeon.removeKeyDoors(rom)
# 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 world.options.music_change_condition == MusicChangeCondition.option_always: if options["music_change_condition"] == Options.MusicChangeCondition.option_always:
patches.aesthetics.noSwordMusic(rom) patches.aesthetics.noSwordMusic(rom)
patches.aesthetics.reduceMessageLengths(rom, world.random) patches.aesthetics.reduceMessageLengths(rom, random)
patches.aesthetics.allowColorDungeonSpritesEverywhere(rom) patches.aesthetics.allowColorDungeonSpritesEverywhere(rom)
if world.ladxr_settings.music == 'random': if options["music"] == Options.Music.option_shuffled:
patches.music.randomizeMusic(rom, world.random) patches.music.randomizeMusic(rom, random)
elif world.ladxr_settings.music == 'off': elif options["music"] == Options.Music.option_off:
patches.music.noMusic(rom) patches.music.noMusic(rom)
if world.ladxr_settings.noflash: if options["no_flash"]:
patches.aesthetics.removeFlashingLights(rom) patches.aesthetics.removeFlashingLights(rom)
if world.ladxr_settings.hardmode == "oracle": if options["hard_mode"] == Options.HardMode.option_oracle:
patches.hardMode.oracleMode(rom) patches.hardMode.oracleMode(rom)
elif world.ladxr_settings.hardmode == "hero": elif options["hard_mode"] == Options.HardMode.option_hero:
patches.hardMode.heroMode(rom) patches.hardMode.heroMode(rom)
elif world.ladxr_settings.hardmode == "ohko": elif options["hard_mode"] == Options.HardMode.option_ohko:
patches.hardMode.oneHitKO(rom) patches.hardMode.oneHitKO(rom)
if world.ladxr_settings.superweapons: #if ladxr_settings["superweapons"]:
patches.weapons.patchSuperWeapons(rom) # patches.weapons.patchSuperWeapons(rom)
if world.ladxr_settings.textmode == 'fast': if options["text_mode"] == Options.TextMode.option_fast:
patches.aesthetics.fastText(rom) patches.aesthetics.fastText(rom)
if world.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 world.ladxr_settings.nagmessages: if not options["nag_messages"]:
patches.aesthetics.removeNagMessages(rom) patches.aesthetics.removeNagMessages(rom)
if world.ladxr_settings.lowhpbeep == 'slow': if options["low_hp_beep"] == Options.LowHpBeep.option_slow:
patches.aesthetics.slowLowHPBeep(rom) patches.aesthetics.slowLowHPBeep(rom)
if world.ladxr_settings.lowhpbeep == 'none': if options["low_hp_beep"] == Options.LowHpBeep.option_none:
patches.aesthetics.removeLowHPBeep(rom) patches.aesthetics.removeLowHPBeep(rom)
if 0 <= int(world.ladxr_settings.linkspalette): if 0 <= options["link_palette"]:
patches.aesthetics.forceLinksPalette(rom, int(world.ladxr_settings.linkspalette)) patches.aesthetics.forceLinksPalette(rom, options["link_palette"])
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 world.ladxr_settings.steal == 'never': #if ladxr_settings["steal"] == 'never':
rom.patch(4, 0x36F9, "FA4EDB", "3E0000") # rom.patch(4, 0x36F9, "FA4EDB", "3E0000")
elif world.ladxr_settings.steal == 'always': #elif ladxr_settings["steal"] == 'always':
rom.patch(4, 0x36F9, "FA4EDB", "3E0100") # rom.patch(4, 0x36F9, "FA4EDB", "3E0100")
if world.ladxr_settings.hpmode == 'inverted': #if ladxr_settings["hpmode"] == 'inverted':
patches.health.setStartHealth(rom, 9) # patches.health.setStartHealth(rom, 9)
elif world.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 world.ladxr_settings.quickswap == 'a': if options["quickswap"] == 'a':
patches.core.quickswap(rom, 1) patches.core.quickswap(rom, 1)
elif world.ladxr_settings.quickswap == 'b': elif options["quickswap"] == 'b':
patches.core.quickswap(rom, 0) patches.core.quickswap(rom, 0)
patches.core.addBootsControls(rom, world.options.boots_controls) patches.core.addBootsControls(rom, options["boots_controls"])
random.seed(patch_data["seed"] + patch_data["player"])
hints.addHints(rom, random, patch_data["hint_texts"])
world_setup = world.ladxr_logic.world_setup if patch_data["world_setup"]["goal"] == "raft":
JUNK_HINT = 0.33
RANDOM_HINT= 0.66
# USEFUL_HINT = 1.0
# TODO: filter events, filter unshuffled keys
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():
if not world.options.in_game_hints:
return 'Hints are disabled!'
chance = world.random.uniform(0, 1)
if chance < JUNK_HINT:
return None
elif chance < RANDOM_HINT:
location = world.random.choice(our_items).location
else: # USEFUL_HINT
location = world.random.choice(our_useful_items).location
if location.item.player == world.player:
name = "Your"
else:
name = f"{world.multiworld.player_name[location.item.player]}'s"
# filter out { and } since they cause issues with string.format later on
name = name.replace("{", "").replace("}", "")
if isinstance(location, LinksAwakeningLocation):
location_name = location.ladxr_item.metadata.name
else:
location_name = location.name
hint = f"{name} {location.item.name} is at {location_name}"
if location.player != world.player:
# filter out { and } since they cause issues with string.format later on
player_name = world.multiworld.player_name[location.player].replace("{", "").replace("}", "")
hint += f" in {player_name}'s world"
# Cap hint size at 85
# Realistically we could go bigger but let's be safe instead
hint = hint[:85]
return hint
hints.addHints(rom, world.random, gen_hint)
if world_setup.goal == "raft":
patches.goal.setRaftGoal(rom) patches.goal.setRaftGoal(rom)
elif world_setup.goal in ("bingo", "bingo-full"): elif patch_data["world_setup"]["goal"] in ("bingo", "bingo-full"):
patches.bingo.setBingoGoal(rom, world_setup.bingo_goals, world_setup.goal) patches.bingo.setBingoGoal(rom, patch_data["world_setup"]["bingo_goals"], patch_data["world_setup"]["goal"])
elif world_setup.goal == "seashells": elif patch_data["world_setup"]["goal"] == "seashells":
patches.goal.setSeashellGoal(rom, 20) patches.goal.setSeashellGoal(rom, 20)
else: else:
patches.goal.setRequiredInstrumentCount(rom, world_setup.goal) patches.goal.setRequiredInstrumentCount(rom, patch_data["world_setup"]["goal"])
# Patch the generated logic into the rom # Patch the generated logic into the rom
patches.chest.setMultiChest(rom, world_setup.multichest) patches.chest.setMultiChest(rom, patch_data["world_setup"]["multichest"])
if world.ladxr_settings.overworld not in {"dungeondive", "random"}: #if ladxr_settings["overworld"] not in {"dungeondive", "random"}:
patches.entrances.changeEntrances(rom, world_setup.entrance_mapping) patches.entrances.changeEntrances(rom, patch_data["world_setup"]["entrance_mapping"])
for spot in item_list: for spot in item_list:
if spot.item and spot.item.startswith("*"): if spot.item and spot.item.startswith("*"):
spot.item = spot.item[1:] spot.item = spot.item[1:]
@@ -327,23 +275,22 @@ def generateRom(args, world: "LinksAwakeningWorld"):
# There are only 101 player name slots (99 + "The Server" + "another world"), so don't use more than that # There are only 101 player name slots (99 + "The Server" + "another world"), so don't use more than that
mw = 100 mw = 100
spot.patch(rom, spot.item, multiworld=mw) spot.patch(rom, spot.item, multiworld=mw)
patches.enemies.changeBosses(rom, world_setup.boss_mapping) patches.enemies.changeBosses(rom, patch_data["world_setup"]["boss_mapping"])
patches.enemies.changeMiniBosses(rom, world_setup.miniboss_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, world.multi_key, world.multiworld.seed_name, world.ladxr_settings, patches.titleScreen.setRomInfo(rom, patch_data)
world.player_name, world.player) if options["ap_title_screen"]:
if world.options.ap_title_screen:
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 world.options.text_shuffle: if options["text_shuffle"]:
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,
@@ -388,6 +335,7 @@ def generateRom(args, world: "LinksAwakeningWorld"):
excluded_texts = [ rom.texts[excluded_id] for excluded_id in excluded_ids] excluded_texts = [ rom.texts[excluded_id] for excluded_id in excluded_ids]
buckets = defaultdict(list) buckets = defaultdict(list)
# For each ROM bank, shuffle text within the bank # For each ROM bank, shuffle text within the bank
random.seed(patch_data["seed"] + patch_data["player"])
for n, data in enumerate(rom.texts._PointerTable__data): for n, data in enumerate(rom.texts._PointerTable__data):
# Don't muck up which text boxes are questions and which are statements # Don't muck up which text boxes are questions and which are statements
if type(data) != int and data and data != b'\xFF' and data not in excluded_texts: if type(data) != int and data and data != b'\xFF' and data not in excluded_texts:
@@ -395,20 +343,20 @@ def generateRom(args, world: "LinksAwakeningWorld"):
for bucket in buckets.values(): for bucket in buckets.values():
# For each bucket, make a copy and shuffle # For each bucket, make a copy and shuffle
shuffled = bucket.copy() shuffled = bucket.copy()
world.random.shuffle(shuffled) random.shuffle(shuffled)
# Then put new text in # Then put new text in
for bucket_idx, (orig_idx, data) in enumerate(bucket): for bucket_idx, (orig_idx, data) in enumerate(bucket):
rom.texts[shuffled[bucket_idx][0]] = data rom.texts[shuffled[bucket_idx][0]] = data
if world.options.trendy_game != TrendyGame.option_normal: if options["trendy_game"] != Options.TrendyGame.option_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 world.options.trendy_game == TrendyGame.option_easy: if options["trendy_game"] == Options.TrendyGame.option_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
@@ -419,7 +367,7 @@ def generateRom(args, world: "LinksAwakeningWorld"):
# 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 world.options.trendy_game >= TrendyGame.option_harder: if options["trendy_game"] >= Options.TrendyGame.option_harder:
""" """
Data_004_76A0:: Data_004_76A0::
db $FC, $00, $04, $00, $00 db $FC, $00, $04, $00, $00
@@ -428,17 +376,18 @@ def generateRom(args, world: "LinksAwakeningWorld"):
db $00, $04, $00, $FC, $00 db $00, $04, $00, $FC, $00
""" """
speeds = { speeds = {
TrendyGame.option_harder: (3, 8), Options.TrendyGame.option_harder: (3, 8),
TrendyGame.option_hardest: (3, 8), Options.TrendyGame.option_hardest: (3, 8),
TrendyGame.option_impossible: (3, 16), Options.TrendyGame.option_impossible: (3, 16),
} }
def speed(): def speed():
return world.random.randint(*speeds[world.options.trendy_game]) random.seed(patch_data["seed"] + patch_data["player"])
return random.randint(*speeds[options["trendy_game"]])
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 world.options.trendy_game >= TrendyGame.option_hardest: if options["trendy_game"] >= Options.TrendyGame.option_hardest:
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()
@@ -462,11 +411,11 @@ def generateRom(args, world: "LinksAwakeningWorld"):
for channel in range(3): for channel in range(3):
color[channel] = color[channel] * 31 // 0xbc color[channel] = color[channel] * 31 // 0xbc
if world.options.warps != Warps.option_vanilla: if options["warps"] != Options.Warps.option_vanilla:
patches.core.addWarpImprovements(rom, world.options.warps == Warps.option_improved_additional) patches.core.addWarpImprovements(rom, options["warps"] == Options.Warps.option_improved_additional)
palette = world.options.palette palette = options["palette"]
if palette != Palette.option_normal: if palette != Options.Palette.option_normal:
ranges = { ranges = {
# Object palettes # Object palettes
# Overworld palettes # Overworld palettes
@@ -496,22 +445,22 @@ def generateRom(args, world: "LinksAwakeningWorld"):
r,g,b = bin_to_rgb(packed) r,g,b = bin_to_rgb(packed)
# 1 bit # 1 bit
if palette == Palette.option_1bit: if palette == Options.Palette.option_1bit:
r &= 0b10000 r &= 0b10000
g &= 0b10000 g &= 0b10000
b &= 0b10000 b &= 0b10000
# 2 bit # 2 bit
elif palette == Palette.option_1bit: elif palette == Options.Palette.option_1bit:
r &= 0b11000 r &= 0b11000
g &= 0b11000 g &= 0b11000
b &= 0b11000 b &= 0b11000
# Invert # Invert
elif palette == Palette.option_inverted: elif palette == Options.Palette.option_inverted:
r = 31 - r r = 31 - r
g = 31 - g g = 31 - g
b = 31 - b b = 31 - b
# Pink # Pink
elif palette == Palette.option_pink: elif palette == Options.Palette.option_pink:
r = r // 2 r = r // 2
r += 16 r += 16
r = int(r) r = int(r)
@@ -520,7 +469,7 @@ def generateRom(args, world: "LinksAwakeningWorld"):
b += 16 b += 16
b = int(b) b = int(b)
b = clamp(b, 0, 0x1F) b = clamp(b, 0, 0x1F)
elif palette == Palette.option_greyscale: elif palette == Options.Palette.option_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
@@ -531,10 +480,10 @@ def generateRom(args, world: "LinksAwakeningWorld"):
SEED_LOCATION = 0x0134 SEED_LOCATION = 0x0134
# Patch over the title # Patch over the title
assert(len(world.multi_key) == 12) assert(len(multi_key) == 12)
rom.patch(0x00, SEED_LOCATION, None, binascii.hexlify(world.multi_key)) rom.patch(0x00, SEED_LOCATION, None, binascii.hexlify(multi_key))
for pymod in pymods: for pymod in pymods:
pymod.postPatch(rom) pymod.postPatch(rom)
return rom return rom.save()

View File

@@ -1,5 +1,7 @@
from .locations.items import * from .locations.items import *
from .utils import formatText from .utils import formatText
from BaseClasses import ItemClassification
from ..Locations import LinksAwakeningLocation
hint_text_ids = [ hint_text_ids = [
@@ -49,14 +51,64 @@ useless_hint = [
] ]
def addHints(rom, rnd, hint_generator): def addHints(rom, rnd, hint_texts):
hint_texts_copy = hint_texts.copy()
text_ids = hint_text_ids.copy() text_ids = hint_text_ids.copy()
rnd.shuffle(text_ids) rnd.shuffle(text_ids)
for text_id in text_ids: for text_id in text_ids:
hint = hint_generator() hint = hint_texts_copy.pop()
if not hint: if not hint:
hint = rnd.choice(hints).format(*rnd.choice(useless_hint)) hint = rnd.choice(hints).format(*rnd.choice(useless_hint))
rom.texts[text_id] = formatText(hint) rom.texts[text_id] = formatText(hint)
for text_id in range(0x200, 0x20C, 2): for text_id in range(0x200, 0x20C, 2):
rom.texts[text_id] = formatText("Read this book?", ask="YES NO") rom.texts[text_id] = formatText("Read this book?", ask="YES NO")
def generate_hint_texts(world):
JUNK_HINT = 0.33
RANDOM_HINT= 0.66
# USEFUL_HINT = 1.0
# TODO: filter events, filter unshuffled keys
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]
hint_texts = []
def gen_hint():
chance = world.random.uniform(0, 1)
if chance < JUNK_HINT:
return None
elif chance < RANDOM_HINT:
location = world.random.choice(our_items).location
else: # USEFUL_HINT
location = world.random.choice(our_useful_items).location
if location.item.player == world.player:
name = "Your"
else:
name = f"{world.multiworld.player_name[location.item.player]}'s"
# filter out { and } since they cause issues with string.format later on
name = name.replace("{", "").replace("}", "")
if isinstance(location, LinksAwakeningLocation):
location_name = location.ladxr_item.metadata.name
else:
location_name = location.name
hint = f"{name} {location.item} is at {location_name}"
if location.player != world.player:
# filter out { and } since they cause issues with string.format later on
player_name = world.multiworld.player_name[location.player].replace("{", "").replace("}", "")
hint += f" in {player_name}'s world"
# Cap hint size at 85
# Realistically we could go bigger but let's be safe instead
hint = hint[:85]
return hint
for _ in hint_text_ids:
hint_texts.append(gen_hint())
return hint_texts

View File

@@ -541,7 +541,7 @@ 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: BootsControls): def addBootsControls(rom, boots_controls: int):
if boots_controls == BootsControls.option_vanilla: if boots_controls == BootsControls.option_vanilla:
return return
consts = { consts = {
@@ -578,7 +578,7 @@ def addBootsControls(rom, boots_controls: BootsControls):
jr z, .yesBoots jr z, .yesBoots
ld a, [hl] ld a, [hl]
""" """
}[boots_controls.value] }[boots_controls]
# 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

@@ -42,7 +42,7 @@ MINIBOSS_ENTITIES = {
"ARMOS_KNIGHT": [(4, 3, 0x88)], "ARMOS_KNIGHT": [(4, 3, 0x88)],
} }
MINIBOSS_ROOMS = { MINIBOSS_ROOMS = {
0: 0x111, 1: 0x128, 2: 0x145, 3: 0x164, 4: 0x193, 5: 0x1C5, 6: 0x228, 7: 0x23F, "0": 0x111, "1": 0x128, "2": 0x145, "3": 0x164, "4": 0x193, "5": 0x1C5, "6": 0x228, "7": 0x23F,
"c1": 0x30C, "c2": 0x303, "c1": 0x30C, "c2": 0x303,
"moblin_cave": 0x2E1, "moblin_cave": 0x2E1,
"armos_temple": 0x27F, "armos_temple": 0x27F,

View File

@@ -1,7 +1,6 @@
from ..backgroundEditor import BackgroundEditor from ..backgroundEditor import BackgroundEditor
from .aesthetics import rgb_to_bin, bin_to_rgb, prepatch from .aesthetics import rgb_to_bin, bin_to_rgb, prepatch
import copy import copy
import pkgutil
CHAR_MAP = {'z': 0x3E, '-': 0x3F, '.': 0x39, ':': 0x42, '?': 0x3C, '!': 0x3D} CHAR_MAP = {'z': 0x3E, '-': 0x3F, '.': 0x39, ':': 0x42, '?': 0x3C, '!': 0x3D}
def _encode(s): def _encode(s):
@@ -18,17 +17,18 @@ def _encode(s):
return result return result
def setRomInfo(rom, seed, seed_name, settings, player_name, player_id): def setRomInfo(rom, patch_data):
seed_name = patch_data["seed_name"]
try: try:
seednr = int(seed, 16) seednr = int(patch_data["seed"], 16)
except: except:
import hashlib import hashlib
seednr = int(hashlib.md5(seed).hexdigest(), 16) seednr = int(hashlib.md5(str(patch_data["seed"]).encode()).hexdigest(), 16)
if settings.race: if patch_data["is_race"]:
seed_name = "Race" seed_name = "Race"
if isinstance(settings.race, str): if isinstance(patch_data["is_race"], str):
seed_name += " " + settings.race seed_name += " " + patch_data["is_race"]
rom.patch(0x00, 0x07, "00", "01") rom.patch(0x00, 0x07, "00", "01")
else: else:
rom.patch(0x00, 0x07, "00", "52") rom.patch(0x00, 0x07, "00", "52")
@@ -37,7 +37,7 @@ def setRomInfo(rom, seed, seed_name, settings, player_name, player_id):
#line_2_hex = _encode(seed[16:]) #line_2_hex = _encode(seed[16:])
BASE_DRAWING_AREA = 0x98a0 BASE_DRAWING_AREA = 0x98a0
LINE_WIDTH = 0x20 LINE_WIDTH = 0x20
player_id_text = f"Player {player_id}:" player_id_text = f"Player {patch_data['player']}:"
for n in (3, 4): for n in (3, 4):
be = BackgroundEditor(rom, n) be = BackgroundEditor(rom, n)
ba = BackgroundEditor(rom, n, attributes=True) ba = BackgroundEditor(rom, n, attributes=True)
@@ -45,9 +45,9 @@ def setRomInfo(rom, seed, seed_name, settings, player_name, player_id):
for n, v in enumerate(_encode(player_id_text)): for n, v in enumerate(_encode(player_id_text)):
be.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 5 + 2 + n] = v be.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 5 + 2 + n] = v
ba.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 5 + 2 + n] = 0x00 ba.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 5 + 2 + n] = 0x00
for n, v in enumerate(_encode(player_name)): for n, v in enumerate(_encode(patch_data['player_name'])):
be.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 6 + 0x13 - len(player_name) + n] = v be.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 6 + 0x13 - len(patch_data['player_name']) + n] = v
ba.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 6 + 0x13 - len(player_name) + n] = 0x00 ba.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 6 + 0x13 - len(patch_data['player_name']) + n] = 0x00
for n, v in enumerate(line_1_hex): for n, v in enumerate(line_1_hex):
be.tiles[0x9a20 + n] = v be.tiles[0x9a20 + n] = v
ba.tiles[0x9a20 + n] = 0x00 ba.tiles[0x9a20 + n] = 0x00

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

@@ -7,9 +7,7 @@ h2b = binascii.unhexlify
class ROM: class ROM:
def __init__(self, filename, patches=None): def __init__(self, data, patches=None):
data = open(Utils.user_path(filename), "rb").read()
if patches: if patches:
for patch in patches: for patch in patches:
data = bsdiff4.patch(data, patch) data = bsdiff4.patch(data, patch)
@@ -64,18 +62,10 @@ class ROM:
self.banks[0][0x14E] = checksum >> 8 self.banks[0][0x14E] = checksum >> 8
self.banks[0][0x14F] = checksum & 0xFF self.banks[0][0x14F] = checksum & 0xFF
def save(self, file, *, name=None): def save(self):
# don't pass the name to fixHeader # don't pass the name to fixHeader
self.fixHeader() self.fixHeader()
if isinstance(file, str): return b"".join(self.banks)
f = open(file, "wb")
for bank in self.banks:
f.write(bank)
f.close()
print("Saved:", file)
else:
for bank in self.banks:
file.write(bank)
def readHexSeed(self): def readHexSeed(self):
return self.banks[0x3E][0x2F00:0x2F10].hex().upper() return self.banks[0x3E][0x2F00:0x2F10].hex().upper()

View File

@@ -181,8 +181,8 @@ class IndoorRoomSpriteData(PointerTable):
class ROMWithTables(ROM): class ROMWithTables(ROM):
def __init__(self, filename, patches=None): def __init__(self, data, patches=None):
super().__init__(filename, patches) super().__init__(data, patches)
# Ability to patch any text in the game with different text # Ability to patch any text in the game with different text
self.texts = Texts(self) self.texts = Texts(self)
@@ -203,7 +203,7 @@ class ROMWithTables(ROM):
self.itemNames = {} self.itemNames = {}
def save(self, filename, *, name=None): def save(self):
# Assert special handling of bank 9 expansion is fine # Assert special handling of bank 9 expansion is fine
for i in range(0x3d42, 0x4000): for i in range(0x3d42, 0x4000):
assert self.banks[9][i] == 0, self.banks[9][i] assert self.banks[9][i] == 0, self.banks[9][i]
@@ -221,4 +221,4 @@ class ROMWithTables(ROM):
self.room_sprite_data_indoor.store(self) self.room_sprite_data_indoor.store(self)
self.background_tiles.store(self) self.background_tiles.store(self)
self.background_attributes.store(self) self.background_attributes.store(self)
super().save(filename, name=name) return super().save()

View File

@@ -425,46 +425,11 @@ class TrendyGame(Choice):
default = option_normal default = option_normal
class GfxMod(FreeText, LADXROption): class GfxMod(DefaultOffToggle):
""" """
Sets the sprite for link, among other things If enabled, the patcher will prompt the user for a modification file to change sprites in the game and optionally some text.
The option should be the same name as a with sprite (and optional name) file in data/sprites/ladx
""" """
display_name = "GFX Modification" display_name = "GFX Modification"
ladxr_name = "gfxmod"
normal = ''
default = 'Link'
__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"]
for file in os.listdir(__spriteDir):
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())}")
def to_ladxr_option(self, all_options):
if self.value == -1 or self.value == "Link":
return None, None
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]}")
return self.ladxr_name, self.__spriteDir + "/" + GfxMod.__spriteFiles[self.value][0]
class Palette(Choice): class Palette(Choice):

View File

@@ -3,19 +3,112 @@ import worlds.Files
import hashlib import hashlib
import Utils import Utils
import os import os
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" LADX_HASH = "07c211479386825042efb4ad31bb525f"
class LADXDeltaPatch(worlds.Files.APDeltaPatch): if TYPE_CHECKING:
from . import LinksAwakeningWorld
class LADXPatchExtensions(worlds.Files.APPatchExtension):
game = LINKS_AWAKENING
@staticmethod
def generate_rom(caller: worlds.Files.APProcedurePatch, rom: bytes, data_file: str) -> bytes:
patch_data = json.loads(caller.get_file(data_file).decode("utf-8"))
# TODO local option overrides
rom_name = get_base_rom_path()
out_name = f"{patch_data['out_base']}{caller.result_file_ending}"
parser = get_parser()
args = parser.parse_args([rom_name, "-o", out_name, "--dump"])
return generator.generateRom(rom, args, patch_data)
@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"]:
return bsdiff4.patch(rom, pkgutil.get_data(__name__, "LADXR/patches/title_screen.bdiff4"))
return rom
class LADXProcedurePatch(worlds.Files.APProcedurePatch):
hash = LADX_HASH hash = LADX_HASH
game = "Links Awakening DX" game = LINKS_AWAKENING
patch_file_ending = ".apladx" patch_file_ending: str = ".apladx"
result_file_ending: str = ".gbc" result_file_ending: str = ".gbc"
procedure = [
("generate_rom", ["data.json"]),
("patch_title_screen", ["data.json"])
]
@classmethod @classmethod
def get_source_data(cls) -> bytes: def get_source_data(cls) -> bytes:
return get_base_rom_bytes() return get_base_rom_bytes()
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 = {
"out_base": world.multiworld.get_out_file_name_base(patch.player),
"is_race": world.multiworld.is_race,
"seed": world.multiworld.seed,
"seed_name": world.multiworld.seed_name,
"multi_key": binascii.hexlify(world.multi_key).decode(),
"player": patch.player,
"player_name": patch.player_name,
"other_player_names": list(world.multiworld.player_name.values()),
"item_list": binascii.hexlify(item_list).decode(),
"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",
),
}
patch.write_file("data.json", json.dumps(data_dict).encode('utf-8'))
def get_base_rom_bytes(file_name: str = "") -> bytes: def get_base_rom_bytes(file_name: str = "") -> bytes:
base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
if not base_rom_bytes: if not base_rom_bytes:

View File

@@ -1,16 +1,12 @@
import binascii import binascii
import dataclasses import dataclasses
import os import os
import pkgutil
import tempfile
import typing import typing
import logging import logging
import re import re
import bsdiff4
import settings import settings
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Tutorial, MultiWorld from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Tutorial
from Fill import fill_restrictive from Fill import fill_restrictive
from worlds.AutoWorld import WebWorld, World from worlds.AutoWorld import WebWorld, World
from .Common import * from .Common import *
@@ -18,19 +14,17 @@ from . import ItemIconGuessing
from .Items import (DungeonItemData, DungeonItemType, ItemName, LinksAwakeningItem, TradeItemData, from .Items import (DungeonItemData, DungeonItemType, ItemName, LinksAwakeningItem, TradeItemData,
ladxr_item_to_la_item_name, links_awakening_items, links_awakening_items_by_name, ladxr_item_to_la_item_name, links_awakening_items, links_awakening_items_by_name,
links_awakening_item_name_groups) links_awakening_item_name_groups)
from .LADXR import generator
from .LADXR.itempool import ItemPool as LADXRItemPool from .LADXR.itempool import ItemPool as LADXRItemPool
from .LADXR.locations.constants import CHEST_ITEMS from .LADXR.locations.constants import CHEST_ITEMS
from .LADXR.locations.instrument import Instrument from .LADXR.locations.instrument import Instrument
from .LADXR.logic import Logic as LADXRLogic from .LADXR.logic import Logic as LADXRLogic
from .LADXR.main import get_parser
from .LADXR.settings import Settings as LADXRSettings from .LADXR.settings import Settings as LADXRSettings
from .LADXR.worldSetup import WorldSetup as LADXRWorldSetup from .LADXR.worldSetup import WorldSetup as LADXRWorldSetup
from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion, from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion,
create_regions_from_ladxr, get_locations_to_id, create_regions_from_ladxr, get_locations_to_id,
links_awakening_location_name_groups) links_awakening_location_name_groups)
from .Options import DungeonItemShuffle, ShuffleInstruments, LinksAwakeningOptions, ladx_option_groups from .Options import DungeonItemShuffle, ShuffleInstruments, LinksAwakeningOptions, ladx_option_groups
from .Rom import LADXDeltaPatch, get_base_rom_path from .Rom import LADXProcedurePatch, write_patch_data
DEVELOPER_MODE = False DEVELOPER_MODE = False
@@ -40,7 +34,7 @@ class LinksAwakeningSettings(settings.Group):
"""File name of the Link's Awakening DX rom""" """File name of the Link's Awakening DX rom"""
copy_to = "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc" copy_to = "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"
description = "LADX ROM File" description = "LADX ROM File"
md5s = [LADXDeltaPatch.hash] md5s = [LADXProcedurePatch.hash]
class RomStart(str): class RomStart(str):
""" """
@@ -57,8 +51,16 @@ class LinksAwakeningSettings(settings.Group):
class DisplayMsgs(settings.Bool): class DisplayMsgs(settings.Bool):
"""Display message inside of Bizhawk""" """Display message inside of Bizhawk"""
class GfxModFile(settings.FilePath):
"""
Gfxmod file, get it from upstream: https://github.com/daid/LADXR/tree/master/gfx
Only .bin or .bdiff files
The same directory will be checked for a matching text modification file
"""
rom_file: RomFile = RomFile(RomFile.copy_to) rom_file: RomFile = RomFile(RomFile.copy_to)
rom_start: typing.Union[RomStart, bool] = True rom_start: typing.Union[RomStart, bool] = True
gfx_mod_file: GfxModFile = GfxModFile()
class LinksAwakeningWebWorld(WebWorld): class LinksAwakeningWebWorld(WebWorld):
tutorials = [Tutorial( tutorials = [Tutorial(
@@ -179,10 +181,10 @@ class LinksAwakeningWorld(World):
assert(start) assert(start)
menu_region = LinksAwakeningRegion("Menu", None, "Menu", self.player, self.multiworld) menu_region = LinksAwakeningRegion("Menu", None, "Menu", self.player, self.multiworld)
menu_region.exits = [Entrance(self.player, "Start Game", menu_region)] menu_region.exits = [Entrance(self.player, "Start Game", menu_region)]
menu_region.exits[0].connect(start) menu_region.exits[0].connect(start)
self.multiworld.regions.append(menu_region) self.multiworld.regions.append(menu_region)
# Place RAFT, other access events # Place RAFT, other access events
@@ -190,14 +192,14 @@ class LinksAwakeningWorld(World):
for loc in region.locations: for loc in region.locations:
if loc.address is None: if loc.address is None:
loc.place_locked_item(self.create_event(loc.ladxr_item.event)) loc.place_locked_item(self.create_event(loc.ladxr_item.event))
# Connect Windfish -> Victory # Connect Windfish -> Victory
windfish = self.multiworld.get_region("Windfish", self.player) windfish = self.multiworld.get_region("Windfish", self.player)
l = Location(self.player, "Windfish", parent=windfish) l = Location(self.player, "Windfish", parent=windfish)
windfish.locations = [l] windfish.locations = [l]
l.place_locked_item(self.create_event("An Alarm Clock")) l.place_locked_item(self.create_event("An Alarm Clock"))
self.multiworld.completion_condition[self.player] = lambda state: state.has("An Alarm Clock", player=self.player) self.multiworld.completion_condition[self.player] = lambda state: state.has("An Alarm Clock", player=self.player)
def create_item(self, item_name: str): def create_item(self, item_name: str):
@@ -279,8 +281,8 @@ class LinksAwakeningWorld(World):
event_location = Location(self.player, "Can Play Trendy Game", parent=trendy_region) event_location = Location(self.player, "Can Play Trendy Game", parent=trendy_region)
trendy_region.locations.insert(0, event_location) trendy_region.locations.insert(0, event_location)
event_location.place_locked_item(self.create_event("Can Play Trendy Game")) event_location.place_locked_item(self.create_event("Can Play Trendy Game"))
self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []] self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []]
for r in self.multiworld.get_regions(self.player): for r in self.multiworld.get_regions(self.player):
# Set aside dungeon locations # Set aside dungeon locations
if r.dungeon_index: if r.dungeon_index:
@@ -354,7 +356,7 @@ class LinksAwakeningWorld(World):
# set containing the list of all possible dungeon locations for the player # set containing the list of all possible dungeon locations for the player
all_dungeon_locs = set() all_dungeon_locs = set()
# Do dungeon specific things # Do dungeon specific things
for dungeon_index in range(0, 9): for dungeon_index in range(0, 9):
# set up allow-list for dungeon specific items # set up allow-list for dungeon specific items
@@ -367,7 +369,7 @@ class LinksAwakeningWorld(World):
# ...also set the rules for the dungeon # ...also set the rules for the dungeon
for location in locs: for location in locs:
orig_rule = location.item_rule orig_rule = location.item_rule
# If an item is about to be placed on a dungeon location, it can go there iff # If an item is about to be placed on a dungeon location, it can go there iff
# 1. it fits the general rules for that location (probably 'return True' for most places) # 1. it fits the general rules for that location (probably 'return True' for most places)
# 2. Either # 2. Either
# 2a. it's not a restricted dungeon item # 2a. it's not a restricted dungeon item
@@ -421,7 +423,7 @@ class LinksAwakeningWorld(World):
partial_all_state.sweep_for_advancements() partial_all_state.sweep_for_advancements()
fill_restrictive(self.multiworld, partial_all_state, all_dungeon_locs_to_fill, all_dungeon_items_to_fill, lock=True, single_player_placement=True, allow_partial=False) fill_restrictive(self.multiworld, partial_all_state, all_dungeon_locs_to_fill, all_dungeon_items_to_fill, lock=True, single_player_placement=True, allow_partial=False)
name_cache = {} name_cache = {}
# Tries to associate an icon from another game with an icon we have # Tries to associate an icon from another game with an icon we have
@@ -458,22 +460,16 @@ class LinksAwakeningWorld(World):
for name in possibles: for name in possibles:
if name in self.name_cache: if name in self.name_cache:
return self.name_cache[name] return self.name_cache[name]
return "TRADING_ITEM_LETTER" return "TRADING_ITEM_LETTER"
@classmethod
def stage_assert_generate(cls, multiworld: MultiWorld):
rom_file = get_base_rom_path()
if not os.path.exists(rom_file):
raise FileNotFoundError(rom_file)
def generate_output(self, output_directory: str): def generate_output(self, output_directory: str):
# copy items back to locations # copy items back to locations
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)
# 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 loc.ladxr_item.item = loc.item.item_data.ladxr_id
@@ -499,31 +495,13 @@ class LinksAwakeningWorld(World):
# Kind of kludge, make it possible for the location to differentiate between local and remote items # Kind of kludge, make it possible for the location to differentiate between local and remote items
loc.ladxr_item.location_owner = self.player 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.player_name}.gbc" patch = LADXProcedurePatch(player=self.player, player_name=self.player_name)
out_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.gbc") write_patch_data(self, patch)
out_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}"
f"{patch.patch_file_ending}")
parser = get_parser() patch.write(out_path)
args = parser.parse_args([rom_name, "-o", out_name, "--dump"])
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.options.ap_title_screen:
with tempfile.NamedTemporaryFile(delete=False) as title_patch:
title_patch.write(pkgutil.get_data(__name__, "LADXR/patches/title_screen.bdiff4"))
bsdiff4.file_patch_inplace(out_path, title_patch.name)
os.unlink(title_patch.name)
patch = LADXDeltaPatch(os.path.splitext(out_path)[0]+LADXDeltaPatch.patch_file_ending, player=self.player,
player_name=self.player_name, patched_path=out_path)
patch.write()
if not DEVELOPER_MODE:
os.unlink(out_path)
def generate_multi_key(self): def generate_multi_key(self):
return bytearray(self.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')