forked from mirror/Archipelago
Compare commits
3 Commits
main
...
active_web
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
069405b217 | ||
|
|
e78d92a4a8 | ||
|
|
ff52318b75 |
2
Utils.py
2
Utils.py
@@ -48,7 +48,7 @@ class Version(typing.NamedTuple):
|
||||
return ".".join(str(item) for item in self)
|
||||
|
||||
|
||||
__version__ = "0.6.5"
|
||||
__version__ = "0.6.6"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
|
||||
@@ -23,6 +23,17 @@ app.jinja_env.filters['any'] = any
|
||||
app.jinja_env.filters['all'] = all
|
||||
app.jinja_env.filters['get_file_safe_name'] = get_file_safe_name
|
||||
|
||||
# overwrites of flask default config
|
||||
app.config["DEBUG"] = False
|
||||
app.config["PORT"] = 80
|
||||
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
|
||||
app.config["MAX_CONTENT_LENGTH"] = 64 * 1024 * 1024 # 64 megabyte limit
|
||||
# if you want to deploy, make sure you have a non-guessable secret key
|
||||
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
|
||||
app.config["SESSION_PERMANENT"] = True
|
||||
app.config["MAX_FORM_MEMORY_SIZE"] = 2 * 1024 * 1024 # 2 MB, needed for large option pages such as SC2
|
||||
|
||||
# custom config
|
||||
app.config["SELFHOST"] = True # application process is in charge of running the websites
|
||||
app.config["GENERATORS"] = 8 # maximum concurrent world gens
|
||||
app.config["HOSTERS"] = 8 # maximum concurrent room hosters
|
||||
@@ -30,19 +41,12 @@ app.config["SELFLAUNCH"] = True # application process is in charge of launching
|
||||
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
|
||||
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
|
||||
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
|
||||
app.config["DEBUG"] = False
|
||||
app.config["PORT"] = 80
|
||||
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||
app.config['MAX_CONTENT_LENGTH'] = 64 * 1024 * 1024 # 64 megabyte limit
|
||||
# if you want to deploy, make sure you have a non-guessable secret key
|
||||
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
|
||||
# at what amount of worlds should scheduling be used, instead of rolling in the web-thread
|
||||
app.config["JOB_THRESHOLD"] = 1
|
||||
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
|
||||
app.config["JOB_TIME"] = 600
|
||||
# memory limit for generator processes in bytes
|
||||
app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296
|
||||
app.config['SESSION_PERMANENT'] = True
|
||||
|
||||
# waitress uses one thread for I/O, these are for processing of views that then get sent
|
||||
# archipelago.gg uses gunicorn + nginx; ignoring this option
|
||||
|
||||
@@ -2,7 +2,6 @@ import binascii
|
||||
import importlib.util
|
||||
import importlib.machinery
|
||||
import random
|
||||
import pickle
|
||||
import Utils
|
||||
from collections import defaultdict
|
||||
from typing import Dict
|
||||
@@ -61,7 +60,11 @@ from .patches import bank34
|
||||
from .roomEditor import RoomEditor, Object
|
||||
from .patches.aesthetics import rgb_to_bin, bin_to_rgb
|
||||
|
||||
from .. import Options
|
||||
from .logic import Logic as LADXRLogic
|
||||
from .settings import Settings as LADXRSettings
|
||||
from .worldSetup import WorldSetup as LADXRWorldSetup
|
||||
from .locations.keyLocation import KeyLocation
|
||||
|
||||
|
||||
class VersionError(Exception):
|
||||
pass
|
||||
@@ -86,8 +89,27 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
|
||||
|
||||
random.seed(patch_data["seed"] + patch_data["player"])
|
||||
multi_key = binascii.unhexlify(patch_data["multi_key"].encode())
|
||||
item_list = pickle.loads(binascii.unhexlify(patch_data["item_list"].encode()))
|
||||
options = patch_data["options"]
|
||||
|
||||
ladxr_settings = LADXRSettings(patch_data["ladxr_settings_dict"])
|
||||
world_setup = LADXRWorldSetup()
|
||||
world_setup.goal = patch_data["world_setup"]["goal"]
|
||||
world_setup.multichest = patch_data["world_setup"]["multichest"]
|
||||
world_setup.entrance_mapping = patch_data["world_setup"]["entrance_mapping"]
|
||||
world_setup.boss_mapping = patch_data["world_setup"]["boss_mapping"]
|
||||
world_setup.miniboss_mapping = patch_data["world_setup"]["miniboss_mapping"]
|
||||
ladxr_logic = LADXRLogic(configuration_options=ladxr_settings, world_setup=world_setup)
|
||||
item_list = [item for item in ladxr_logic.iteminfo_list if not isinstance(item, KeyLocation)]
|
||||
for spot in patch_data["rom_item_placements"]:
|
||||
ladxr_item = next((item for item in item_list if item.nameId == spot["name_id"]), None)
|
||||
if not ladxr_item:
|
||||
continue
|
||||
ladxr_item.item = spot["item"][1:] if spot["item"].startswith('*') else spot["item"]
|
||||
ladxr_item.custom_item_name = spot["custom_item_name"]
|
||||
mw = None
|
||||
if patch_data["player"] != spot["item_owner"]:
|
||||
mw = min(spot["item_owner"], 101)
|
||||
ladxr_item.mw = mw
|
||||
|
||||
rom_patches = []
|
||||
rom = ROMWithTables(base_rom, rom_patches)
|
||||
rom.player_names = patch_data["other_player_names"]
|
||||
@@ -101,7 +123,7 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
|
||||
for pymod in pymods:
|
||||
pymod.prePatch(rom)
|
||||
|
||||
if options["gfxmod"]:
|
||||
if ladxr_settings.gfxmod:
|
||||
try:
|
||||
gfx_mod_file = LinksAwakeningWorld.settings.gfx_mod_file
|
||||
patches.aesthetics.gfxMod(rom, gfx_mod_file)
|
||||
@@ -136,7 +158,7 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
|
||||
assembler.const("wLinkSpawnDelay", 0xDE13)
|
||||
|
||||
#assembler.const("HARDWARE_LINK", 1)
|
||||
assembler.const("HARD_MODE", 1 if options["hard_mode"] else 0)
|
||||
assembler.const("HARD_MODE", 1 if ladxr_settings.hardmode else 0)
|
||||
|
||||
patches.core.cleanup(rom)
|
||||
patches.save.singleSaveSlot(rom)
|
||||
@@ -159,17 +181,16 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
|
||||
patches.core.alwaysAllowSecretBook(rom)
|
||||
patches.core.injectMainLoop(rom)
|
||||
|
||||
if options["shuffle_small_keys"] != Options.ShuffleSmallKeys.option_original_dungeon or\
|
||||
options["shuffle_nightmare_keys"] != Options.ShuffleNightmareKeys.option_original_dungeon:
|
||||
if ladxr_settings.shufflesmallkeys != 'originaldungeon' or ladxr_settings.shufflenightmarekeys != 'originaldungeon':
|
||||
patches.inventory.advancedInventorySubscreen(rom)
|
||||
patches.inventory.moreSlots(rom)
|
||||
# if ladxr_settings["witch"]:
|
||||
# if ladxr_settings.witch:
|
||||
patches.witch.updateWitch(rom)
|
||||
patches.softlock.fixAll(rom)
|
||||
if not options["rooster"]:
|
||||
if not ladxr_settings.rooster:
|
||||
patches.maptweaks.tweakMap(rom)
|
||||
patches.maptweaks.tweakBirdKeyRoom(rom)
|
||||
if options["overworld"] == Options.Overworld.option_open_mabe:
|
||||
if ladxr_settings.overworld == 'openmabe':
|
||||
patches.maptweaks.openMabe(rom)
|
||||
patches.chest.fixChests(rom)
|
||||
patches.shop.fixShop(rom)
|
||||
@@ -181,9 +202,9 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
|
||||
patches.tarin.updateTarin(rom)
|
||||
patches.fishingMinigame.updateFinishingMinigame(rom)
|
||||
patches.health.upgradeHealthContainers(rom)
|
||||
# if ladxr_settings["owlstatues"] in ("dungeon", "both"):
|
||||
# if ladxr_settings.owlstatues in ("dungeon", "both"):
|
||||
# patches.owl.upgradeDungeonOwlStatues(rom)
|
||||
# if ladxr_settings["owlstatues"] in ("overworld", "both"):
|
||||
# if ladxr_settings.owlstatues in ("overworld", "both"):
|
||||
# patches.owl.upgradeOverworldOwlStatues(rom)
|
||||
patches.goldenLeaf.fixGoldenLeaf(rom)
|
||||
patches.heartPiece.fixHeartPiece(rom)
|
||||
@@ -194,17 +215,17 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
|
||||
patches.songs.upgradeManbo(rom)
|
||||
patches.songs.upgradeMamu(rom)
|
||||
|
||||
patches.tradeSequence.patchTradeSequence(rom, options)
|
||||
patches.tradeSequence.patchTradeSequence(rom, ladxr_settings)
|
||||
patches.bowwow.fixBowwow(rom, everywhere=False)
|
||||
# if ladxr_settings["bowwow"] != 'normal':
|
||||
# if ladxr_settings.bowwow != 'normal':
|
||||
# patches.bowwow.bowwowMapPatches(rom)
|
||||
patches.desert.desertAccess(rom)
|
||||
# if ladxr_settings["overworld"] == 'dungeondive':
|
||||
# if ladxr_settings.overworld == 'dungeondive':
|
||||
# patches.overworld.patchOverworldTilesets(rom)
|
||||
# patches.overworld.createDungeonOnlyOverworld(rom)
|
||||
# elif ladxr_settings["overworld"] == 'nodungeons':
|
||||
# elif ladxr_settings.overworld == 'nodungeons':
|
||||
# patches.dungeon.patchNoDungeons(rom)
|
||||
#elif world.ladxr_settings["overworld"] == 'random':
|
||||
#elif ladxr_settings.overworld == 'random':
|
||||
# patches.overworld.patchOverworldTilesets(rom)
|
||||
# mapgen.store_map(rom, world.ladxr_logic.world.map)
|
||||
#if settings.dungeon_items == 'keysy':
|
||||
@@ -212,102 +233,94 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
|
||||
# patches.reduceRNG.slowdownThreeOfAKind(rom)
|
||||
patches.reduceRNG.fixHorseHeads(rom)
|
||||
patches.bomb.onlyDropBombsWhenHaveBombs(rom)
|
||||
if options["music_change_condition"] == Options.MusicChangeCondition.option_always:
|
||||
if ladxr_settings.musicchange == 'always':
|
||||
patches.aesthetics.noSwordMusic(rom)
|
||||
patches.aesthetics.reduceMessageLengths(rom, random)
|
||||
patches.aesthetics.allowColorDungeonSpritesEverywhere(rom)
|
||||
if options["music"] == Options.Music.option_shuffled:
|
||||
if ladxr_settings.music == 'shuffled':
|
||||
patches.music.randomizeMusic(rom, random)
|
||||
elif options["music"] == Options.Music.option_off:
|
||||
elif ladxr_settings.music == 'off':
|
||||
patches.music.noMusic(rom)
|
||||
if options["no_flash"]:
|
||||
if ladxr_settings.noflash:
|
||||
patches.aesthetics.removeFlashingLights(rom)
|
||||
if options["hard_mode"] == Options.HardMode.option_oracle:
|
||||
if ladxr_settings.hardmode == 'oracle':
|
||||
patches.hardMode.oracleMode(rom)
|
||||
elif options["hard_mode"] == Options.HardMode.option_hero:
|
||||
elif ladxr_settings.hardmode == 'hero':
|
||||
patches.hardMode.heroMode(rom)
|
||||
elif options["hard_mode"] == Options.HardMode.option_ohko:
|
||||
elif ladxr_settings.hardmode == 'ohko':
|
||||
patches.hardMode.oneHitKO(rom)
|
||||
#if ladxr_settings["superweapons"]:
|
||||
#if ladxr_settings.superweapons:
|
||||
# patches.weapons.patchSuperWeapons(rom)
|
||||
if options["text_mode"] == Options.TextMode.option_fast:
|
||||
if ladxr_settings.textmode == 'fast':
|
||||
patches.aesthetics.fastText(rom)
|
||||
#if ladxr_settings["textmode"] == 'none':
|
||||
#if ladxr_settings.textmode == 'none':
|
||||
# patches.aesthetics.fastText(rom)
|
||||
# patches.aesthetics.noText(rom)
|
||||
if not options["nag_messages"]:
|
||||
if not ladxr_settings.nagmessages:
|
||||
patches.aesthetics.removeNagMessages(rom)
|
||||
if options["low_hp_beep"] == Options.LowHpBeep.option_slow:
|
||||
if ladxr_settings.lowhpbeep == 'slow':
|
||||
patches.aesthetics.slowLowHPBeep(rom)
|
||||
if options["low_hp_beep"] == Options.LowHpBeep.option_none:
|
||||
if ladxr_settings.lowhpbeep == 'none':
|
||||
patches.aesthetics.removeLowHPBeep(rom)
|
||||
if 0 <= options["link_palette"]:
|
||||
patches.aesthetics.forceLinksPalette(rom, options["link_palette"])
|
||||
if 0 <= int(ladxr_settings.linkspalette):
|
||||
patches.aesthetics.forceLinksPalette(rom, int(ladxr_settings.linkspalette))
|
||||
if args.romdebugmode:
|
||||
# The default rom has this build in, just need to set a flag and we get this save.
|
||||
rom.patch(0, 0x0003, "00", "01")
|
||||
|
||||
# Patch the sword check on the shopkeeper turning around.
|
||||
if options["stealing"] == Options.Stealing.option_disabled:
|
||||
if ladxr_settings.steal == 'disabled':
|
||||
rom.patch(4, 0x36F9, "FA4EDB", "3E0000")
|
||||
rom.texts[0x2E] = utils.formatText("Hey! Welcome! Did you know that I have eyes on the back of my head?")
|
||||
rom.texts[0x2F] = utils.formatText("Nothing escapes my gaze! Your thieving ways shall never prosper!")
|
||||
|
||||
#if ladxr_settings["hpmode"] == 'inverted':
|
||||
#if ladxr_settings.hpmode == 'inverted':
|
||||
# patches.health.setStartHealth(rom, 9)
|
||||
#elif ladxr_settings["hpmode"] == '1':
|
||||
#elif ladxr_settings.hpmode == '1':
|
||||
# patches.health.setStartHealth(rom, 1)
|
||||
|
||||
patches.inventory.songSelectAfterOcarinaSelect(rom)
|
||||
if options["quickswap"] == Options.Quickswap.option_a:
|
||||
if ladxr_settings.quickswap == 'a':
|
||||
patches.core.quickswap(rom, 1)
|
||||
elif options["quickswap"] == Options.Quickswap.option_b:
|
||||
elif ladxr_settings.quickswap == 'b':
|
||||
patches.core.quickswap(rom, 0)
|
||||
|
||||
patches.core.addBootsControls(rom, options["boots_controls"])
|
||||
patches.core.addBootsControls(rom, ladxr_settings.bootscontrols)
|
||||
|
||||
random.seed(patch_data["seed"] + patch_data["player"])
|
||||
hints.addHints(rom, random, patch_data["hint_texts"])
|
||||
|
||||
if patch_data["world_setup"]["goal"] == "raft":
|
||||
if world_setup.goal == "raft":
|
||||
patches.goal.setRaftGoal(rom)
|
||||
elif patch_data["world_setup"]["goal"] in ("bingo", "bingo-full"):
|
||||
patches.bingo.setBingoGoal(rom, patch_data["world_setup"]["bingo_goals"], patch_data["world_setup"]["goal"])
|
||||
elif patch_data["world_setup"]["goal"] == "seashells":
|
||||
elif world_setup.goal in ("bingo", "bingo-full"):
|
||||
patches.bingo.setBingoGoal(rom, world_setup.bingo_goals, world_setup.goal)
|
||||
elif world_setup.goal == "seashells":
|
||||
patches.goal.setSeashellGoal(rom, 20)
|
||||
else:
|
||||
patches.goal.setRequiredInstrumentCount(rom, patch_data["world_setup"]["goal"])
|
||||
patches.goal.setRequiredInstrumentCount(rom, world_setup.goal)
|
||||
|
||||
# Patch the generated logic into the rom
|
||||
patches.chest.setMultiChest(rom, patch_data["world_setup"]["multichest"])
|
||||
#if ladxr_settings["overworld"] not in {"dungeondive", "random"}:
|
||||
patches.entrances.changeEntrances(rom, patch_data["world_setup"]["entrance_mapping"])
|
||||
for spot in item_list:
|
||||
if spot.item and spot.item.startswith("*"):
|
||||
spot.item = spot.item[1:]
|
||||
mw = None
|
||||
if spot.item_owner != spot.location_owner:
|
||||
mw = spot.item_owner
|
||||
if mw > 101:
|
||||
# There are only 101 player name slots (99 + "The Server" + "another world"), so don't use more than that
|
||||
mw = 101
|
||||
spot.patch(rom, spot.item, multiworld=mw)
|
||||
patches.enemies.changeBosses(rom, patch_data["world_setup"]["boss_mapping"])
|
||||
patches.enemies.changeMiniBosses(rom, patch_data["world_setup"]["miniboss_mapping"])
|
||||
patches.chest.setMultiChest(rom, world_setup.multichest)
|
||||
#if ladxr_settings.overworld not in {"dungeondive", "random"}:
|
||||
patches.entrances.changeEntrances(rom, world_setup.entrance_mapping)
|
||||
for ladxr_item in item_list:
|
||||
ladxr_item.patch(rom, ladxr_item.item, multiworld=ladxr_item.mw)
|
||||
patches.enemies.changeBosses(rom, world_setup.boss_mapping)
|
||||
patches.enemies.changeMiniBosses(rom, world_setup.miniboss_mapping)
|
||||
|
||||
if not args.romdebugmode:
|
||||
patches.core.addFrameCounter(rom, len(item_list))
|
||||
|
||||
patches.core.warpHome(rom) # Needs to be done after setting the start location.
|
||||
patches.titleScreen.setRomInfo(rom, patch_data)
|
||||
if options["ap_title_screen"]:
|
||||
if ladxr_settings.aptitlescreen:
|
||||
patches.titleScreen.setTitleGraphics(rom)
|
||||
patches.endscreen.updateEndScreen(rom)
|
||||
patches.aesthetics.updateSpriteData(rom)
|
||||
if args.doubletrouble:
|
||||
patches.enemies.doubleTrouble(rom)
|
||||
|
||||
if options["text_shuffle"]:
|
||||
if ladxr_settings.textshuffle:
|
||||
excluded_ids = [
|
||||
# Overworld owl statues
|
||||
0x1B6, 0x1B7, 0x1B8, 0x1B9, 0x1BA, 0x1BB, 0x1BC, 0x1BD, 0x1BE, 0x22D,
|
||||
@@ -366,14 +379,14 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
|
||||
rom.texts[shuffled[bucket_idx][0]] = data
|
||||
|
||||
|
||||
if options["trendy_game"] != Options.TrendyGame.option_normal:
|
||||
if ladxr_settings.trendygame != 'normal':
|
||||
|
||||
# TODO: if 0 or 4, 5, remove inaccurate conveyor tiles
|
||||
|
||||
|
||||
room_editor = RoomEditor(rom, 0x2A0)
|
||||
|
||||
if options["trendy_game"] == Options.TrendyGame.option_easy:
|
||||
if ladxr_settings.trendygame == 'easy':
|
||||
# Set physics flag on all objects
|
||||
for i in range(0, 6):
|
||||
rom.banks[0x4][0x6F1E + i -0x4000] = 0x4
|
||||
@@ -384,7 +397,7 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
|
||||
# Add new conveyor to "push" yoshi (it's only a visual)
|
||||
room_editor.objects.append(Object(5, 3, 0xD0))
|
||||
|
||||
if options["trendy_game"] >= Options.TrendyGame.option_harder:
|
||||
if ladxr_settings.trendygame in ('harder', 'hardest', 'impossible'):
|
||||
"""
|
||||
Data_004_76A0::
|
||||
db $FC, $00, $04, $00, $00
|
||||
@@ -393,18 +406,18 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
|
||||
db $00, $04, $00, $FC, $00
|
||||
"""
|
||||
speeds = {
|
||||
Options.TrendyGame.option_harder: (3, 8),
|
||||
Options.TrendyGame.option_hardest: (3, 8),
|
||||
Options.TrendyGame.option_impossible: (3, 16),
|
||||
'harder': (3, 8),
|
||||
'hardest': (3, 8),
|
||||
'impossible': (3, 16),
|
||||
}
|
||||
def speed():
|
||||
random.seed(patch_data["seed"] + patch_data["player"])
|
||||
return random.randint(*speeds[options["trendy_game"]])
|
||||
return random.randint(*speeds[ladxr_settings.trendygame])
|
||||
rom.banks[0x4][0x76A0-0x4000] = 0xFF - speed()
|
||||
rom.banks[0x4][0x76A2-0x4000] = speed()
|
||||
rom.banks[0x4][0x76A6-0x4000] = speed()
|
||||
rom.banks[0x4][0x76A8-0x4000] = 0xFF - speed()
|
||||
if options["trendy_game"] >= Options.TrendyGame.option_hardest:
|
||||
if ladxr_settings.trendygame in ('hardest', 'impossible'):
|
||||
rom.banks[0x4][0x76A1-0x4000] = 0xFF - speed()
|
||||
rom.banks[0x4][0x76A3-0x4000] = speed()
|
||||
rom.banks[0x4][0x76A5-0x4000] = speed()
|
||||
@@ -428,11 +441,11 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
|
||||
for channel in range(3):
|
||||
color[channel] = color[channel] * 31 // 0xbc
|
||||
|
||||
if options["warps"] != Options.Warps.option_vanilla:
|
||||
patches.core.addWarpImprovements(rom, options["warps"] == Options.Warps.option_improved_additional)
|
||||
if ladxr_settings.warps != 'vanilla':
|
||||
patches.core.addWarpImprovements(rom, ladxr_settings.warps == 'improved_additional')
|
||||
|
||||
palette = options["palette"]
|
||||
if palette != Options.Palette.option_normal:
|
||||
palette = ladxr_settings.palette
|
||||
if palette != 'normal':
|
||||
ranges = {
|
||||
# Object palettes
|
||||
# Overworld palettes
|
||||
@@ -462,22 +475,22 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
|
||||
r,g,b = bin_to_rgb(packed)
|
||||
|
||||
# 1 bit
|
||||
if palette == Options.Palette.option_1bit:
|
||||
if palette == '1bit':
|
||||
r &= 0b10000
|
||||
g &= 0b10000
|
||||
b &= 0b10000
|
||||
# 2 bit
|
||||
elif palette == Options.Palette.option_1bit:
|
||||
elif palette == '1bit':
|
||||
r &= 0b11000
|
||||
g &= 0b11000
|
||||
b &= 0b11000
|
||||
# Invert
|
||||
elif palette == Options.Palette.option_inverted:
|
||||
elif palette == 'inverted':
|
||||
r = 31 - r
|
||||
g = 31 - g
|
||||
b = 31 - b
|
||||
# Pink
|
||||
elif palette == Options.Palette.option_pink:
|
||||
elif palette == 'pink':
|
||||
r = r // 2
|
||||
r += 16
|
||||
r = int(r)
|
||||
@@ -486,7 +499,7 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
|
||||
b += 16
|
||||
b = int(b)
|
||||
b = clamp(b, 0, 0x1F)
|
||||
elif palette == Options.Palette.option_greyscale:
|
||||
elif palette == 'greyscale':
|
||||
# gray=int(0.299*r+0.587*g+0.114*b)
|
||||
gray = (r + g + b) // 3
|
||||
r = g = b = gray
|
||||
|
||||
@@ -20,7 +20,7 @@ class Dungeon1:
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
Location(dungeon=1).add(OwlStatue(0x10A)).connect(dungeon1_right_side, STONE_BEAK1)
|
||||
dungeon1_3_of_a_kind = Location(dungeon=1).add(DungeonChest(0x10A)).connect(dungeon1_right_side, OR(r.attack_hookshot_no_bomb, SHIELD)) # three of a kind, shield stops the suit from changing
|
||||
dungeon1_miniboss = Location(dungeon=1).connect(dungeon1_right_side, AND(r.miniboss_requirements[world_setup.miniboss_mapping[0]], FEATHER))
|
||||
dungeon1_miniboss = Location(dungeon=1).connect(dungeon1_right_side, AND(r.miniboss_requirements[world_setup.miniboss_mapping['0']], FEATHER))
|
||||
dungeon1_boss = Location(dungeon=1).connect(dungeon1_miniboss, NIGHTMARE_KEY1)
|
||||
boss = Location(dungeon=1).add(HeartContainer(0x106), Instrument(0x102)).connect(dungeon1_boss, r.boss_requirements[world_setup.boss_mapping[0]])
|
||||
|
||||
@@ -30,7 +30,7 @@ class Dungeon1:
|
||||
|
||||
if options.logic == 'glitched' or options.logic == 'hell':
|
||||
boss_key.connect(entrance, r.super_jump_feather) # super jump
|
||||
dungeon1_miniboss.connect(dungeon1_right_side, r.miniboss_requirements[world_setup.miniboss_mapping[0]]) # damage boost or buffer pause over the pit to cross or mushroom
|
||||
dungeon1_miniboss.connect(dungeon1_right_side, r.miniboss_requirements[world_setup.miniboss_mapping['0']]) # damage boost or buffer pause over the pit to cross or mushroom
|
||||
|
||||
if options.logic == 'hell':
|
||||
feather_chest.connect(dungeon1_upper_left, SWORD) # keep slashing the spiked beetles until they keep moving 1 pixel close towards you and the pit, to get them to fall
|
||||
|
||||
@@ -23,7 +23,7 @@ class Dungeon2:
|
||||
dungeon2_r5 = Location(dungeon=2).connect(dungeon2_r4, AND(KEY2, FOUND(KEY2, 3))) # push two blocks together room with owl statue
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
Location(dungeon=2).add(OwlStatue(0x12F)).connect(dungeon2_r5, STONE_BEAK2) # owl statue is before miniboss
|
||||
miniboss = Location(dungeon=2).add(DungeonChest(0x126)).add(DungeonChest(0x121)).connect(dungeon2_r5, AND(FEATHER, r.miniboss_requirements[world_setup.miniboss_mapping[1]])) # post hinox
|
||||
miniboss = Location(dungeon=2).add(DungeonChest(0x126)).add(DungeonChest(0x121)).connect(dungeon2_r5, AND(FEATHER, r.miniboss_requirements[world_setup.miniboss_mapping['1']])) # post hinox
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
Location(dungeon=2).add(OwlStatue(0x129)).connect(miniboss, STONE_BEAK2) # owl statue after the miniboss
|
||||
|
||||
@@ -45,7 +45,7 @@ class Dungeon2:
|
||||
dungeon2_map_chest.connect(dungeon2_l2, AND(r.attack_hookshot_powder, r.boots_bonk_pit)) # use boots to jump over the pits
|
||||
dungeon2_r4.connect(dungeon2_r3, OR(r.boots_bonk_pit, r.hookshot_spam_pit)) # can use both pegasus boots bonks or hookshot spam to cross the pit room
|
||||
dungeon2_r4.connect(shyguy_key_drop, r.rear_attack_range, one_way=True) # adjust for alternate requirements for dungeon2_r4
|
||||
miniboss.connect(dungeon2_r5, AND(r.boots_dash_2d, r.miniboss_requirements[world_setup.miniboss_mapping[1]])) # use boots to dash over the spikes in the 2d section
|
||||
miniboss.connect(dungeon2_r5, AND(r.boots_dash_2d, r.miniboss_requirements[world_setup.miniboss_mapping['1']])) # use boots to dash over the spikes in the 2d section
|
||||
dungeon2_pre_stairs_boss.connect(dungeon2_r6, AND(HOOKSHOT, OR(BOW, BOMB, MAGIC_ROD, AND(OCARINA, SONG1)), FOUND(KEY2, 5))) # hookshot clip through the pot using both pol's voice
|
||||
dungeon2_post_stairs_boss.connect(dungeon2_pre_stairs_boss, OR(BOMB, r.boots_jump)) # use a bomb to lower the last platform, or boots + feather to cross over top (only relevant in hell logic)
|
||||
dungeon2_pre_boss.connect(dungeon2_post_stairs_boss, AND(r.boots_bonk_pit, r.hookshot_spam_pit)) # boots bonk off bottom wall + hookshot spam across the two 1 tile pits vertically
|
||||
|
||||
@@ -33,7 +33,7 @@ class Dungeon3:
|
||||
Location(dungeon=3).add(DroppedKey(0x14D)).connect(area_right, r.attack_hookshot_powder) # key after the stairs.
|
||||
|
||||
dungeon3_nightmare_key_chest = Location(dungeon=3).add(DungeonChest(0x147)).connect(area_right, AND(BOMB, FEATHER, PEGASUS_BOOTS)) # nightmare key chest
|
||||
dungeon3_post_dodongo_chest = Location(dungeon=3).add(DungeonChest(0x146)).connect(area_right, AND(r.attack_hookshot_powder, r.miniboss_requirements[world_setup.miniboss_mapping[2]])) # boots after the miniboss
|
||||
dungeon3_post_dodongo_chest = Location(dungeon=3).add(DungeonChest(0x146)).connect(area_right, AND(r.attack_hookshot_powder, r.miniboss_requirements[world_setup.miniboss_mapping['2']])) # boots after the miniboss
|
||||
compass_chest = Location(dungeon=3).add(DungeonChest(0x142)).connect(area_right, OR(SWORD, BOMB, AND(SHIELD, r.attack_hookshot_powder))) # bomb only activates with sword, bomb or shield
|
||||
dungeon3_3_bombite_room = Location(dungeon=3).add(DroppedKey(0x141)).connect(compass_chest, BOMB) # 3 bombite room
|
||||
Location(dungeon=3).add(DroppedKey(0x148)).connect(area_right, r.attack_no_boomerang) # 2 zol 2 owl drop key
|
||||
|
||||
@@ -29,7 +29,7 @@ class Dungeon4:
|
||||
left_water_area = Location(dungeon=4).connect(before_miniboss, OR(FEATHER, FLIPPERS)) # area left with zol chest and 5 symbol puzzle (water area)
|
||||
left_water_area.add(DungeonChest(0x16D)) # gel chest
|
||||
left_water_area.add(DungeonChest(0x168)) # key chest near the puzzle
|
||||
miniboss = Location(dungeon=4).connect(before_miniboss, AND(KEY4, FOUND(KEY4, 5), r.miniboss_requirements[world_setup.miniboss_mapping[3]]))
|
||||
miniboss = Location(dungeon=4).connect(before_miniboss, AND(KEY4, FOUND(KEY4, 5), r.miniboss_requirements[world_setup.miniboss_mapping['3']]))
|
||||
terrace_zols_chest = Location(dungeon=4).connect(before_miniboss, FLIPPERS) # flippers to move around miniboss through 5 tile room
|
||||
miniboss = Location(dungeon=4).connect(terrace_zols_chest, POWER_BRACELET, one_way=True) # reach flippers chest through the miniboss room
|
||||
terrace_zols_chest.add(DungeonChest(0x160)) # flippers chest
|
||||
|
||||
@@ -15,7 +15,7 @@ class Dungeon5:
|
||||
Location(dungeon=5).add(OwlStatue(0x19A)).connect(area2, STONE_BEAK5)
|
||||
Location(dungeon=5).add(DungeonChest(0x19B)).connect(area2, r.attack_hookshot_powder) # map chest
|
||||
blade_trap_chest = Location(dungeon=5).add(DungeonChest(0x197)).connect(area2, HOOKSHOT) # key chest on the left
|
||||
post_gohma = Location(dungeon=5).connect(area2, AND(HOOKSHOT, r.miniboss_requirements[world_setup.miniboss_mapping[4]], KEY5, FOUND(KEY5,2))) # staircase after gohma
|
||||
post_gohma = Location(dungeon=5).connect(area2, AND(HOOKSHOT, r.miniboss_requirements[world_setup.miniboss_mapping['4']], KEY5, FOUND(KEY5,2))) # staircase after gohma
|
||||
staircase_before_boss = Location(dungeon=5).connect(post_gohma, AND(HOOKSHOT, FEATHER)) # bottom right section pits room before boss door. Path via gohma
|
||||
after_keyblock_boss = Location(dungeon=5).connect(staircase_before_boss, AND(KEY5, FOUND(KEY5, 3))) # top right section pits room before boss door
|
||||
after_stalfos = Location(dungeon=5).add(DungeonChest(0x196)).connect(area2, AND(SWORD, BOMB)) # Need to defeat master stalfos once for this empty chest; l2 sword beams kill but obscure
|
||||
|
||||
@@ -24,7 +24,7 @@ class Dungeon6:
|
||||
|
||||
# right side
|
||||
to_miniboss = Location(dungeon=6).connect(entrance, KEY6)
|
||||
miniboss = Location(dungeon=6).connect(to_miniboss, AND(BOMB, r.miniboss_requirements[world_setup.miniboss_mapping[5]]))
|
||||
miniboss = Location(dungeon=6).connect(to_miniboss, AND(BOMB, r.miniboss_requirements[world_setup.miniboss_mapping['5']]))
|
||||
lower_right_side = Location(dungeon=6).add(DungeonChest(0x1BE)).connect(entrance, AND(r.attack_wizrobe, COUNT(POWER_BRACELET, 2))) # waterway key
|
||||
medicine_chest = Location(dungeon=6).add(DungeonChest(0x1D1)).connect(lower_right_side, FEATHER) # ledge chest medicine
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
|
||||
@@ -22,7 +22,7 @@ class Dungeon7:
|
||||
# Most of the dungeon can be accessed at this point.
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
bottomleft_owl = Location(dungeon=7).add(OwlStatue(0x21C)).connect(bottomleftF2_area, AND(BOMB, STONE_BEAK7))
|
||||
nightmare_key = Location(dungeon=7).add(DungeonChest(0x224)).connect(bottomleftF2_area, r.miniboss_requirements[world_setup.miniboss_mapping[6]]) # nightmare key after the miniboss
|
||||
nightmare_key = Location(dungeon=7).add(DungeonChest(0x224)).connect(bottomleftF2_area, r.miniboss_requirements[world_setup.miniboss_mapping['6']]) # nightmare key after the miniboss
|
||||
mirror_shield_chest = Location(dungeon=7).add(DungeonChest(0x21A)).connect(bottomleftF2_area, r.hit_switch) # mirror shield chest, need to be able to hit a switch to reach or
|
||||
bottomleftF2_area.connect(mirror_shield_chest, AND(KEY7, FOUND(KEY7, 3)), one_way = True) # reach mirror shield chest from hinox area by opening keyblock
|
||||
toprightF1_chest = Location(dungeon=7).add(DungeonChest(0x204)).connect(bottomleftF2_area, r.hit_switch) # chest on the F1 right ledge. Added attack_hookshot since switch needs to be hit to get back up
|
||||
|
||||
@@ -40,7 +40,7 @@ class Dungeon8:
|
||||
middle_center_2 = Location(dungeon=8).connect(middle_center_1, AND(KEY8, FOUND(KEY8, 4)))
|
||||
middle_center_3 = Location(dungeon=8).connect(middle_center_2, KEY8)
|
||||
miniboss_entrance = Location(dungeon=8).connect(middle_center_3, AND(HOOKSHOT, KEY8, FOUND(KEY8, 7))) # hookshot to get across to keyblock, 7 to fix keylock issues if keys are used on other keyblocks
|
||||
miniboss = Location(dungeon=8).connect(miniboss_entrance, AND(FEATHER, r.miniboss_requirements[world_setup.miniboss_mapping[7]])) # feather for 2d section, sword to kill
|
||||
miniboss = Location(dungeon=8).connect(miniboss_entrance, AND(FEATHER, r.miniboss_requirements[world_setup.miniboss_mapping['7']])) # feather for 2d section, sword to kill
|
||||
miniboss.add(DungeonChest(0x237)) # fire rod chest
|
||||
|
||||
up_left = Location(dungeon=8).connect(upper_center, AND(r.attack_hookshot_powder, AND(KEY8, FOUND(KEY8, 4))))
|
||||
@@ -94,7 +94,7 @@ class Dungeon8:
|
||||
entrance.connect(bottomright_pot_chest, r.shaq_jump, one_way=True) # use NW zamboni staircase backwards, and get a naked shaq jump off the bottom wall in the bottom right corner to pass by the pot
|
||||
gibdos_drop_key.connect(upper_center, AND(FEATHER, SHIELD)) # lock gibdos into pits and crack the tile they stand on, then use shield to bump them into the pit
|
||||
medicine_chest.connect(upper_center, AND(r.pit_buffer_boots, HOOKSHOT)) # boots bonk + lava buffer to the bottom wall, then bonk onto the middle section
|
||||
miniboss.connect(miniboss_entrance, AND(r.boots_bonk_2d_hell, r.miniboss_requirements[world_setup.miniboss_mapping[7]])) # get through 2d section with boots bonks
|
||||
miniboss.connect(miniboss_entrance, AND(r.boots_bonk_2d_hell, r.miniboss_requirements[world_setup.miniboss_mapping['7']])) # get through 2d section with boots bonks
|
||||
top_left_stairs.connect(map_chest, AND(r.jesus_buffer, r.boots_bonk_2d_hell, MAGIC_ROD)) # boots bonk + lava buffer from map chest to entrance_up, then boots bonk through 2d section
|
||||
nightmare_key.connect(top_left_stairs, AND(r.boots_bonk_pit, SWORD, FOUND(KEY8, 7))) # use a boots bonk to cross the 2d section + the lava in cueball room
|
||||
bottom_right.connect(entrance_up, AND(POWER_BRACELET, r.jesus_buffer), one_way=True) # take staircase to NW zamboni room, boots bonk onto the lava and water buffer all the way down to push the zamboni
|
||||
|
||||
@@ -541,8 +541,8 @@ OAMData:
|
||||
rom.banks[0x38][0x1400+n*0x20:0x1410+n*0x20] = utils.createTileData(gfx_high)
|
||||
rom.banks[0x38][0x1410+n*0x20:0x1420+n*0x20] = utils.createTileData(gfx_low)
|
||||
|
||||
def addBootsControls(rom, boots_controls: int):
|
||||
if boots_controls == BootsControls.option_vanilla:
|
||||
def addBootsControls(rom, bootscontrols):
|
||||
if bootscontrols == 'vanilla':
|
||||
return
|
||||
consts = {
|
||||
"INVENTORY_PEGASUS_BOOTS": 0x8,
|
||||
@@ -560,25 +560,25 @@ def addBootsControls(rom, boots_controls: int):
|
||||
|
||||
BOOTS_START_ADDR = 0x11E8
|
||||
condition = {
|
||||
BootsControls.option_bracelet: """
|
||||
'bracelet': """
|
||||
ld a, [hl]
|
||||
; Check if we are using the bracelet
|
||||
cp INVENTORY_POWER_BRACELET
|
||||
jr z, .yesBoots
|
||||
""",
|
||||
BootsControls.option_press_a: """
|
||||
'pressa': """
|
||||
; Check if we are using the A slot
|
||||
cp J_A
|
||||
jr z, .yesBoots
|
||||
ld a, [hl]
|
||||
""",
|
||||
BootsControls.option_press_b: """
|
||||
'pressb': """
|
||||
; Check if we are using the B slot
|
||||
cp J_B
|
||||
jr z, .yesBoots
|
||||
ld a, [hl]
|
||||
"""
|
||||
}[boots_controls]
|
||||
}[bootscontrols]
|
||||
|
||||
# The new code fits exactly within Nintendo's poorly space optimzied code while having more features
|
||||
boots_code = assembler.ASM("""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -68,7 +68,7 @@ class Setting:
|
||||
|
||||
|
||||
class Settings:
|
||||
def __init__(self, ap_options):
|
||||
def __init__(self, settings_dict):
|
||||
self.__all = [
|
||||
Setting('seed', 'Main', '<', 'Seed', placeholder='Leave empty for random seed', default="", multiworld=False,
|
||||
description="""For multiple people to generate the same randomization result, enter the generated seed number here.
|
||||
@@ -178,6 +178,14 @@ Note, some entrances can lead into water, use the warp-to-home from the save&qui
|
||||
description='Replaces the hints from owl statues with additional randomized items'),
|
||||
Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False,
|
||||
description='All items will be more powerful, faster, harder, bigger stronger. You name it.'),
|
||||
Setting('trendygame', 'Special', 'a', 'Trendy Game', description="",
|
||||
options=[('easy', 'e', 'Easy'), ('normal', 'n', 'Normal'), ('hard', 'h', 'Hard'), ('harder', 'r', 'Harder'), ('hardest', 't', 'Hardest'), ('impossible', 'i', 'Impossible')], default='normal'),
|
||||
Setting('warps', 'Special', 'a', 'Warps', description="",
|
||||
options=[('vanilla', 'v', 'Vanilla'), ('improved', 'i', 'Improved'), ('improvedadditional', 'a', 'Improved Additional')], default='vanilla'),
|
||||
Setting('shufflenightmarekeys', 'Special', 'a', 'Shuffle Nightmare Keys', description="",
|
||||
options=[('originaldungeon', '0', 'Original Dungeon'), ('owndungeons', '1', 'Own Dungeons'), ('ownworld', '2', 'Own World'), ('anyworld', '3', 'Any World'), ('differentworld', '4', 'Different World')], default="originaldungeon"),
|
||||
Setting('shufflesmallkeys', 'Special', 'a', 'Shuffle Small Keys', description="",
|
||||
options=[('originaldungeon', '0', 'Original Dungeon'), ('owndungeons', '1', 'Own Dungeons'), ('ownworld', '2', 'Own World'), ('anyworld', '3', 'Any World'), ('differentworld', '4', 'Different World')], default="originaldungeon"),
|
||||
Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none',
|
||||
description='Adds that the select button swaps with either A or B. The item is swapped with the top inventory slot. The map is not available when quickswap is enabled.',
|
||||
aesthetic=True),
|
||||
@@ -192,7 +200,7 @@ Note, some entrances can lead into water, use the warp-to-home from the save&qui
|
||||
Setting('nagmessages', 'User options', 'S', 'Show nag messages', default=False,
|
||||
description='Enables the nag messages normally shown when touching stones and crystals',
|
||||
aesthetic=True),
|
||||
Setting('gfxmod', 'User options', 'c', 'Graphics', default='',
|
||||
Setting('gfxmod', 'User options', 'c', 'Graphics', default=False,
|
||||
description='Generally affects at least Link\'s sprite, but can alter any graphics in the game',
|
||||
aesthetic=True),
|
||||
Setting('linkspalette', 'User options', 'C', "Link's color",
|
||||
@@ -202,25 +210,31 @@ Note, some entrances can lead into water, use the warp-to-home from the save&qui
|
||||
[Normal] color of link depends on the tunic.
|
||||
[Green/Yellow/Red/Blue] forces link into one of these colors.
|
||||
[?? A/B/C/D] colors of link are usually inverted and color depends on the area you are in."""),
|
||||
Setting('palette', 'User options', 'a', 'Palette', description="",
|
||||
options=[('normal', 'n', 'Normal'), ('1bit', '1', '1 Bit'), ('2bit', '2', '2 Bit'), ('greyscale', 'g', 'Greyscale'), ('pink', 'p', 'Pink'), ('inverted', 'i', 'Inverted')], default='normal', aesthetic=True),
|
||||
Setting('music', 'User options', 'M', 'Music', options=[('', '', 'Default'), ('random', 'r', 'Random'), ('off', 'o', 'Disable')], default='',
|
||||
description="""
|
||||
[Random] Randomizes overworld and dungeon music'
|
||||
[Disable] no music in the whole game""",
|
||||
aesthetic=True),
|
||||
Setting('musicchange', 'User options', 'a', 'Music Change Condition', description="",
|
||||
options=[('always', 'a', 'Always'), ('sword', 's', 'Sword')], default='always', aesthetic=True),
|
||||
Setting('bootscontrols', 'User options', 'a', 'Boots Controls', description="",
|
||||
options=[('vanilla', 'v', 'Vanilla'), ('bracelet', 'p', 'Bracelet'), ('pressa', 'a', 'Press A'), ('pressb', 'b', 'Press B')], default='vanilla', aesthetic=True),
|
||||
Setting('foreignitemicons', 'User options', 'a', 'Foreign Item Icons', description="",
|
||||
options=[('guessbyname', 'g', 'Guess By Name'), ('indicateprogression', 'p', 'Indicate Progression')], default="guessbyname", aesthetic=True),
|
||||
Setting('aptitlescreen', 'User options', 'a', 'AP Title Screen', description="", default=True),
|
||||
Setting('textshuffle', 'User options', 'a', 'Text Shuffle', description="", default=False),
|
||||
]
|
||||
self.__by_key = {s.key: s for s in self.__all}
|
||||
|
||||
# Make sure all short keys are unique
|
||||
short_keys = set()
|
||||
for s in self.__all:
|
||||
assert s.short_key not in short_keys, s.label
|
||||
short_keys.add(s.short_key)
|
||||
self.ap_options = ap_options
|
||||
# don't worry about unique short keys for AP
|
||||
#short_keys = set()
|
||||
#for s in self.__all:
|
||||
# assert s.short_key not in short_keys, s.label
|
||||
# short_keys.add(s.short_key)
|
||||
|
||||
for option in self.ap_options.values():
|
||||
if not hasattr(option, 'to_ladxr_option'):
|
||||
continue
|
||||
name, value = option.to_ladxr_option(self.ap_options)
|
||||
for name, value in settings_dict.items():
|
||||
if value == "true":
|
||||
value = 1
|
||||
elif value == "false":
|
||||
|
||||
@@ -28,7 +28,7 @@ class WorldSetup:
|
||||
self.boss_mapping = list(range(9))
|
||||
self.miniboss_mapping = {
|
||||
# Main minibosses
|
||||
0: "ROLLING_BONES", 1: "HINOX", 2: "DODONGO", 3: "CUE_BALL", 4: "GHOMA", 5: "SMASHER", 6: "GRIM_CREEPER", 7: "BLAINO",
|
||||
'0': "ROLLING_BONES", '1': "HINOX", '2': "DODONGO", '3': "CUE_BALL", '4': "GHOMA", '5': "SMASHER", '6': "GRIM_CREEPER", '7': "BLAINO",
|
||||
# Color dungeon needs to be special, as always.
|
||||
"c1": "AVALAUNCH", "c2": "GIANT_BUZZ_BLOB",
|
||||
# Overworld
|
||||
|
||||
@@ -60,11 +60,12 @@ class TradeQuest(DefaultOffToggle, LADXROption):
|
||||
ladxr_name = "tradequest"
|
||||
|
||||
|
||||
class TextShuffle(DefaultOffToggle):
|
||||
class TextShuffle(DefaultOffToggle, LADXROption):
|
||||
"""
|
||||
Shuffles all text in the game.
|
||||
"""
|
||||
display_name = "Text Shuffle"
|
||||
ladxr_name = "textshuffle"
|
||||
|
||||
|
||||
class Rooster(DefaultOnToggle, LADXROption):
|
||||
@@ -112,11 +113,12 @@ class DungeonShuffle(DefaultOffToggle, LADXROption):
|
||||
ladxr_name = "dungeonshuffle"
|
||||
|
||||
|
||||
class APTitleScreen(DefaultOnToggle):
|
||||
class APTitleScreen(DefaultOnToggle, LADXROption):
|
||||
"""
|
||||
Enables AP specific title screen and disables the intro cutscene.
|
||||
"""
|
||||
display_name = "AP Title Screen"
|
||||
ladxr_name = "aptitlescreen"
|
||||
|
||||
|
||||
class BossShuffle(Choice):
|
||||
@@ -142,7 +144,7 @@ class DungeonItemShuffle(Choice):
|
||||
ladxr_item: str
|
||||
|
||||
|
||||
class ShuffleNightmareKeys(DungeonItemShuffle):
|
||||
class ShuffleNightmareKeys(DungeonItemShuffle, LADXROption):
|
||||
"""
|
||||
**Original Dungeon:** The item will be within its original dungeon.
|
||||
|
||||
@@ -156,9 +158,10 @@ class ShuffleNightmareKeys(DungeonItemShuffle):
|
||||
"""
|
||||
display_name = "Shuffle Nightmare Keys"
|
||||
ladxr_item = "NIGHTMARE_KEY"
|
||||
ladxr_name = "shufflenightmarekeys"
|
||||
|
||||
|
||||
class ShuffleSmallKeys(DungeonItemShuffle):
|
||||
class ShuffleSmallKeys(DungeonItemShuffle, LADXROption):
|
||||
"""
|
||||
**Original Dungeon:** The item will be within its original dungeon.
|
||||
|
||||
@@ -172,6 +175,7 @@ class ShuffleSmallKeys(DungeonItemShuffle):
|
||||
"""
|
||||
display_name = "Shuffle Small Keys"
|
||||
ladxr_item = "KEY"
|
||||
ladxr_name = "shufflesmallkeys"
|
||||
|
||||
|
||||
class ShuffleMaps(DungeonItemShuffle):
|
||||
@@ -266,7 +270,7 @@ class Goal(Choice, LADXROption):
|
||||
|
||||
def to_ladxr_option(self, all_options):
|
||||
if self.value == self.option_instruments:
|
||||
return ("goal", all_options["instrument_count"])
|
||||
return ("goal", int(all_options["instrument_count"]))
|
||||
else:
|
||||
return LADXROption.to_ladxr_option(self, all_options)
|
||||
|
||||
@@ -291,7 +295,7 @@ class NagMessages(DefaultOffToggle, LADXROption):
|
||||
ladxr_name = "nagmessages"
|
||||
|
||||
|
||||
class MusicChangeCondition(Choice):
|
||||
class MusicChangeCondition(Choice, LADXROption):
|
||||
"""
|
||||
Controls how the music changes.
|
||||
|
||||
@@ -304,6 +308,7 @@ class MusicChangeCondition(Choice):
|
||||
option_sword = 0
|
||||
option_always = 1
|
||||
default = option_always
|
||||
ladxr_name = "musicchange"
|
||||
|
||||
|
||||
class HardMode(Choice, LADXROption):
|
||||
@@ -396,7 +401,7 @@ class NoFlash(DefaultOnToggle, LADXROption):
|
||||
ladxr_name = "noflash"
|
||||
|
||||
|
||||
class BootsControls(Choice):
|
||||
class BootsControls(Choice, LADXROption):
|
||||
"""
|
||||
Adds an additional button to activate Pegasus Boots (does nothing if you
|
||||
haven't picked up your boots!)
|
||||
@@ -418,6 +423,7 @@ class BootsControls(Choice):
|
||||
alias_a = 2
|
||||
option_press_b = 3
|
||||
alias_b = 3
|
||||
ladxr_name = "bootscontrols"
|
||||
|
||||
|
||||
class LinkPalette(Choice, LADXROption):
|
||||
@@ -444,7 +450,7 @@ class LinkPalette(Choice, LADXROption):
|
||||
return self.ladxr_name, str(self.value)
|
||||
|
||||
|
||||
class TrendyGame(Choice):
|
||||
class TrendyGame(Choice, LADXROption):
|
||||
"""
|
||||
**Easy:** All of the items hold still for you.
|
||||
|
||||
@@ -468,16 +474,18 @@ class TrendyGame(Choice):
|
||||
option_hardest = 4
|
||||
option_impossible = 5
|
||||
default = option_normal
|
||||
ladxr_name = "trendygame"
|
||||
|
||||
|
||||
class GfxMod(DefaultOffToggle):
|
||||
class GfxMod(DefaultOffToggle, LADXROption):
|
||||
"""
|
||||
If enabled, the patcher will prompt the user for a modification file to change sprites in the game and optionally some text.
|
||||
"""
|
||||
display_name = "GFX Modification"
|
||||
ladxr_name = "gfxmod"
|
||||
|
||||
|
||||
class Palette(Choice):
|
||||
class Palette(Choice, LADXROption):
|
||||
"""
|
||||
Sets the palette for the game.
|
||||
|
||||
@@ -504,6 +512,7 @@ class Palette(Choice):
|
||||
option_greyscale = 3
|
||||
option_pink = 4
|
||||
option_inverted = 5
|
||||
ladxr_name = "palette"
|
||||
|
||||
|
||||
class Music(Choice, LADXROption):
|
||||
@@ -530,7 +539,7 @@ class Music(Choice, LADXROption):
|
||||
return self.ladxr_name, s
|
||||
|
||||
|
||||
class Warps(Choice):
|
||||
class Warps(Choice, LADXROption):
|
||||
"""
|
||||
**Improved:** Adds remake style warp screen to the game. Choose your warp
|
||||
destination on the map after jumping in a portal and press *B* to select.
|
||||
@@ -544,6 +553,7 @@ class Warps(Choice):
|
||||
option_improved = 1
|
||||
option_improved_additional = 2
|
||||
default = option_vanilla
|
||||
ladxr_name = 'warps'
|
||||
|
||||
|
||||
class InGameHints(DefaultOnToggle):
|
||||
@@ -583,7 +593,7 @@ class StabilizeItemPool(DefaultOffToggle):
|
||||
rich_text_doc = True
|
||||
|
||||
|
||||
class ForeignItemIcons(Choice):
|
||||
class ForeignItemIcons(Choice, LADXROption):
|
||||
"""
|
||||
Choose how to display foreign items.
|
||||
|
||||
@@ -597,6 +607,7 @@ class ForeignItemIcons(Choice):
|
||||
option_guess_by_name = 0
|
||||
option_indicate_progression = 1
|
||||
default = option_guess_by_name
|
||||
ladxr_name = 'foreignitemicons'
|
||||
|
||||
|
||||
ladx_option_groups = [
|
||||
|
||||
@@ -6,13 +6,11 @@ import json
|
||||
import pkgutil
|
||||
import bsdiff4
|
||||
import binascii
|
||||
import pickle
|
||||
from typing import TYPE_CHECKING
|
||||
from .Common import *
|
||||
from .LADXR import generator
|
||||
from .LADXR.main import get_parser
|
||||
from .LADXR.hints import generate_hint_texts
|
||||
from .LADXR.locations.keyLocation import KeyLocation
|
||||
LADX_HASH = "07c211479386825042efb4ad31bb525f"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -35,7 +33,7 @@ class LADXPatchExtensions(worlds.Files.APPatchExtension):
|
||||
@staticmethod
|
||||
def patch_title_screen(caller: worlds.Files.APProcedurePatch, rom: bytes, data_file: str) -> bytes:
|
||||
patch_data = json.loads(caller.get_file(data_file).decode("utf-8"))
|
||||
if patch_data["options"]["ap_title_screen"]:
|
||||
if patch_data["ladxr_settings_dict"]["aptitlescreen"] == 'true':
|
||||
return bsdiff4.patch(rom, pkgutil.get_data(__name__, "LADXR/patches/title_screen.bdiff4"))
|
||||
return rom
|
||||
|
||||
@@ -56,7 +54,6 @@ class LADXProcedurePatch(worlds.Files.APProcedurePatch):
|
||||
|
||||
|
||||
def write_patch_data(world: "LinksAwakeningWorld", patch: LADXProcedurePatch):
|
||||
item_list = pickle.dumps([item for item in world.ladxr_logic.iteminfo_list if not isinstance(item, KeyLocation)])
|
||||
data_dict = {
|
||||
"generated_world_version": world.world_version.as_simple_string(),
|
||||
"out_base": world.multiworld.get_out_file_name_base(patch.player),
|
||||
@@ -67,44 +64,16 @@ def write_patch_data(world: "LinksAwakeningWorld", patch: LADXProcedurePatch):
|
||||
"player": patch.player,
|
||||
"player_name": patch.player_name,
|
||||
"other_player_names": list(world.multiworld.player_name.values()),
|
||||
"item_list": binascii.hexlify(item_list).decode(),
|
||||
"rom_item_placements": world.rom_item_placements,
|
||||
"hint_texts": generate_hint_texts(world),
|
||||
"world_setup": {
|
||||
"goal": world.ladxr_logic.world_setup.goal,
|
||||
"bingo_goals": world.ladxr_logic.world_setup.bingo_goals,
|
||||
"multichest": world.ladxr_logic.world_setup.multichest,
|
||||
"entrance_mapping": world.ladxr_logic.world_setup.entrance_mapping,
|
||||
"boss_mapping": world.ladxr_logic.world_setup.boss_mapping,
|
||||
"miniboss_mapping": world.ladxr_logic.world_setup.miniboss_mapping,
|
||||
},
|
||||
"options": world.options.as_dict(
|
||||
"tradequest",
|
||||
"rooster",
|
||||
"experimental_dungeon_shuffle",
|
||||
"experimental_entrance_shuffle",
|
||||
"goal",
|
||||
"instrument_count",
|
||||
"link_palette",
|
||||
"warps",
|
||||
"trendy_game",
|
||||
"gfxmod",
|
||||
"palette",
|
||||
"text_shuffle",
|
||||
"shuffle_nightmare_keys",
|
||||
"shuffle_small_keys",
|
||||
"music",
|
||||
"music_change_condition",
|
||||
"nag_messages",
|
||||
"ap_title_screen",
|
||||
"boots_controls",
|
||||
"stealing",
|
||||
"quickswap",
|
||||
"hard_mode",
|
||||
"low_hp_beep",
|
||||
"text_mode",
|
||||
"no_flash",
|
||||
"overworld",
|
||||
),
|
||||
"ladxr_settings_dict": world.ladxr_settings_dict,
|
||||
}
|
||||
patch.write_file("data.json", json.dumps(data_dict).encode('utf-8'))
|
||||
|
||||
|
||||
@@ -180,7 +180,17 @@ class LinksAwakeningWorld(World):
|
||||
}
|
||||
|
||||
def convert_ap_options_to_ladxr_logic(self):
|
||||
self.ladxr_settings = LADXRSettings(dataclasses.asdict(self.options))
|
||||
# store a dict of ladxr settings as a middle step so that we can also create a
|
||||
# ladxr settings object on the other side of the patch
|
||||
options_dict = dataclasses.asdict(self.options)
|
||||
self.ladxr_settings_dict = {}
|
||||
for option in options_dict.values():
|
||||
if not hasattr(option, 'to_ladxr_option'):
|
||||
continue
|
||||
name, value = option.to_ladxr_option(options_dict)
|
||||
if name:
|
||||
self.ladxr_settings_dict[name] = value
|
||||
self.ladxr_settings = LADXRSettings(self.ladxr_settings_dict)
|
||||
|
||||
self.ladxr_settings.validate()
|
||||
world_setup = LADXRWorldSetup()
|
||||
@@ -503,36 +513,36 @@ class LinksAwakeningWorld(World):
|
||||
return "TRADING_ITEM_LETTER"
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
# copy items back to locations
|
||||
self.rom_item_placements = []
|
||||
for r in self.multiworld.get_regions(self.player):
|
||||
for loc in r.locations:
|
||||
if isinstance(loc, LinksAwakeningLocation):
|
||||
assert(loc.item)
|
||||
|
||||
spot = {}
|
||||
# If we're a links awakening item, just use the item
|
||||
if isinstance(loc.item, LinksAwakeningItem):
|
||||
loc.ladxr_item.item = loc.item.item_data.ladxr_id
|
||||
spot["item"] = loc.item.item_data.ladxr_id
|
||||
|
||||
# If the item name contains "sword", use a sword icon, etc
|
||||
# Otherwise, use a cute letter as the icon
|
||||
elif self.options.foreign_item_icons == 'guess_by_name':
|
||||
loc.ladxr_item.item = self.guess_icon_for_other_world(loc.item)
|
||||
loc.ladxr_item.setCustomItemName(loc.item.name)
|
||||
spot["item"] = self.guess_icon_for_other_world(loc.item)
|
||||
|
||||
else:
|
||||
if loc.item.advancement:
|
||||
loc.ladxr_item.item = 'PIECE_OF_POWER'
|
||||
spot["item"] = 'PIECE_OF_POWER'
|
||||
else:
|
||||
loc.ladxr_item.item = 'GUARDIAN_ACORN'
|
||||
loc.ladxr_item.setCustomItemName(loc.item.name)
|
||||
spot["item"] = 'GUARDIAN_ACORN'
|
||||
|
||||
spot["custom_item_name"] = loc.item.name
|
||||
|
||||
if loc.item:
|
||||
loc.ladxr_item.item_owner = loc.item.player
|
||||
spot["item_owner"] = loc.item.player
|
||||
else:
|
||||
loc.ladxr_item.item_owner = self.player
|
||||
spot["item_owner"] = self.player
|
||||
|
||||
# Kind of kludge, make it possible for the location to differentiate between local and remote items
|
||||
loc.ladxr_item.location_owner = self.player
|
||||
spot["name_id"] = loc.ladxr_item.nameId
|
||||
self.rom_item_placements.append(spot)
|
||||
|
||||
|
||||
patch = LADXProcedurePatch(player=self.player, player_name=self.player_name)
|
||||
|
||||
Reference in New Issue
Block a user