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.machinery
import os
import pkgutil
import random
import pickle
import Utils
import settings
from collections import defaultdict
from typing import TYPE_CHECKING
from typing import Dict
from .romTables import ROMWithTables
from . import assembler
from . import mapgen
from . import patches
from .patches import overworld as _
from .patches import dungeon as _
@@ -57,27 +59,20 @@ from .patches import tradeSequence as _
from . import hints
from .patches import bank34
from .utils import formatText
from .roomEditor import RoomEditor, Object
from .patches.aesthetics import rgb_to_bin, bin_to_rgb
from .locations.keyLocation import KeyLocation
from BaseClasses import ItemClassification
from ..Locations import LinksAwakeningLocation
from ..Options import TrendyGame, Palette, MusicChangeCondition, Warps
if TYPE_CHECKING:
from .. import LinksAwakeningWorld
from .. import Options
# 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 = []
player_names = list(world.multiworld.player_name.values())
rom = ROMWithTables(args.input_filename, rom_patches)
rom.player_names = player_names
rom = ROMWithTables(base_rom, rom_patches)
rom.player_names = patch_data["other_player_names"]
pymods = []
if args.pymod:
for pymod in args.pymod:
@@ -88,10 +83,13 @@ def generateRom(args, world: "LinksAwakeningWorld"):
for pymod in pymods:
pymod.prePatch(rom)
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 world.ladxr_logic.iteminfo_list if not isinstance(item, KeyLocation)]
if options["gfxmod"]:
user_settings = settings.get_settings()
try:
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.const("INV_SIZE", 16)
@@ -121,7 +119,7 @@ def generateRom(args, world: "LinksAwakeningWorld"):
assembler.const("wLinkSpawnDelay", 0xDE13)
#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.save.singleSaveSlot(rom)
@@ -135,7 +133,7 @@ def generateRom(args, world: "LinksAwakeningWorld"):
patches.core.easyColorDungeonAccess(rom)
patches.owl.removeOwlEvents(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.bank34.addBank34(rom, item_list)
patches.core.removeGhost(rom)
@@ -144,19 +142,17 @@ def generateRom(args, world: "LinksAwakeningWorld"):
patches.core.alwaysAllowSecretBook(rom)
patches.core.injectMainLoop(rom)
from ..Options import ShuffleSmallKeys, ShuffleNightmareKeys
if world.options.shuffle_small_keys != ShuffleSmallKeys.option_original_dungeon or\
world.options.shuffle_nightmare_keys != ShuffleNightmareKeys.option_original_dungeon:
if options["shuffle_small_keys"] != Options.ShuffleSmallKeys.option_original_dungeon or\
options["shuffle_nightmare_keys"] != Options.ShuffleNightmareKeys.option_original_dungeon:
patches.inventory.advancedInventorySubscreen(rom)
patches.inventory.moreSlots(rom)
if world.ladxr_settings.witch:
patches.witch.updateWitch(rom)
# if ladxr_settings["witch"]:
patches.witch.updateWitch(rom)
patches.softlock.fixAll(rom)
if not world.ladxr_settings.rooster:
if not options["rooster"]:
patches.maptweaks.tweakMap(rom)
patches.maptweaks.tweakBirdKeyRoom(rom)
if world.ladxr_settings.overworld == "openmabe":
if options["overworld"] == Options.Overworld.option_open_mabe:
patches.maptweaks.openMabe(rom)
patches.chest.fixChests(rom)
patches.shop.fixShop(rom)
@@ -168,10 +164,10 @@ def generateRom(args, world: "LinksAwakeningWorld"):
patches.tarin.updateTarin(rom)
patches.fishingMinigame.updateFinishingMinigame(rom)
patches.health.upgradeHealthContainers(rom)
if world.ladxr_settings.owlstatues in ("dungeon", "both"):
patches.owl.upgradeDungeonOwlStatues(rom)
if world.ladxr_settings.owlstatues in ("overworld", "both"):
patches.owl.upgradeOverworldOwlStatues(rom)
# if ladxr_settings["owlstatues"] in ("dungeon", "both"):
# patches.owl.upgradeDungeonOwlStatues(rom)
# if ladxr_settings["owlstatues"] in ("overworld", "both"):
# patches.owl.upgradeOverworldOwlStatues(rom)
patches.goldenLeaf.fixGoldenLeaf(rom)
patches.heartPiece.fixHeartPiece(rom)
patches.seashell.fixSeashell(rom)
@@ -180,143 +176,95 @@ def generateRom(args, world: "LinksAwakeningWorld"):
patches.songs.upgradeMarin(rom)
patches.songs.upgradeManbo(rom)
patches.songs.upgradeMamu(rom)
patches.tradeSequence.patchTradeSequence(rom, world.ladxr_settings)
patches.bowwow.fixBowwow(rom, everywhere=world.ladxr_settings.bowwow != 'normal')
if world.ladxr_settings.bowwow != 'normal':
patches.bowwow.bowwowMapPatches(rom)
patches.tradeSequence.patchTradeSequence(rom, options)
patches.bowwow.fixBowwow(rom, everywhere=False)
# if ladxr_settings["bowwow"] != 'normal':
# patches.bowwow.bowwowMapPatches(rom)
patches.desert.desertAccess(rom)
if world.ladxr_settings.overworld == 'dungeondive':
patches.overworld.patchOverworldTilesets(rom)
patches.overworld.createDungeonOnlyOverworld(rom)
elif world.ladxr_settings.overworld == 'nodungeons':
patches.dungeon.patchNoDungeons(rom)
elif world.ladxr_settings.overworld == 'random':
patches.overworld.patchOverworldTilesets(rom)
mapgen.store_map(rom, world.ladxr_logic.world.map)
# if ladxr_settings["overworld"] == 'dungeondive':
# patches.overworld.patchOverworldTilesets(rom)
# patches.overworld.createDungeonOnlyOverworld(rom)
# elif ladxr_settings["overworld"] == 'nodungeons':
# patches.dungeon.patchNoDungeons(rom)
#elif world.ladxr_settings["overworld"] == 'random':
# patches.overworld.patchOverworldTilesets(rom)
# 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 world.options.music_change_condition == MusicChangeCondition.option_always:
if options["music_change_condition"] == Options.MusicChangeCondition.option_always:
patches.aesthetics.noSwordMusic(rom)
patches.aesthetics.reduceMessageLengths(rom, world.random)
patches.aesthetics.reduceMessageLengths(rom, random)
patches.aesthetics.allowColorDungeonSpritesEverywhere(rom)
if world.ladxr_settings.music == 'random':
patches.music.randomizeMusic(rom, world.random)
elif world.ladxr_settings.music == 'off':
if options["music"] == Options.Music.option_shuffled:
patches.music.randomizeMusic(rom, random)
elif options["music"] == Options.Music.option_off:
patches.music.noMusic(rom)
if world.ladxr_settings.noflash:
if options["no_flash"]:
patches.aesthetics.removeFlashingLights(rom)
if world.ladxr_settings.hardmode == "oracle":
if options["hard_mode"] == Options.HardMode.option_oracle:
patches.hardMode.oracleMode(rom)
elif world.ladxr_settings.hardmode == "hero":
elif options["hard_mode"] == Options.HardMode.option_hero:
patches.hardMode.heroMode(rom)
elif world.ladxr_settings.hardmode == "ohko":
elif options["hard_mode"] == Options.HardMode.option_ohko:
patches.hardMode.oneHitKO(rom)
if world.ladxr_settings.superweapons:
patches.weapons.patchSuperWeapons(rom)
if world.ladxr_settings.textmode == 'fast':
#if ladxr_settings["superweapons"]:
# patches.weapons.patchSuperWeapons(rom)
if options["text_mode"] == Options.TextMode.option_fast:
patches.aesthetics.fastText(rom)
if world.ladxr_settings.textmode == 'none':
patches.aesthetics.fastText(rom)
patches.aesthetics.noText(rom)
if not world.ladxr_settings.nagmessages:
#if ladxr_settings["textmode"] == 'none':
# patches.aesthetics.fastText(rom)
# patches.aesthetics.noText(rom)
if not options["nag_messages"]:
patches.aesthetics.removeNagMessages(rom)
if world.ladxr_settings.lowhpbeep == 'slow':
if options["low_hp_beep"] == Options.LowHpBeep.option_slow:
patches.aesthetics.slowLowHPBeep(rom)
if world.ladxr_settings.lowhpbeep == 'none':
if options["low_hp_beep"] == Options.LowHpBeep.option_none:
patches.aesthetics.removeLowHPBeep(rom)
if 0 <= int(world.ladxr_settings.linkspalette):
patches.aesthetics.forceLinksPalette(rom, int(world.ladxr_settings.linkspalette))
if 0 <= options["link_palette"]:
patches.aesthetics.forceLinksPalette(rom, options["link_palette"])
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 world.ladxr_settings.steal == 'never':
rom.patch(4, 0x36F9, "FA4EDB", "3E0000")
elif world.ladxr_settings.steal == 'always':
rom.patch(4, 0x36F9, "FA4EDB", "3E0100")
#if ladxr_settings["steal"] == 'never':
# rom.patch(4, 0x36F9, "FA4EDB", "3E0000")
#elif ladxr_settings["steal"] == 'always':
# rom.patch(4, 0x36F9, "FA4EDB", "3E0100")
if world.ladxr_settings.hpmode == 'inverted':
patches.health.setStartHealth(rom, 9)
elif world.ladxr_settings.hpmode == '1':
patches.health.setStartHealth(rom, 1)
#if ladxr_settings["hpmode"] == 'inverted':
# patches.health.setStartHealth(rom, 9)
#elif ladxr_settings["hpmode"] == '1':
# patches.health.setStartHealth(rom, 1)
patches.inventory.songSelectAfterOcarinaSelect(rom)
if world.ladxr_settings.quickswap == 'a':
if options["quickswap"] == 'a':
patches.core.quickswap(rom, 1)
elif world.ladxr_settings.quickswap == 'b':
elif options["quickswap"] == 'b':
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
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":
if patch_data["world_setup"]["goal"] == "raft":
patches.goal.setRaftGoal(rom)
elif world_setup.goal in ("bingo", "bingo-full"):
patches.bingo.setBingoGoal(rom, world_setup.bingo_goals, world_setup.goal)
elif world_setup.goal == "seashells":
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":
patches.goal.setSeashellGoal(rom, 20)
else:
patches.goal.setRequiredInstrumentCount(rom, world_setup.goal)
patches.goal.setRequiredInstrumentCount(rom, patch_data["world_setup"]["goal"])
# Patch the generated logic into the rom
patches.chest.setMultiChest(rom, world_setup.multichest)
if world.ladxr_settings.overworld not in {"dungeondive", "random"}:
patches.entrances.changeEntrances(rom, world_setup.entrance_mapping)
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:]
@@ -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
mw = 100
spot.patch(rom, spot.item, multiworld=mw)
patches.enemies.changeBosses(rom, world_setup.boss_mapping)
patches.enemies.changeMiniBosses(rom, world_setup.miniboss_mapping)
patches.enemies.changeBosses(rom, patch_data["world_setup"]["boss_mapping"])
patches.enemies.changeMiniBosses(rom, patch_data["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, world.multi_key, world.multiworld.seed_name, world.ladxr_settings,
world.player_name, world.player)
if world.options.ap_title_screen:
patches.titleScreen.setRomInfo(rom, patch_data)
if options["ap_title_screen"]:
patches.titleScreen.setTitleGraphics(rom)
patches.endscreen.updateEndScreen(rom)
patches.aesthetics.updateSpriteData(rom)
if args.doubletrouble:
patches.enemies.doubleTrouble(rom)
if world.options.text_shuffle:
if options["text_shuffle"]:
excluded_ids = [
# Overworld owl statues
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]
buckets = defaultdict(list)
# 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):
# 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:
@@ -395,20 +343,20 @@ def generateRom(args, world: "LinksAwakeningWorld"):
for bucket in buckets.values():
# For each bucket, make a copy and shuffle
shuffled = bucket.copy()
world.random.shuffle(shuffled)
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 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
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
for i in range(0, 6):
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)
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::
db $FC, $00, $04, $00, $00
@@ -428,17 +376,18 @@ def generateRom(args, world: "LinksAwakeningWorld"):
db $00, $04, $00, $FC, $00
"""
speeds = {
TrendyGame.option_harder: (3, 8),
TrendyGame.option_hardest: (3, 8),
TrendyGame.option_impossible: (3, 16),
Options.TrendyGame.option_harder: (3, 8),
Options.TrendyGame.option_hardest: (3, 8),
Options.TrendyGame.option_impossible: (3, 16),
}
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][0x76A2-0x4000] = speed()
rom.banks[0x4][0x76A6-0x4000] = 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][0x76A3-0x4000] = speed()
rom.banks[0x4][0x76A5-0x4000] = speed()
@@ -462,11 +411,11 @@ def generateRom(args, world: "LinksAwakeningWorld"):
for channel in range(3):
color[channel] = color[channel] * 31 // 0xbc
if world.options.warps != Warps.option_vanilla:
patches.core.addWarpImprovements(rom, world.options.warps == Warps.option_improved_additional)
if options["warps"] != Options.Warps.option_vanilla:
patches.core.addWarpImprovements(rom, options["warps"] == Options.Warps.option_improved_additional)
palette = world.options.palette
if palette != Palette.option_normal:
palette = options["palette"]
if palette != Options.Palette.option_normal:
ranges = {
# Object palettes
# Overworld palettes
@@ -496,22 +445,22 @@ def generateRom(args, world: "LinksAwakeningWorld"):
r,g,b = bin_to_rgb(packed)
# 1 bit
if palette == Palette.option_1bit:
if palette == Options.Palette.option_1bit:
r &= 0b10000
g &= 0b10000
b &= 0b10000
# 2 bit
elif palette == Palette.option_1bit:
elif palette == Options.Palette.option_1bit:
r &= 0b11000
g &= 0b11000
b &= 0b11000
# Invert
elif palette == Palette.option_inverted:
elif palette == Options.Palette.option_inverted:
r = 31 - r
g = 31 - g
b = 31 - b
# Pink
elif palette == Palette.option_pink:
elif palette == Options.Palette.option_pink:
r = r // 2
r += 16
r = int(r)
@@ -520,7 +469,7 @@ def generateRom(args, world: "LinksAwakeningWorld"):
b += 16
b = int(b)
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 = (r + g + b) // 3
r = g = b = gray
@@ -531,10 +480,10 @@ def generateRom(args, world: "LinksAwakeningWorld"):
SEED_LOCATION = 0x0134
# Patch over the title
assert(len(world.multi_key) == 12)
rom.patch(0x00, SEED_LOCATION, None, binascii.hexlify(world.multi_key))
assert(len(multi_key) == 12)
rom.patch(0x00, SEED_LOCATION, None, binascii.hexlify(multi_key))
for pymod in pymods:
pymod.postPatch(rom)
return rom
return rom.save()

View File

@@ -1,5 +1,7 @@
from .locations.items import *
from .utils import formatText
from BaseClasses import ItemClassification
from ..Locations import LinksAwakeningLocation
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()
rnd.shuffle(text_ids)
for text_id in text_ids:
hint = hint_generator()
hint = hint_texts_copy.pop()
if not hint:
hint = rnd.choice(hints).format(*rnd.choice(useless_hint))
rom.texts[text_id] = formatText(hint)
for text_id in range(0x200, 0x20C, 2):
rom.texts[text_id] = formatText("Read this book?", ask="YES NO")
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][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:
return
consts = {
@@ -578,7 +578,7 @@ def addBootsControls(rom, boots_controls: BootsControls):
jr z, .yesBoots
ld a, [hl]
"""
}[boots_controls.value]
}[boots_controls]
# The new code fits exactly within Nintendo's poorly space optimzied code while having more features
boots_code = assembler.ASM("""

View File

@@ -42,7 +42,7 @@ MINIBOSS_ENTITIES = {
"ARMOS_KNIGHT": [(4, 3, 0x88)],
}
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,
"moblin_cave": 0x2E1,
"armos_temple": 0x27F,

View File

@@ -1,7 +1,6 @@
from ..backgroundEditor import BackgroundEditor
from .aesthetics import rgb_to_bin, bin_to_rgb, prepatch
import copy
import pkgutil
CHAR_MAP = {'z': 0x3E, '-': 0x3F, '.': 0x39, ':': 0x42, '?': 0x3C, '!': 0x3D}
def _encode(s):
@@ -18,17 +17,18 @@ def _encode(s):
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:
seednr = int(seed, 16)
seednr = int(patch_data["seed"], 16)
except:
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"
if isinstance(settings.race, str):
seed_name += " " + settings.race
if isinstance(patch_data["is_race"], str):
seed_name += " " + patch_data["is_race"]
rom.patch(0x00, 0x07, "00", "01")
else:
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:])
BASE_DRAWING_AREA = 0x98a0
LINE_WIDTH = 0x20
player_id_text = f"Player {player_id}:"
player_id_text = f"Player {patch_data['player']}:"
for n in (3, 4):
be = BackgroundEditor(rom, n)
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)):
be.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 5 + 2 + n] = v
ba.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 5 + 2 + n] = 0x00
for n, v in enumerate(_encode(player_name)):
be.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 6 + 0x13 - len(player_name) + n] = v
ba.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 6 + 0x13 - len(player_name) + n] = 0x00
for n, v in enumerate(_encode(patch_data['player_name'])):
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(patch_data['player_name']) + n] = 0x00
for n, v in enumerate(line_1_hex):
be.tiles[0x9a20 + n] = v
ba.tiles[0x9a20 + n] = 0x00

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

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

View File

@@ -181,8 +181,8 @@ class IndoorRoomSpriteData(PointerTable):
class ROMWithTables(ROM):
def __init__(self, filename, patches=None):
super().__init__(filename, patches)
def __init__(self, data, patches=None):
super().__init__(data, patches)
# Ability to patch any text in the game with different text
self.texts = Texts(self)
@@ -203,7 +203,7 @@ class ROMWithTables(ROM):
self.itemNames = {}
def save(self, filename, *, name=None):
def save(self):
# Assert special handling of bank 9 expansion is fine
for i in range(0x3d42, 0x4000):
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.background_tiles.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
class GfxMod(FreeText, LADXROption):
class GfxMod(DefaultOffToggle):
"""
Sets the sprite for link, among other things
The option should be the same name as a with sprite (and optional name) file in data/sprites/ladx
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"
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):

View File

@@ -3,19 +3,112 @@ import worlds.Files
import hashlib
import Utils
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"
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
game = "Links Awakening DX"
patch_file_ending = ".apladx"
game = LINKS_AWAKENING
patch_file_ending: str = ".apladx"
result_file_ending: str = ".gbc"
procedure = [
("generate_rom", ["data.json"]),
("patch_title_screen", ["data.json"])
]
@classmethod
def get_source_data(cls) -> 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:
base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
if not base_rom_bytes:

View File

@@ -1,16 +1,12 @@
import binascii
import dataclasses
import os
import pkgutil
import tempfile
import typing
import logging
import re
import bsdiff4
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 worlds.AutoWorld import WebWorld, World
from .Common import *
@@ -18,19 +14,17 @@ from . import ItemIconGuessing
from .Items import (DungeonItemData, DungeonItemType, ItemName, LinksAwakeningItem, TradeItemData,
ladxr_item_to_la_item_name, links_awakening_items, links_awakening_items_by_name,
links_awakening_item_name_groups)
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 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,
links_awakening_location_name_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
@@ -40,7 +34,7 @@ class LinksAwakeningSettings(settings.Group):
"""File name of the Link's Awakening DX rom"""
copy_to = "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"
description = "LADX ROM File"
md5s = [LADXDeltaPatch.hash]
md5s = [LADXProcedurePatch.hash]
class RomStart(str):
"""
@@ -57,8 +51,16 @@ class LinksAwakeningSettings(settings.Group):
class DisplayMsgs(settings.Bool):
"""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_start: typing.Union[RomStart, bool] = True
gfx_mod_file: GfxModFile = GfxModFile()
class LinksAwakeningWebWorld(WebWorld):
tutorials = [Tutorial(
@@ -461,12 +463,6 @@ class LinksAwakeningWorld(World):
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):
# copy items back to locations
for r in self.multiworld.get_regions(self.player):
@@ -499,31 +495,13 @@ class LinksAwakeningWorld(World):
# Kind of kludge, make it possible for the location to differentiate between local and remote items
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"
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"])
patch = LADXProcedurePatch(player=self.player, player_name=self.player_name)
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}")
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)
patch.write(out_path)
def generate_multi_key(self):
return bytearray(self.random.getrandbits(8) for _ in range(10)) + self.player.to_bytes(2, 'big')